use std::cmp::Ordering;
use std::fmt;
use super::{Duration, Fraction, Interval, Pitch};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TieType {
Start,
Continue,
Stop,
LetRing,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tie {
pub type_: TieType,
pub placement: Option<Placement>,
}
impl Tie {
pub fn new(type_: TieType) -> Self {
Self {
type_,
placement: None,
}
}
pub fn start() -> Self {
Self::new(TieType::Start)
}
pub fn stop() -> Self {
Self::new(TieType::Stop)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Placement {
Above,
Below,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum StemDirection {
#[default]
Auto,
Up,
Down,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum NoteHeadType {
#[default]
Normal,
Diamond,
Square,
Triangle,
Slash,
Cross,
X,
CircleX,
Arrow,
Cluster,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct NoteHead {
pub type_: NoteHeadType,
pub filled: Option<bool>,
pub parenthesis: bool,
}
impl NoteHead {
pub fn normal() -> Self {
Self::default()
}
pub fn diamond() -> Self {
Self {
type_: NoteHeadType::Diamond,
..Default::default()
}
}
pub fn x() -> Self {
Self {
type_: NoteHeadType::X,
..Default::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArticulationMark {
Accent,
StrongAccent,
Staccato,
Staccatissimo,
Tenuto,
DetachedLegato,
Marcato,
Fermata,
ShortFermata,
LongFermata,
BreathMark,
Caesura,
UpBow,
DownBow,
Harmonic,
OpenString,
Stopped,
Pizzicato,
SnapPizzicato,
ThumbPosition,
Pluck,
DoubleTongue,
TripleTongue,
Heel,
Toe,
Spiccato,
Scoop,
Plop,
Doit,
Falloff,
Stress,
Unstress,
StringHarmonic,
Fret(u8),
StringNumber(u8),
HandbellIndication,
HarpFingerNails,
WoodwindIndication,
BrassIndication,
}
impl ArticulationMark {
pub fn symbol(&self) -> &'static str {
match self {
ArticulationMark::Accent => ">",
ArticulationMark::StrongAccent => "^",
ArticulationMark::Staccato => ".",
ArticulationMark::Staccatissimo => "▼",
ArticulationMark::Tenuto => "-",
ArticulationMark::DetachedLegato => "-.",
ArticulationMark::Marcato => "^",
ArticulationMark::Fermata => "𝄐",
ArticulationMark::ShortFermata => "𝄑",
ArticulationMark::LongFermata => "𝄒",
ArticulationMark::BreathMark => ",",
ArticulationMark::Caesura => "//",
ArticulationMark::UpBow => "∨",
ArticulationMark::DownBow => "∏",
ArticulationMark::Harmonic => "○",
ArticulationMark::OpenString => "○",
ArticulationMark::Stopped => "+",
ArticulationMark::Pizzicato => "+",
ArticulationMark::SnapPizzicato => "⊙",
ArticulationMark::ThumbPosition => "◯",
ArticulationMark::Pluck => "i",
ArticulationMark::DoubleTongue => "‥",
ArticulationMark::TripleTongue => "…",
ArticulationMark::Heel => "U",
ArticulationMark::Toe => "^",
ArticulationMark::Spiccato => "'",
ArticulationMark::Scoop => "⌒",
ArticulationMark::Plop => "⌒",
ArticulationMark::Doit => "⌒",
ArticulationMark::Falloff => "⌒",
ArticulationMark::Stress => "▵",
ArticulationMark::Unstress => "▿",
ArticulationMark::StringHarmonic => "◇",
ArticulationMark::Fret(_) => "fr.",
ArticulationMark::StringNumber(_) => "#",
ArticulationMark::HandbellIndication => "hb.",
ArticulationMark::HarpFingerNails => "n.",
ArticulationMark::WoodwindIndication => "ww.",
ArticulationMark::BrassIndication => "br.",
}
}
pub fn name(&self) -> &'static str {
match self {
ArticulationMark::Accent => "accent",
ArticulationMark::StrongAccent => "strong accent",
ArticulationMark::Staccato => "staccato",
ArticulationMark::Staccatissimo => "staccatissimo",
ArticulationMark::Tenuto => "tenuto",
ArticulationMark::DetachedLegato => "detached legato",
ArticulationMark::Marcato => "marcato",
ArticulationMark::Fermata => "fermata",
ArticulationMark::ShortFermata => "short fermata",
ArticulationMark::LongFermata => "long fermata",
ArticulationMark::BreathMark => "breath mark",
ArticulationMark::Caesura => "caesura",
ArticulationMark::UpBow => "up bow",
ArticulationMark::DownBow => "down bow",
ArticulationMark::Harmonic => "harmonic",
ArticulationMark::OpenString => "open string",
ArticulationMark::Stopped => "stopped",
ArticulationMark::Pizzicato => "pizzicato",
ArticulationMark::SnapPizzicato => "snap pizzicato",
ArticulationMark::ThumbPosition => "thumb position",
ArticulationMark::Pluck => "pluck",
ArticulationMark::DoubleTongue => "double tongue",
ArticulationMark::TripleTongue => "triple tongue",
ArticulationMark::Heel => "heel",
ArticulationMark::Toe => "toe",
ArticulationMark::Spiccato => "spiccato",
ArticulationMark::Scoop => "scoop",
ArticulationMark::Plop => "plop",
ArticulationMark::Doit => "doit",
ArticulationMark::Falloff => "falloff",
ArticulationMark::Stress => "stress",
ArticulationMark::Unstress => "unstress",
ArticulationMark::StringHarmonic => "string harmonic",
ArticulationMark::Fret(_) => "fret",
ArticulationMark::StringNumber(_) => "string number",
ArticulationMark::HandbellIndication => "handbell indication",
ArticulationMark::HarpFingerNails => "harp fingernails",
ArticulationMark::WoodwindIndication => "woodwind indication",
ArticulationMark::BrassIndication => "brass indication",
}
}
pub fn fret_number(&self) -> Option<u8> {
match self {
ArticulationMark::Fret(n) => Some(*n),
_ => None,
}
}
pub fn string_number(&self) -> Option<u8> {
match self {
ArticulationMark::StringNumber(n) => Some(*n),
_ => None,
}
}
pub fn volume_shift(&self) -> i8 {
match self {
ArticulationMark::Accent => 16,
ArticulationMark::StrongAccent | ArticulationMark::Marcato => 24,
_ => 0,
}
}
pub fn velocity_multiplier(&self) -> f64 {
match self {
ArticulationMark::Accent => 1.2,
ArticulationMark::StrongAccent | ArticulationMark::Marcato => 1.4,
ArticulationMark::Staccato => 0.9,
ArticulationMark::Staccatissimo => 1.1,
ArticulationMark::Tenuto => 1.0,
ArticulationMark::DetachedLegato => 0.95,
_ => 1.0,
}
}
pub fn duration_multiplier(&self) -> f64 {
match self {
ArticulationMark::Staccato => 0.5,
ArticulationMark::Staccatissimo => 0.25,
ArticulationMark::Tenuto => 1.0,
ArticulationMark::DetachedLegato => 0.75,
_ => 1.0,
}
}
pub fn affects_duration(&self) -> bool {
matches!(
self,
ArticulationMark::Staccato
| ArticulationMark::Staccatissimo
| ArticulationMark::DetachedLegato
)
}
pub fn is_fermata(&self) -> bool {
matches!(
self,
ArticulationMark::Fermata
| ArticulationMark::ShortFermata
| ArticulationMark::LongFermata
)
}
}
impl fmt::Display for ArticulationMark {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Articulation {
pub type_: ArticulationMark,
pub placement: Option<Placement>,
}
impl Articulation {
pub fn new(type_: ArticulationMark) -> Self {
Self {
type_,
placement: None,
}
}
pub fn staccato() -> Self {
Self::new(ArticulationMark::Staccato)
}
pub fn accent() -> Self {
Self::new(ArticulationMark::Accent)
}
pub fn tenuto() -> Self {
Self::new(ArticulationMark::Tenuto)
}
pub fn fermata() -> Self {
Self::new(ArticulationMark::Fermata)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExpressionType {
Trill,
Turn,
InvertedTurn,
Mordent,
InvertedMordent,
Tremolo,
Vibrato,
Glissando,
Slide,
ArpeggioUp,
ArpeggioDown,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Expression {
pub type_: ExpressionType,
pub placement: Option<Placement>,
}
impl Expression {
pub fn new(type_: ExpressionType) -> Self {
Self {
type_,
placement: None,
}
}
pub fn trill() -> Self {
Self::new(ExpressionType::Trill)
}
pub fn mordent() -> Self {
Self::new(ExpressionType::Mordent)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Syllabic {
#[default]
Single,
Begin,
Middle,
End,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lyric {
pub text: String,
pub syllabic: Syllabic,
pub number: u8,
pub identifier: Option<String>,
}
impl Lyric {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
syllabic: Syllabic::Single,
number: 1,
identifier: None,
}
}
pub fn with_syllabic(mut self, syllabic: Syllabic) -> Self {
self.syllabic = syllabic;
self
}
pub fn with_number(mut self, number: u8) -> Self {
self.number = number;
self
}
pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = Some(identifier.into());
self
}
pub fn identifier(&self) -> Option<&str> {
self.identifier.as_deref()
}
pub fn from_raw_text(raw_text: &str, number: u8) -> Vec<Lyric> {
let parts: Vec<&str> = raw_text.split('-').collect();
if parts.len() == 1 {
vec![Lyric::new(raw_text).with_number(number)]
} else {
let last = parts.len() - 1;
parts
.into_iter()
.enumerate()
.map(|(i, part)| {
let syllabic = if i == 0 {
Syllabic::Begin
} else if i == last {
Syllabic::End
} else {
Syllabic::Middle
};
Lyric::new(part).with_syllabic(syllabic).with_number(number)
})
.collect()
}
}
}
pub fn is_composite_lyric_set(lyrics: &[Lyric]) -> bool {
lyrics.len() > 1
}
#[derive(Debug, Clone, PartialEq)]
pub struct Volume {
pub velocity: u8,
pub realized_volume: f64,
}
impl Volume {
pub fn from_velocity(velocity: u8) -> Self {
Self {
velocity,
realized_volume: velocity as f64 / 127.0,
}
}
pub fn from_scalar(scalar: f64) -> Self {
Self {
velocity: (scalar.clamp(0.0, 1.0) * 127.0) as u8,
realized_volume: scalar.clamp(0.0, 1.0),
}
}
pub fn mf() -> Self {
Self::from_velocity(80)
}
pub fn get_realized(&self, articulations: &[Articulation]) -> Volume {
let multiplier: f64 = articulations
.iter()
.map(|a| a.type_.velocity_multiplier())
.product();
let shift: i32 = articulations.iter().map(|a| a.type_.volume_shift() as i32).sum();
let shifted = (self.velocity as f64 * multiplier).round() as i32 + shift;
Volume::from_velocity(shifted.clamp(0, 127) as u8)
}
}
impl Default for Volume {
fn default() -> Self {
Self::mf()
}
}
#[derive(Debug, Clone)]
pub struct Note {
pitch: Pitch,
duration: Duration,
offset: Fraction,
tie: Option<Tie>,
lyrics: Vec<Lyric>,
articulations: Vec<Articulation>,
expressions: Vec<Expression>,
volume: Volume,
notehead: NoteHead,
stem_direction: StemDirection,
grace_slash: Option<bool>,
}
impl Note {
pub fn new(pitch: Pitch, duration: Duration) -> Self {
Self {
pitch,
duration,
offset: Fraction::new(0, 1),
tie: None,
lyrics: Vec::new(),
articulations: Vec::new(),
expressions: Vec::new(),
volume: Volume::default(),
notehead: NoteHead::default(),
stem_direction: StemDirection::default(),
grace_slash: None,
}
}
pub fn from_str(pitch: &str, duration: Duration) -> Result<Self, super::ParseError> {
Ok(Self::new(pitch.parse()?, duration))
}
pub fn quarter(pitch: Pitch) -> Self {
Self::new(pitch, Duration::quarter())
}
pub fn half(pitch: Pitch) -> Self {
Self::new(pitch, Duration::half())
}
pub fn whole(pitch: Pitch) -> Self {
Self::new(pitch, Duration::whole())
}
pub fn eighth(pitch: Pitch) -> Self {
Self::new(pitch, Duration::eighth())
}
pub fn pitch(&self) -> &Pitch {
&self.pitch
}
pub fn pitch_mut(&mut self) -> &mut Pitch {
&mut self.pitch
}
pub fn set_pitch(&mut self, pitch: Pitch) {
self.pitch = pitch;
}
pub fn duration(&self) -> &Duration {
&self.duration
}
pub fn duration_mut(&mut self) -> &mut Duration {
&mut self.duration
}
pub fn set_duration(&mut self, duration: Duration) {
self.duration = duration;
}
pub fn offset(&self) -> Fraction {
self.offset
}
pub fn set_offset(&mut self, offset: Fraction) {
self.offset = offset;
}
pub fn quarter_length(&self) -> Fraction {
self.duration.quarter_length()
}
pub fn midi(&self) -> u8 {
self.pitch.midi()
}
pub fn name(&self) -> String {
self.pitch.name_with_octave()
}
pub fn tie(&self) -> Option<&Tie> {
self.tie.as_ref()
}
pub fn set_tie(&mut self, tie: Option<Tie>) {
self.tie = tie;
}
pub fn lyrics(&self) -> &[Lyric] {
&self.lyrics
}
pub fn add_lyric(&mut self, lyric: Lyric) {
self.lyrics.push(lyric);
}
pub fn add_lyric_text(&mut self, text: impl Into<String>) {
self.lyrics.push(Lyric::new(text));
}
pub fn articulations(&self) -> &[Articulation] {
&self.articulations
}
pub fn add_articulation(&mut self, articulation: Articulation) {
self.articulations.push(articulation);
}
pub fn expressions(&self) -> &[Expression] {
&self.expressions
}
pub fn add_expression(&mut self, expression: Expression) {
self.expressions.push(expression);
}
pub fn volume(&self) -> &Volume {
&self.volume
}
pub fn set_volume(&mut self, volume: Volume) {
self.volume = volume;
}
pub fn set_velocity(&mut self, velocity: u8) {
self.volume = Volume::from_velocity(velocity);
}
pub fn realized_volume(&self) -> Volume {
self.volume.get_realized(&self.articulations)
}
pub fn realized_quarter_length(&self) -> f64 {
let multiplier: f64 = self
.articulations
.iter()
.map(|a| a.type_.duration_multiplier())
.product();
self.duration.quarter_length_f64() * multiplier
}
pub fn notehead(&self) -> &NoteHead {
&self.notehead
}
pub fn set_notehead(&mut self, notehead: NoteHead) {
self.notehead = notehead;
}
pub fn stem_direction(&self) -> StemDirection {
self.stem_direction
}
pub fn set_stem_direction(&mut self, direction: StemDirection) {
self.stem_direction = direction;
}
pub fn is_grace(&self) -> bool {
self.grace_slash.is_some()
}
pub fn is_grace_slashed(&self) -> Option<bool> {
self.grace_slash
}
pub fn to_grace(&self) -> Note {
let mut grace = self.clone();
grace.grace_slash = Some(true);
grace.duration = Duration::zero();
grace
}
pub fn to_appoggiatura(&self) -> Note {
let mut grace = self.clone();
grace.grace_slash = Some(false);
grace.duration = Duration::zero();
grace
}
pub fn transpose(&self, interval: &Interval) -> Note {
let mut transposed = self.clone();
transposed.pitch = self.pitch.transpose(interval);
transposed
}
pub fn transpose_semitones(&self, semitones: i32) -> Note {
let mut transposed = self.clone();
transposed.pitch = self.pitch.transpose_semitones(semitones);
transposed
}
pub fn transpose_str(&self, interval: &str) -> Result<Note, super::ParseError> {
let interval: Interval = interval.parse()?;
Ok(self.transpose(&interval))
}
pub fn pitches(&self) -> Vec<&Pitch> {
vec![&self.pitch]
}
pub fn full_name(&self) -> String {
format!(
"{} in octave {} {}",
self.pitch.name(),
self.pitch.implicit_octave(),
self.duration.full_name()
)
}
pub fn augment_or_diminish(&self, scalar: Fraction) -> Note {
let mut scaled = self.clone();
scaled.duration = self.duration.augment_or_diminish(scalar);
scaled
}
}
impl Default for Note {
fn default() -> Self {
Self::new(Pitch::default(), Duration::default())
}
}
impl fmt::Display for Note {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.pitch, self.duration)
}
}
impl PartialEq for Note {
fn eq(&self, other: &Self) -> bool {
self.pitch == other.pitch && self.duration == other.duration
}
}
impl Eq for Note {}
impl PartialOrd for Note {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Note {
fn cmp(&self, other: &Self) -> Ordering {
self.pitch.cmp(&other.pitch)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Unpitched {
display_position: i8,
duration: Duration,
}
impl Unpitched {
pub fn new(duration: Duration) -> Self {
Self {
display_position: 0,
duration,
}
}
pub fn display_position(&self) -> i8 {
self.display_position
}
pub fn set_display_position(&mut self, position: i8) {
self.display_position = position;
}
pub fn duration(&self) -> &Duration {
&self.duration
}
pub fn set_duration(&mut self, duration: Duration) {
self.duration = duration;
}
pub fn quarter_length(&self) -> Fraction {
self.duration.quarter_length()
}
pub fn display_name(&self) -> String {
format!("staff position {}", self.display_position)
}
}
impl fmt::Display for Unpitched {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unpitched({})", self.display_name())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Step;
#[test]
fn test_note_creation() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let note = Note::quarter(pitch);
assert_eq!(note.midi(), 60);
assert_eq!(note.quarter_length(), Fraction::new(1, 1));
}
#[test]
fn test_note_transpose() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let note = Note::quarter(pitch);
let transposed = note.transpose(&Interval::perfect_fifth());
assert_eq!(transposed.pitch().step(), Step::G);
assert_eq!(transposed.pitch().octave(), Some(4));
}
#[test]
fn test_note_articulations() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let mut note = Note::quarter(pitch);
note.add_articulation(Articulation::staccato());
note.add_articulation(Articulation::accent());
assert_eq!(note.articulations().len(), 2);
}
#[test]
fn test_articulation_metadata_reachable_from_note() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let mut note = Note::quarter(pitch);
note.add_articulation(Articulation::staccato());
assert_eq!(note.articulations()[0].type_.symbol(), ".");
assert!(note.articulations()[0].type_.affects_duration());
}
#[test]
fn test_volume_get_realized_applies_velocity_multiplier() {
let volume = Volume::from_velocity(50);
let accented = volume.get_realized(&[Articulation::accent()]);
assert_eq!(accented.velocity, 76);
let unarticulated = volume.get_realized(&[]);
assert_eq!(unarticulated.velocity, 50);
}
#[test]
fn test_note_realized_volume_and_duration() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let mut note = Note::quarter(pitch);
note.set_velocity(100);
note.add_articulation(Articulation::staccato());
assert_eq!(note.realized_volume().velocity, 90);
assert!((note.realized_quarter_length() - 0.5).abs() < 1e-9);
}
#[test]
fn test_note_lyrics() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let mut note = Note::quarter(pitch);
note.add_lyric_text("la");
assert_eq!(note.lyrics().len(), 1);
assert_eq!(note.lyrics()[0].text, "la");
}
#[test]
fn test_note_grace() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let note = Note::quarter(pitch);
let grace = note.to_grace();
assert!(grace.is_grace());
assert_eq!(grace.quarter_length(), Fraction::new(0, 1));
assert_eq!(grace.is_grace_slashed(), Some(true));
}
#[test]
fn test_note_appoggiatura() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let note = Note::quarter(pitch);
let appoggiatura = note.to_appoggiatura();
assert!(appoggiatura.is_grace());
assert_eq!(appoggiatura.is_grace_slashed(), Some(false));
assert_eq!(note.is_grace_slashed(), None);
}
#[test]
fn test_note_full_name() {
let pitch = Pitch::from_parts(Step::C, Some(4), Some(crate::core::Accidental::Sharp));
let note = Note::quarter(pitch);
assert_eq!(note.full_name(), "C# in octave 4 quarter");
}
#[test]
fn test_note_transpose_str() {
let pitch = Pitch::from_parts(Step::C, Some(4), None);
let note = Note::quarter(pitch);
let transposed = note.transpose_str("P5").unwrap();
assert_eq!(transposed.pitch().step(), Step::G);
assert!(note.transpose_str("bogus").is_err());
}
#[test]
fn test_note_pitches_accessor() {
let pitch = Pitch::from_parts(Step::D, Some(4), None);
let note = Note::quarter(pitch);
let pitches = note.pitches();
assert_eq!(pitches.len(), 1);
assert_eq!(pitches[0].step(), Step::D);
}
#[test]
fn test_unpitched() {
let mut unpitched = Unpitched::new(Duration::quarter());
assert_eq!(unpitched.display_position(), 0);
assert_eq!(unpitched.quarter_length(), Fraction::new(1, 1));
unpitched.set_display_position(3);
assert_eq!(unpitched.display_position(), 3);
assert_eq!(unpitched.display_name(), "staff position 3");
}
#[test]
fn test_lyric_from_raw_text_single_syllable() {
let lyrics = Lyric::from_raw_text("hello", 1);
assert_eq!(lyrics.len(), 1);
assert_eq!(lyrics[0].syllabic, Syllabic::Single);
assert!(!is_composite_lyric_set(&lyrics));
}
#[test]
fn test_lyric_from_raw_text_multi_syllable() {
let lyrics = Lyric::from_raw_text("con-tra-ry", 2);
assert_eq!(lyrics.len(), 3);
assert_eq!(lyrics[0].text, "con");
assert_eq!(lyrics[0].syllabic, Syllabic::Begin);
assert_eq!(lyrics[1].text, "tra");
assert_eq!(lyrics[1].syllabic, Syllabic::Middle);
assert_eq!(lyrics[2].text, "ry");
assert_eq!(lyrics[2].syllabic, Syllabic::End);
assert!(lyrics.iter().all(|l| l.number == 2));
assert!(is_composite_lyric_set(&lyrics));
}
#[test]
fn test_lyric_identifier() {
let lyric = Lyric::new("la").with_identifier("chorus");
assert_eq!(lyric.identifier(), Some("chorus"));
}
}