use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::{Section, SimpleChord};
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone)]
pub struct Song {
pub title: String,
pub titles: Option<Vec<String>>,
pub subtitle: Option<String>,
pub copyright: Option<String>,
pub key: Option<SimpleChord>,
pub artist: Option<String>,
pub artists: Option<Vec<String>>,
pub language: Option<String>,
pub languages: Option<Vec<String>>,
pub tempo: Option<u32>,
pub time: Option<(u32, u32)>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tags: BTreeMap<String, String>,
pub sections: Vec<Section>,
}
impl Song {
pub fn transpose(&mut self, key: SimpleChord) -> &mut Self {
self.key = Some(key);
self
}
pub fn normalize(&mut self) -> &mut Self {
for section in &mut self.sections {
if let Some(key) = &self.key {
section.normalize(key);
}
}
self
}
pub fn bar_duration(&self) -> u32 {
if let Some((numerator, denominator)) = self.time {
1000 * numerator * 4 / denominator
} else {
4000
}
}
pub fn beats_per_bar(&self) -> u32 {
self.time.map(|(n, _)| n).unwrap_or(4).max(1)
}
pub fn chord_duration_to_layout(&self, stored_millibeats: Option<u32>) -> u32 {
chord_duration_to_layout_milliclicks(
stored_millibeats,
self.bar_duration(),
self.beats_per_bar(),
)
}
pub fn title_for_language(&self, language: Option<usize>) -> &str {
let idx = language.unwrap_or(0);
if let Some(titles) = &self.titles
&& let Some(candidate) = titles.get(idx)
&& !candidate.is_empty()
{
return candidate;
}
&self.title
}
pub fn move_chords_to_next_vowels(mut self) -> Self {
self.sections = self
.sections
.into_iter()
.map(Section::move_chords_to_next_vowels)
.collect();
self
}
pub fn remove_manual_spacing(mut self) -> Self {
self.sections = self
.sections
.into_iter()
.map(Section::remove_manual_spacing)
.collect();
self
}
pub fn artist_slice(&self) -> Option<&[String]> {
if let Some(artists) = &self.artists {
if artists.is_empty() {
return None;
}
return Some(artists.as_slice());
}
self.artist.as_ref().map(std::slice::from_ref)
}
pub fn language_list(&self) -> Option<Vec<String>> {
if let Some(langs) = &self.languages {
if langs.is_empty() {
return None;
}
return Some(langs.clone());
}
self.language.as_ref().map(|langs| {
langs
.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
})
}
pub fn artist_list(&self) -> Option<Vec<String>> {
if let Some(artists) = &self.artists {
if artists.is_empty() {
return None;
}
return Some(artists.clone());
}
self.artist.as_ref().map(|a| vec![a.clone()])
}
}
pub fn chord_duration_to_layout_milliclicks(
stored_millibeats: Option<u32>,
bar_duration: u32,
beats_per_bar: u32,
) -> u32 {
let beats = beats_per_bar.max(1) as u64;
let bar = bar_duration as u64;
match stored_millibeats {
None => bar_duration,
Some(mb) => (mb as u64 * bar / (beats * 1000)) as u32,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bar_duration_milliclicks() {
let song_44 = Song {
time: Some((4, 4)),
..Song::default()
};
assert_eq!(
song_44.bar_duration(),
4000,
"4/4 bar = 4 clicks = 4000 milliclicks"
);
let song_34 = Song {
time: Some((3, 4)),
..Song::default()
};
assert_eq!(song_34.bar_duration(), 3000, "3/4 bar = 3000 milliclicks");
let song_68 = Song {
time: Some((6, 8)),
..Song::default()
};
assert_eq!(
song_68.bar_duration(),
3000,
"6/8 bar should equal 3/4 bar duration"
);
let song_22 = Song {
time: Some((2, 2)),
..Song::default()
};
assert_eq!(
song_22.bar_duration(),
4000,
"2/2 bar should equal 4/4 bar duration"
);
let song_default = Song::default();
assert_eq!(song_default.bar_duration(), 4000);
}
#[test]
fn chord_duration_to_layout_respects_time_signature() {
let song_44 = Song {
time: Some((4, 4)),
..Song::default()
};
assert_eq!(song_44.beats_per_bar(), 4);
assert_eq!(
chord_duration_to_layout_milliclicks(Some(1000), song_44.bar_duration(), 4),
1000,
);
assert_eq!(
chord_duration_to_layout_milliclicks(Some(1500), song_44.bar_duration(), 4),
1500
);
let song_68 = Song {
time: Some((6, 8)),
..Song::default()
};
assert_eq!(song_68.beats_per_bar(), 6);
assert_eq!(song_68.bar_duration(), 3000);
assert_eq!(
chord_duration_to_layout_milliclicks(Some(1000), 3000, 6),
500,
);
assert_eq!(
chord_duration_to_layout_milliclicks(Some(500), 3000, 6),
250,
);
}
#[test]
fn language_list_prefers_structured_languages() {
let song = Song {
language: Some("legacy should be ignored when vector present".to_string()),
languages: Some(vec!["en".to_string(), "de".to_string(), "fr".to_string()]),
..Song::default()
};
let langs = song.language_list().expect("language list");
assert_eq!(langs, vec!["en", "de", "fr"]);
}
#[test]
fn title_for_language_prefers_titles_vector_and_falls_back() {
let song = Song {
title: "Primary".to_string(),
titles: Some(vec![
"Primary".to_string(),
"Secondary".to_string(),
String::new(),
]),
..Song::default()
};
assert_eq!(song.title_for_language(None), "Primary");
assert_eq!(song.title_for_language(Some(0)), "Primary");
assert_eq!(song.title_for_language(Some(1)), "Secondary");
assert_eq!(song.title_for_language(Some(2)), "Primary");
assert_eq!(song.title_for_language(Some(10)), "Primary");
}
}