use std::fmt;
use crate::core::DurationType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TempoIndication {
Grave,
Largo,
Larghetto,
Adagio,
Andante,
Andantino,
Moderato,
Allegretto,
Allegro,
Vivace,
Vivacissimo,
Presto,
Prestissimo,
}
impl TempoIndication {
pub fn bpm_range(&self) -> (f64, f64) {
match self {
TempoIndication::Grave => (20.0, 40.0),
TempoIndication::Largo => (40.0, 60.0),
TempoIndication::Larghetto => (45.0, 65.0),
TempoIndication::Adagio => (55.0, 75.0),
TempoIndication::Andante => (65.0, 80.0),
TempoIndication::Andantino => (80.0, 100.0),
TempoIndication::Moderato => (100.0, 120.0),
TempoIndication::Allegretto => (120.0, 140.0),
TempoIndication::Allegro => (140.0, 170.0),
TempoIndication::Vivace => (170.0, 200.0),
TempoIndication::Vivacissimo => (170.0, 200.0),
TempoIndication::Presto => (180.0, 220.0),
TempoIndication::Prestissimo => (200.0, 280.0),
}
}
pub fn typical_bpm(&self) -> f64 {
let (min, max) = self.bpm_range();
(min + max) / 2.0
}
pub fn name(&self) -> &'static str {
match self {
TempoIndication::Grave => "Grave",
TempoIndication::Largo => "Largo",
TempoIndication::Larghetto => "Larghetto",
TempoIndication::Adagio => "Adagio",
TempoIndication::Andante => "Andante",
TempoIndication::Andantino => "Andantino",
TempoIndication::Moderato => "Moderato",
TempoIndication::Allegretto => "Allegretto",
TempoIndication::Allegro => "Allegro",
TempoIndication::Vivace => "Vivace",
TempoIndication::Vivacissimo => "Vivacissimo",
TempoIndication::Presto => "Presto",
TempoIndication::Prestissimo => "Prestissimo",
}
}
pub fn from_bpm(bpm: f64) -> Option<TempoIndication> {
let indications = [
TempoIndication::Grave,
TempoIndication::Largo,
TempoIndication::Adagio,
TempoIndication::Andante,
TempoIndication::Moderato,
TempoIndication::Allegretto,
TempoIndication::Allegro,
TempoIndication::Vivace,
TempoIndication::Presto,
TempoIndication::Prestissimo,
];
for indication in indications {
let (min, max) = indication.bpm_range();
if bpm >= min && bpm <= max {
return Some(indication);
}
}
None
}
}
impl fmt::Display for TempoIndication {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MetronomeMark {
beat_unit: DurationType,
dots: u8,
bpm: f64,
}
impl MetronomeMark {
pub fn new(beat_unit: DurationType, bpm: f64) -> Self {
Self {
beat_unit,
dots: 0,
bpm,
}
}
pub fn dotted(beat_unit: DurationType, bpm: f64) -> Self {
Self {
beat_unit,
dots: 1,
bpm,
}
}
pub fn beat_unit(&self) -> DurationType {
self.beat_unit
}
pub fn dots(&self) -> u8 {
self.dots
}
pub fn bpm(&self) -> f64 {
self.bpm
}
pub fn set_bpm(&mut self, bpm: f64) {
self.bpm = bpm;
}
}
impl fmt::Display for MetronomeMark {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let dots = ".".repeat(self.dots as usize);
write!(f, "{}{} = {:.0}", self.beat_unit, dots, self.bpm)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Tempo {
bpm: f64,
indication: Option<TempoIndication>,
metronome: Option<MetronomeMark>,
text: Option<String>,
}
impl Tempo {
pub fn new(bpm: f64) -> Self {
Self {
bpm,
indication: TempoIndication::from_bpm(bpm),
metronome: Some(MetronomeMark::new(DurationType::Quarter, bpm)),
text: None,
}
}
pub fn from_indication(indication: TempoIndication) -> Self {
let bpm = indication.typical_bpm();
Self {
bpm,
indication: Some(indication),
metronome: Some(MetronomeMark::new(DurationType::Quarter, bpm)),
text: Some(indication.name().to_string()),
}
}
pub fn with_text(bpm: f64, text: impl Into<String>) -> Self {
Self {
bpm,
indication: TempoIndication::from_bpm(bpm),
metronome: Some(MetronomeMark::new(DurationType::Quarter, bpm)),
text: Some(text.into()),
}
}
pub fn bpm(&self) -> f64 {
self.bpm
}
pub fn set_bpm(&mut self, bpm: f64) {
self.bpm = bpm;
self.indication = TempoIndication::from_bpm(bpm);
if let Some(ref mut metro) = self.metronome {
metro.set_bpm(bpm);
}
}
pub fn indication(&self) -> Option<TempoIndication> {
self.indication
}
pub fn set_indication(&mut self, indication: TempoIndication) {
self.indication = Some(indication);
}
pub fn metronome(&self) -> Option<&MetronomeMark> {
self.metronome.as_ref()
}
pub fn text(&self) -> Option<&str> {
self.text.as_deref()
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = Some(text.into());
}
pub fn microseconds_per_quarter(&self) -> u32 {
(60_000_000.0 / self.bpm).round() as u32
}
pub fn from_microseconds(us: u32) -> Self {
let bpm = 60_000_000.0 / us as f64;
Self::new(bpm)
}
pub fn seconds_per_beat(&self) -> f64 {
60.0 / self.bpm
}
}
impl Default for Tempo {
fn default() -> Self {
Self::new(120.0)
}
}
impl fmt::Display for Tempo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(text) = &self.text {
write!(f, "{} ({:.0} BPM)", text, self.bpm)
} else if let Some(metro) = &self.metronome {
write!(f, "{}", metro)
} else {
write!(f, "{:.0} BPM", self.bpm)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tempo_creation() {
let tempo = Tempo::new(120.0);
assert_eq!(tempo.bpm(), 120.0);
assert_eq!(tempo.indication(), Some(TempoIndication::Moderato));
}
#[test]
fn test_tempo_indication() {
let allegro = TempoIndication::Allegro;
assert_eq!(allegro.name(), "Allegro");
let (min, max) = allegro.bpm_range();
assert!(min < max);
}
#[test]
fn test_tempo_from_indication() {
let tempo = Tempo::from_indication(TempoIndication::Andante);
let (min, max) = TempoIndication::Andante.bpm_range();
assert!(tempo.bpm() >= min && tempo.bpm() <= max);
}
#[test]
fn test_metronome_mark() {
let mark = MetronomeMark::new(DurationType::Quarter, 120.0);
assert_eq!(mark.beat_unit(), DurationType::Quarter);
assert_eq!(mark.bpm(), 120.0);
}
#[test]
fn test_microseconds() {
let tempo = Tempo::new(120.0);
assert_eq!(tempo.microseconds_per_quarter(), 500_000);
let tempo2 = Tempo::from_microseconds(500_000);
assert!((tempo2.bpm() - 120.0).abs() < 0.01);
}
}