1use sva_ast::{Expr, Graph, Literal, PerBar};
4
5use crate::cli_error::CliError;
6
7fn beats_per_bar(meter_str: &str) -> Result<f64, CliError> {
9 let numerator = meter_str.split('/').next().ok_or_else(|| {
10 CliError::BadTempo(format!(
11 "meter `{meter_str}` has no `/`-separated numerator"
12 ))
13 })?;
14 numerator.trim().parse::<f64>().map_err(|e| {
15 CliError::BadTempo(format!(
16 "meter `{meter_str}`'s beats-per-bar component `{numerator}` is not a number: {e}"
17 ))
18 })
19}
20
21pub fn refuse_unresolved_bars(graph: &Graph) -> Result<(), CliError> {
23 match graph.unresolved_bar_literals().first() {
24 None => Ok(()),
25 Some(path) => Err(CliError::BadTempo(format!(
26 "`{path}` uses a `b` time literal, but this composition declares no bpm/meter \
27 (add `variables/bpm` and `variables/meter`)"
28 ))),
29 }
30}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct Tempo {
34 pub seconds_per_bar: f64,
35 pub beats_per_bar: f64,
36 pub bpm: f64,
37}
38
39pub fn resolved(graph: &Graph) -> Result<Option<Tempo>, CliError> {
41 match (graph.global("bpm"), graph.global("meter")) {
42 (None, None) => Ok(None),
43 (Some(Expr::Lit(Literal::Num(bpm))), Some(Expr::Lit(Literal::Str(meter_str)))) => {
44 let bpm = *bpm;
45 if bpm <= 0.0 {
46 return Err(CliError::BadTempo(format!(
47 "bpm must be positive, got {bpm}"
48 )));
49 }
50 let beats_per_bar = beats_per_bar(meter_str)?;
51 Ok(Some(Tempo {
52 seconds_per_bar: beats_per_bar * 60.0 / bpm,
53 beats_per_bar,
54 bpm,
55 }))
56 }
57 (Some(_), Some(_)) => Err(CliError::BadTempo(
58 "`bpm` must be a numeric literal and `meter` a string literal (e.g. \"4/4\")"
59 .to_string(),
60 )),
61 (Some(_), None) => Err(CliError::BadTempo(
62 "`bpm` is present but `meter` is missing; both or neither".to_string(),
63 )),
64 (None, Some(_)) => Err(CliError::BadTempo(
65 "`meter` is present but `bpm` is missing; both or neither".to_string(),
66 )),
67 }
68}
69
70pub fn resolve(graph: &mut Graph) -> Result<(), CliError> {
72 match resolved(graph)? {
73 None => refuse_unresolved_bars(graph),
74 Some(tempo) => {
75 graph.resolve_bar_spans(PerBar {
76 seconds: tempo.beats_per_bar * 60.0,
77 per: tempo.bpm,
78 });
79 Ok(())
80 }
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn beats_per_bar_parses_the_numerator() {
90 assert_eq!(beats_per_bar("4/4").unwrap(), 4.0);
91 assert_eq!(beats_per_bar("3/4").unwrap(), 3.0);
92 }
93
94 #[test]
95 fn beats_per_bar_rejects_a_non_numeric_numerator() {
96 assert!(beats_per_bar("four/4").is_err());
97 }
98}