use crate::ast::{
ArrangeItem, GroupDef, PatternDef, PitchRoot, Program, ScaleMode, SourceLine, Transform,
};
use crate::error::{CompileError, CompileErrorKind, SourceLocation};
#[derive(Debug, Clone, PartialEq)]
pub struct Section {
pub name: String,
pub muted: bool,
pub cycles: u32,
pub bpm: u32,
pub sig: (u8, u8),
pub scale: Option<(PitchRoot, ScaleMode)>,
pub patterns: Vec<PatternDef>,
pub groups: Vec<GroupDef>,
}
impl Section {
pub fn cycle_seconds(&self) -> f64 {
cycle_seconds(self.bpm, self.sig)
}
pub fn seconds(&self) -> f64 {
self.cycle_seconds() * f64::from(self.cycles)
}
pub fn audible_on(&self, cycle: u32) -> impl Iterator<Item = &PatternDef> {
let cycles = self.cycles;
let muted = self.muted;
self.patterns.iter().filter(move |pattern| {
!muted && !pattern.muted && pattern.span.is_none_or(|span| span.contains(cycle, cycles))
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Occurrence {
pub section: usize,
pub start_cycle: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Piece {
pub sections: Vec<Section>,
pub arrangement: Vec<ArrangeItem>,
pub timeline: Vec<Occurrence>,
pub throughout: Vec<PatternDef>,
pub tail: f64,
pub seed: u64,
pub unused: Vec<String>,
pub metadata: Vec<(String, String)>,
}
pub const DEFAULT_TAIL: f64 = 2.0;
impl Piece {
pub fn total_cycles(&self) -> u64 {
self.timeline
.iter()
.map(|occurrence| u64::from(self.sections[occurrence.section].cycles))
.sum()
}
pub fn seconds(&self) -> f64 {
self.timeline
.iter()
.map(|occurrence| self.sections[occurrence.section].seconds())
.sum()
}
pub fn meta(&self, key: &str) -> Option<&str> {
self.metadata
.iter()
.find(|(name, _)| name == key)
.map(|(_, value)| value.as_str())
}
pub fn render_seconds(&self) -> f64 {
self.seconds() + self.tail
}
pub fn section_index(&self, name: &str) -> Option<usize> {
self.sections
.iter()
.position(|section| section.name == name)
}
pub fn first_playing(&self, section: usize) -> u64 {
self.timeline
.iter()
.find(|occurrence| occurrence.section == section)
.map_or(0, |occurrence| occurrence.start_cycle)
}
pub fn isolate(&self, section: usize, passes: u32) -> Piece {
let kept = self.sections[section].clone();
let passes = passes.max(1);
let origin = self.first_playing(section);
Piece {
arrangement: vec![ArrangeItem {
section: kept.name.clone(),
repeat: passes,
}],
timeline: (0..passes)
.map(|pass| Occurrence {
section: 0,
start_cycle: origin + u64::from(pass) * u64::from(kept.cycles),
})
.collect(),
sections: vec![kept],
throughout: self.throughout.clone(),
tail: self.tail,
seed: self.seed,
unused: Vec::new(),
metadata: self.metadata.clone(),
}
}
pub fn played_instruments(&self) -> Vec<&str> {
let mut played: Vec<usize> = self.timeline.iter().map(|o| o.section).collect();
played.sort_unstable();
played.dedup();
let mut instruments: Vec<&str> = played
.into_iter()
.flat_map(|index| &self.sections[index].patterns)
.chain(&self.throughout)
.map(|line| line.instrument.as_str())
.collect();
instruments.sort_unstable();
instruments.dedup();
instruments
}
pub fn mute_except(&mut self, keep: &[&str]) {
for line in self
.sections
.iter_mut()
.flat_map(|section| section.patterns.iter_mut())
.chain(self.throughout.iter_mut())
{
if !keep.contains(&line.instrument.as_str()) {
line.muted = true;
}
}
}
pub fn walk(&self) -> impl Iterator<Item = PlacedSection<'_>> {
let mut at_seconds = 0.0;
self.timeline.iter().map(move |occurrence| {
let section = &self.sections[occurrence.section];
let placed = PlacedSection {
section,
start_cycle: occurrence.start_cycle,
start_seconds: at_seconds,
};
at_seconds += section.seconds();
placed
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlacedSection<'a> {
pub section: &'a Section,
pub start_cycle: u64,
pub start_seconds: f64,
}
pub fn cycle_seconds(bpm: u32, signature: (u8, u8)) -> f64 {
let quarters = f64::from(signature.0) * 4.0 / f64::from(signature.1.max(1));
quarters * 60.0 / f64::from(bpm.max(1))
}
pub fn is_piece(program: &Program) -> bool {
program
.lines
.iter()
.any(|line| matches!(line, SourceLine::SectionStart { .. }))
}
fn error(line: usize, message: String) -> CompileError {
CompileError {
kind: CompileErrorKind::ParseError,
location: SourceLocation {
line,
column: 1,
file: None,
},
message,
suggestion: None,
}
}
#[derive(Clone, Copy)]
struct Ambient {
bpm: u32,
sig: (u8, u8),
scale: Option<(PitchRoot, ScaleMode)>,
}
pub fn resolve(
program: &Program,
defaults: (u32, (u8, u8), Option<(PitchRoot, ScaleMode)>),
) -> (Piece, Vec<CompileError>) {
let mut errors = Vec::new();
let mut ambient = Ambient {
bpm: defaults.0,
sig: defaults.1,
scale: defaults.2,
};
let mut sections: Vec<Section> = Vec::new();
let mut throughout: Vec<PatternDef> = Vec::new();
let mut arrangement: Option<Vec<ArrangeItem>> = None;
let mut tail = DEFAULT_TAIL;
let mut seed = 0u64;
let mut metadata: Vec<(String, String)> = Vec::new();
let mut open: Option<(Section, Ambient)> = None;
let mut open_group: Option<(bool, String, usize)> = None;
for (index, line) in program.lines.iter().enumerate() {
let at = index + 1;
match line {
SourceLine::Bpm(value) => match &mut open {
Some((section, _)) => section.bpm = *value,
None => ambient.bpm = *value,
},
SourceLine::Sig(num, den) => match &mut open {
Some((section, _)) => section.sig = (*num, *den),
None => ambient.sig = (*num, *den),
},
SourceLine::Scale(root, mode) => match &mut open {
Some((section, _)) => section.scale = Some((*root, *mode)),
None => ambient.scale = Some((*root, *mode)),
},
SourceLine::Phrase(_) => {
errors.push(error(
at,
"phrase: a piece is not edited while it plays, so it has no boundary to \
land a change on — drop the line (§2.6)"
.to_string(),
));
}
SourceLine::Tail(seconds) => tail = *seconds,
SourceLine::Seed(value) => seed = *value,
SourceLine::Meta(key, value) => {
metadata.retain(|(existing, _)| existing != key);
metadata.push((key.clone(), value.clone()));
}
SourceLine::Arrange(items) => {
if arrangement.is_some() {
errors.push(error(
at,
"arrange: the piece already has an arrangement — one line names the \
whole order"
.to_string(),
));
} else {
arrangement = Some(items.clone());
}
}
SourceLine::SectionStart {
muted,
name,
cycles,
} => {
if sections.iter().any(|section| §ion.name == name) {
errors.push(error(at, format!("section '{name}' is declared twice")));
}
open = Some((
Section {
name: name.clone(),
muted: *muted,
cycles: *cycles,
bpm: ambient.bpm,
sig: ambient.sig,
scale: ambient.scale,
patterns: Vec::new(),
groups: Vec::new(),
},
ambient,
));
}
SourceLine::SectionEnd => {
if let Some((section, restore)) = open.take() {
ambient = restore;
sections.push(section);
}
}
SourceLine::GroupStart { muted, name } => {
open_group = Some((*muted, name.clone(), at));
}
SourceLine::GroupEnd(transforms) => {
if let Some((muted, name, _)) = open_group.take()
&& let Some((section, _)) = &mut open
{
section.groups.push(GroupDef {
muted,
name,
transforms: transforms.clone(),
});
}
}
SourceLine::Pattern(def) => match &mut open {
Some((section, _)) => {
if let Some(span) = def.span
&& let Some(reason) = span.conflict(section.cycles)
{
errors.push(error(
at,
format!("'{}': {reason} — the line can never sound", def.name),
));
}
if section.patterns.iter().any(|other| other.name == def.name) {
errors.push(error(
at,
format!(
"'{}' is defined twice in section '{}' — the second would \
replace the first",
def.name, section.name
),
));
}
section.patterns.push(def.clone());
}
None => throughout.push(def.clone()),
},
_ => {}
}
}
errors.extend(check_group_chains(§ions));
errors.extend(check_name_collisions(§ions, &throughout));
let arrangement = arrangement.unwrap_or_else(|| {
sections
.iter()
.map(|section| ArrangeItem {
section: section.name.clone(),
repeat: 1,
})
.collect()
});
let (timeline, arrange_errors) = lay_out(§ions, &arrangement);
errors.extend(arrange_errors);
let played: Vec<&str> = timeline
.iter()
.map(|occurrence| sections[occurrence.section].name.as_str())
.collect();
let unused = sections
.iter()
.filter(|section| !played.contains(§ion.name.as_str()))
.map(|section| section.name.clone())
.collect();
(
Piece {
sections,
arrangement,
timeline,
throughout,
tail,
seed,
unused,
metadata,
},
errors,
)
}
fn lay_out(
sections: &[Section],
arrangement: &[ArrangeItem],
) -> (Vec<Occurrence>, Vec<CompileError>) {
let mut timeline = Vec::new();
let mut errors = Vec::new();
let mut cycle = 0u64;
for item in arrangement {
let Some(index) = sections
.iter()
.position(|section| section.name == item.section)
else {
errors.push(error(
1,
format!(
"arrange: '{}' is not a section in this buffer",
item.section
),
));
continue;
};
for _ in 0..item.repeat {
timeline.push(Occurrence {
section: index,
start_cycle: cycle,
});
cycle += u64::from(sections[index].cycles);
}
}
if timeline.is_empty() && !sections.is_empty() {
errors.push(error(
1,
"arrange: the arrangement plays no section, so the piece has no length".to_string(),
));
}
(timeline, errors)
}
fn check_group_chains(sections: &[Section]) -> Vec<CompileError> {
let mut errors = Vec::new();
let mut seen: Vec<(&str, &[Transform], &str)> = Vec::new();
for section in sections {
for group in §ion.groups {
match seen
.iter()
.find(|(name, _, _)| *name == group.name.as_str())
{
Some((_, chain, first)) if *chain != group.transforms.as_slice() => {
errors.push(error(
1,
format!(
"group '{}' declares a different chain in '{}' than in '{first}' — \
a bus is one bus for the whole piece, so its filters cannot change \
between sections",
group.name, section.name
),
));
}
Some(_) => {}
None => seen.push((&group.name, &group.transforms, §ion.name)),
}
}
}
errors
}
fn check_name_collisions(sections: &[Section], throughout: &[PatternDef]) -> Vec<CompileError> {
let mut errors = Vec::new();
for section in sections {
for group in §ion.groups {
if section.patterns.iter().any(|pattern| {
pattern.name == group.name && pattern.group.as_deref() != Some(&group.name)
}) {
errors.push(error(
1,
format!(
"'{}' names both a group and a pattern in section '{}'; pick two names",
group.name, section.name
),
));
}
}
if let Some(clash) = throughout.iter().find(|line| {
section
.patterns
.iter()
.any(|member| member.name == line.name)
}) {
errors.push(error(
1,
format!(
"'{}' is both a line that sounds throughout the piece and a member of \
section '{}' — one of them would silence the other",
clash.name, section.name
),
));
}
if sections
.iter()
.any(|other| other.patterns.iter().any(|p| p.name == section.name))
{
errors.push(error(
1,
format!(
"'{}' names both a section and a pattern; pick two names",
section.name
),
));
}
}
errors
}
#[cfg(test)]
mod tests;