From 8a9edb3b910093bd0fce816db7cf53b2941fc818 Mon Sep 17 00:00:00 2001 From: Jeron Aldaron Lau Date: Sat, 14 Sep 2019 23:40:33 -0500 Subject: [PATCH] Fix bug: Notes are now bound in range C-1 to B9. --- README.md | 3 + src/lib.rs | 347 +-------------------------------------- src/note/articulation.rs | 48 ++++++ src/note/mod.rs | 344 ++++++++++++++++++++++++++++++++++++++ src/note/pitch.rs | 131 +++++++++++++++ 5 files changed, 530 insertions(+), 343 deletions(-) create mode 100644 src/note/articulation.rs create mode 100644 src/note/mod.rs create mode 100644 src/note/pitch.rs diff --git a/README.md b/README.md index 9d3d8c2..c402542 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ Format is in a zip file. An example contents are in `scof/`. A schema describi ## Language Each file is based on [MuON (Micro Object Notation)](https://github.com/DougLau/muon/). Details can be found in `schema`. +## About +Bars of music in scof must always have the correct number of beats. Using functions provided in this crate you should not be able to get the wrong number of beats, or incorrect/hard-to-read notation. + ### TODO - Schema for synthesis (soundfont data, instruments) - Schema for formatting (page breaks, line breaks, customized spacing, font choice) diff --git a/src/lib.rs b/src/lib.rs index 1a2678a..ee0a710 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,9 +20,11 @@ use serde_derive::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; +pub mod note; mod fraction; pub use fraction::{Fraction, IsZero}; +pub use note::{Note, Articulation, PitchClass, PitchName, PitchAccidental, PitchOctave}; /// Cursor pointing to a marking #[derive(Clone, Default, Debug, PartialEq)] @@ -72,46 +74,6 @@ impl Cursor { } } -/// A Pitch Name. -pub enum PitchName { - C, - D, - E, - F, - G, - A, - B, -} - -/// A Pitch Accidental. -#[derive(Copy, Clone)] -pub enum PitchAccidental { - /// - DoubleFlat, - /// - FlatQuarterFlat, - /// - Flat, - /// - QuarterFlat, - /// - Natural, - /// - QuarterSharp, - /// - Sharp, - /// - SharpQuarterSharp, - /// - DoubleSharp, -} - -/// A Pitch Class -pub struct PitchClass { - pub name: PitchName, - pub accidental: Option, -} - /// A Dynamic. pub enum Dynamic { PPPPPP, @@ -135,307 +97,6 @@ pub enum Dynamic { SFP, } -/// An articulation (affects how the note is played). -#[derive(Copy, Clone)] -pub enum Articulation { - /// Really separated. - Staccatissimo, - /// Separated (short 1/2) - Staccato, - /// Tenuto - Tenuto, - /// Tenuto Staccato - TenutoStaccato, - /// Marcato (short sharp attack) (2/3) - Marcato, - /// Marcato Staccato - MarcatoStaccato, - /// Marcato Tenuto - MarcatoTenuto, - /// Accent (sharp attack) - Accent, - /// Accent Staccato - AccentStaccato, - /// Accent Tenuto - AccentTenuto, - /// Slur - Slur, - /// Glissando - Glissando, - /// Pitch bend slide up into - BendUpInto, - /// Pitch bend slide down into - BendDownInto, - /// Pitch bend slide up out of - BendUpOut, - /// Pitch bend slide down out of (fall) - BendDownOut, - /// Fermata (everyone plays long) - Fermata, - /// Closed mute (or palm mute rendered as _ on guitar) - Mute, - /// Open (no) Mute - Open, - /// Harmonic - Harmonic, - /// Turn - Turn, - /// Inverted Turn - TurnInverted, - /// Trill - Trill, - /// Tremelo - Tremelo, - /// Arpeggio (strum) pitch up, strum guitar down. - StrumDown, - /// Arpeggio (strum) pitch down, strum guitar up - StrumUp, - /// Pedal - Pedal, -} - -/// A note. -pub struct Note { - /// Pitch & Octave - pub pitch: Option<(PitchClass, i8)>, - /// Duration of the note as a fraction. - pub duration: Fraction, - /// Articulation. - pub articulation: Option, -} - -impl fmt::Display for Note { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // Write duration. - if self.duration.num != 1 { - write!(f, "{}/", self.duration.num)?; - } - write!(f, "{}", self.duration.den)?; - - // Write note name & octave. - match &self.pitch { - Some(pitch) => { - let class = match pitch.0.name { - PitchName::A => "A", - PitchName::B => "B", - PitchName::C => "C", - PitchName::D => "D", - PitchName::E => "E", - PitchName::F => "F", - PitchName::G => "G", - }; - write!(f, "{}{}", class, pitch.1) - }, - None => write!(f, "R"), - } - } -} - -impl FromStr for Note { - type Err = (); - - fn from_str(s: &str) -> Result { - // Read duration. - let start_index = 0; - let mut end_index = 0; - for (i, c) in s.char_indices() { - if !c.is_numeric() { - end_index = i; - break; - } - } - - // Read rest of duration. - let duration = if s[end_index..].chars().next() == Some('/') { - let numer = s[start_index..end_index].parse::().or(Err(()))?; - - end_index += 1; - - let start_index = end_index; - - for (i, c) in s[start_index..].char_indices() { - if !c.is_numeric() { - end_index = i; - break; - } - } - - let denom = s[start_index..end_index].parse::().or(Err(()))?; - - Fraction::new(numer, denom) - } else { - let numer = 1; - let denom = s[start_index..end_index].parse::().or(Err(()))?; - - Fraction::new(numer, denom) - }; - - // Read note name. - match s.get(end_index..).ok_or(())? { - "R" => Ok(Note { - pitch: None, - duration, - articulation: None, - }), - a => { - let two = a.chars().collect::>(); - let letter_name = two[0]; - let octave_num = str::parse::(&format!("{}", two[1])) - .or(Err(()))?; - - Ok(Note { - pitch: Some(( - match letter_name { - 'A' => PitchClass { - name: PitchName::A, - accidental: None, - }, - 'B' => PitchClass { - name: PitchName::B, - accidental: None, - }, - 'C' => PitchClass { - name: PitchName::C, - accidental: None, - }, - 'D' => PitchClass { - name: PitchName::D, - accidental: None, - }, - 'E' => PitchClass { - name: PitchName::E, - accidental: None, - }, - 'F' => PitchClass { - name: PitchName::F, - accidental: None, - }, - 'G' => PitchClass { - name: PitchName::G, - accidental: None, - }, - // FIXME: return Err - a => panic!("Failed to parse '{}'", a), - }, - octave_num, - )), - duration, - articulation: None, - }) - } - } - } -} - -impl Note { - /// Get the note's visual distance from middle C (C4). - pub fn visual_distance(&self) -> i32 { - if let Some(ref pitch) = self.pitch { - let octave_offset = -7 * (pitch.1 as i32 - 4); - octave_offset - - match pitch.0.name { - PitchName::C => 0, - PitchName::D => 1, - PitchName::E => 2, - PitchName::F => 3, - PitchName::G => 4, - PitchName::A => 5, - PitchName::B => 6, - } - } else { - 0 - } - } - - /// Set pitch class and octave. - pub fn set_pitch(&mut self, pitch: (PitchClass, i8)) { - self.pitch = Some(pitch); - } - - /// Set duration of note. - pub fn set_duration(&mut self, duration: Fraction) { - self.duration = duration; - } - - fn move_step(&self, create: (PitchClass, i8), run: &dyn Fn(&(PitchClass, i8)) -> Option<(PitchClass, i8)>) -> Note { - let pitch = if let Some(ref pitch) = self.pitch { - (run)(pitch) - } else { - Some(create) - }; - - Note { - pitch, - duration: self.duration, - articulation: self.articulation, - } - } - - /// Calculate note one quarter step up. - pub fn quarter_step_up(&self, create: (PitchClass, i8)) -> Note { - self.step_up(create) // FIXME - } - - /// Calculate note one quarter step down. - pub fn quarter_step_down(&self, create: (PitchClass, i8)) -> Note { - self.step_down(create) // FIXME - } - - /// Calculate note one half step up. - pub fn half_step_up(&self, create: (PitchClass, i8)) -> Note { - self.step_up(create) // FIXME - } - - /// Calculate note one half step down. - pub fn half_step_down(&self, create: (PitchClass, i8)) -> Note { - self.step_down(create) // FIXME - } - - /// Calculate note one step up within the key. - /// - `create`: Note that is generated from a rest. - pub fn step_up(&self, create: (PitchClass, i8)) -> Note { - self.move_step(create, &|pitch| { - let (pitch_class, offset) = match pitch.0.name { - PitchName::A => (PitchName::B, 0), - PitchName::B => (PitchName::C, 1), - PitchName::C => (PitchName::D, 0), - PitchName::D => (PitchName::E, 0), - PitchName::E => (PitchName::F, 0), - PitchName::F => (PitchName::G, 0), - PitchName::G => (PitchName::A, 0), - }; - let pitch_octave = pitch.1 + offset; - - Some((PitchClass { - name: pitch_class, - accidental: pitch.0.accidental, - }, pitch_octave)) - }) - } - - /// Calculate note one step down within the key. - /// - `create`: Note that is generated from a rest. - pub fn step_down(&self, create: (PitchClass, i8)) -> Note { - self.move_step(create, &|pitch| { - let (pitch_class, offset) = match pitch.0.name { - PitchName::A => (PitchName::G, 0), - PitchName::B => (PitchName::A, 0), - PitchName::C => (PitchName::B, -1), - PitchName::D => (PitchName::C, 0), - PitchName::E => (PitchName::D, 0), - PitchName::F => (PitchName::E, 0), - PitchName::G => (PitchName::F, 0), - }; - let pitch_octave = pitch.1 + offset; - - Some((PitchClass { - name: pitch_class, - accidental: pitch.0.accidental, - }, pitch_octave)) - }) - } -} - /// A marking. pub enum Marking { /// Change intensity of sound. @@ -565,7 +226,7 @@ pub struct Chan { impl Default for Chan { fn default() -> Self { - let notes = vec!["1R".to_string()]; // whole Rest + let notes = vec!["R".to_string()]; // whole measure rest let lyric = vec![]; Chan { notes, lyric } } @@ -848,7 +509,7 @@ impl Scof { } /// Set pitch class and octave of a note at a cursor - pub fn set_pitch(&mut self, cursor: &Cursor, pitch: (PitchClass, i8)) { + pub fn set_pitch(&mut self, cursor: &Cursor, pitch: (PitchClass, PitchOctave)) { let mut note = self.note(cursor).unwrap(); note.set_pitch(pitch); let m = self.marking_str_mut(0, cursor).unwrap(); diff --git a/src/note/articulation.rs b/src/note/articulation.rs new file mode 100644 index 0000000..77dca7e --- /dev/null +++ b/src/note/articulation.rs @@ -0,0 +1,48 @@ +/// An articulation (affects how the note is played). +#[derive(Copy, Clone)] +pub enum Articulation { + /// Really separated. + Staccatissimo, + /// Separated (short 1/2) + Staccato, + /// Tenuto + Tenuto, + /// Marcato (short sharp attack) (2/3) + Marcato, + /// Accent (sharp attack) + Accent, + /// Slur + Slur, + /// Glissando + Glissando, + /// Pitch bend slide up into + BendUpInto, + /// Pitch bend slide down into + BendDownInto, + /// Pitch bend slide up out of + BendUpOut, + /// Pitch bend slide down out of (fall) + BendDownOut, + /// Fermata (everyone plays long) + Fermata, + /// Closed mute (or palm mute rendered as _ on guitar) + Mute, + /// Open (no) Mute + Open, + /// Harmonic + Harmonic, + /// Turn + Turn, + /// Inverted Turn + TurnInverted, + /// Trill + Trill, + /// Tremelo + Tremelo, + /// Arpeggio (strum) pitch up, strum guitar down. + StrumDown, + /// Arpeggio (strum) pitch down, strum guitar up + StrumUp, + /// Pedal + Pedal, +} diff --git a/src/note/mod.rs b/src/note/mod.rs new file mode 100644 index 0000000..c32980f --- /dev/null +++ b/src/note/mod.rs @@ -0,0 +1,344 @@ +//! # Note (convert Note Struct <-> String) +//! A note has an optional pitch, None = Rest. +//! +//! ## Structure +//! +//! **note length**: Required number for note duration 8=eighth note. +//! +//! - O: 128th note +//! - X: 64th note +//! - Y: 32nd note +//! - S: 16th note +//! - T: 8th note +//! - Q: quarter note +//! - U: half note +//! - W: whole note +//! - V: double whole note (breve) +//! - L: quadruple whole note (longa) +//! - .: augmentation dot +//! +//! **note name**: Required name of the note. A-G, or R for rest. +//! +//! ``` +//! A +//! B +//! C +//! D +//! E +//! F +//! G +//! R +//! ``` +//! +//! **accidental**: Optional accidental. If not provided, from key signature. Cannot be same as what is in the key signature. +//! +//! - `bb`: Double Flat (Whole-Tone Flat) +//! - `db`: 3/4-Tone Flat +//! - `b`: Flat (1/2-Tone Flat) +//! - `d`: 1/4-Tone Flat +//! - `n`: Natural +//! - `t`: 1/4-Tone Sharp +//! - `#`: Sharp (1/2-Tone Sharp) +//! - `t#`: 3/4-Tone Sharp +//! - `x`: Double Sharp (Whole-Tone Sharp) +//! +//! **octave**: Required octave. `-`=-1,`0`,`1`,`2`,`3`,`4`,`5`,`6`,`7`,`8`,`9` +//! +//! **articulation**: Optional articulation. +//! +//! - `^`: Marcato (separated sharp attack) +//! - `>`: Accent (sharp attack) +//! - `.`: Staccato (separated) +//! - `'`: Staccatissimo (very separated) +//! - `_`: Tenuto (connected) +//! - `_.`: Tenuto Staccato +//! - `^.`: Marcato Staccato +//! - `^_`: Marcato Tenuto +//! - `>.`: Accent Staccato +//! - `>_`: Accent Tenuto + +use std::{fmt, str::FromStr}; +use crate::Fraction; + +mod articulation; +mod pitch; + +pub use self::articulation::*; +pub use self::pitch::*; + +/// A note. +pub struct Note { + /// Pitch & Octave + pub pitch: Option<(PitchClass, PitchOctave)>, + /// Duration of the note as a fraction. + pub duration: Fraction, + /// Articulation. + pub articulation: Vec, +} + +impl fmt::Display for Note { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write duration. + if self.duration.num != 1 { + write!(f, "{}/", self.duration.num)?; + } + write!(f, "{}", self.duration.den)?; + + // Write note name & octave. + match &self.pitch { + Some(pitch) => { + let class = match pitch.0.name { + PitchName::A => "A", + PitchName::B => "B", + PitchName::C => "C", + PitchName::D => "D", + PitchName::E => "E", + PitchName::F => "F", + PitchName::G => "G", + }; + write!(f, "{}{}", class, pitch.1) + }, + None => write!(f, "R"), + } + } +} + +impl FromStr for Note { + type Err = (); + + fn from_str(s: &str) -> Result { + // Read duration. + let start_index = 0; + let mut end_index = 0; + for (i, c) in s.char_indices() { + if !c.is_numeric() { + end_index = i; + break; + } + } + + // Read rest of duration. + let duration = if s[end_index..].chars().next() == Some('/') { + let numer = s[start_index..end_index].parse::().or(Err(()))?; + + end_index += 1; + + let start_index = end_index; + + for (i, c) in s[start_index..].char_indices() { + if !c.is_numeric() { + end_index = i; + break; + } + } + + let denom = s[start_index..end_index].parse::().or(Err(()))?; + + Fraction::new(numer, denom) + } else { + let numer = 1; + let denom = s[start_index..end_index].parse::().or(Err(()))?; + + Fraction::new(numer, denom) + }; + let articulation = vec![]; + + // Read note name. + match s.get(end_index..).ok_or(())? { + "R" => Ok(Note { + pitch: None, + duration, + articulation, + }), + a => { + let two = a.chars().collect::>(); + let letter_name = two[0]; + let octave_num = match two[1] { + '-' => PitchOctave::Octave_, + '0' => PitchOctave::Octave0, + '1' => PitchOctave::Octave1, + '2' => PitchOctave::Octave2, + '3' => PitchOctave::Octave3, + '4' => PitchOctave::Octave4, + '5' => PitchOctave::Octave5, + '6' => PitchOctave::Octave6, + '7' => PitchOctave::Octave7, + '8' => PitchOctave::Octave8, + '9' => PitchOctave::Octave9, + _ => return Err(()), + }; + + Ok(Note { + pitch: Some(( + match letter_name { + 'A' => PitchClass { + name: PitchName::A, + accidental: None, + }, + 'B' => PitchClass { + name: PitchName::B, + accidental: None, + }, + 'C' => PitchClass { + name: PitchName::C, + accidental: None, + }, + 'D' => PitchClass { + name: PitchName::D, + accidental: None, + }, + 'E' => PitchClass { + name: PitchName::E, + accidental: None, + }, + 'F' => PitchClass { + name: PitchName::F, + accidental: None, + }, + 'G' => PitchClass { + name: PitchName::G, + accidental: None, + }, + // FIXME: return Err + a => panic!("Failed to parse '{}'", a), + }, + octave_num, + )), + duration, + articulation, + }) + } + } + } +} + +impl Note { + /// Get the note's visual distance from middle C (C4). + pub fn visual_distance(&self) -> i32 { + if let Some(ref pitch) = self.pitch { + // Calculate number of octaves from middle C (C4). + let octaves = pitch.1 as i32 - 4; + // Calculate number of steps from C within key. + let steps = match pitch.0.name { + PitchName::C => 0, + PitchName::D => 1, + PitchName::E => 2, + PitchName::F => 3, + PitchName::G => 4, + PitchName::A => 5, + PitchName::B => 6, + }; + // Calculate total number of steps from middle C. + let total_steps = steps + octaves * 7; + + // Invert value, because higher notes = -y + -total_steps + } else { + 0 + } + } + + /// Set pitch class and octave. + pub fn set_pitch(&mut self, pitch: (PitchClass, PitchOctave)) { + self.pitch = Some(pitch); + } + + /// Set duration of note. + pub fn set_duration(&mut self, duration: Fraction) { + self.duration = duration; + } + + fn move_step(&self, create: (PitchClass, PitchOctave), run: &dyn Fn(&(PitchClass, PitchOctave)) -> Option<(PitchClass, PitchOctave)>) -> Note { + let pitch = if let Some(ref pitch) = self.pitch { + (run)(pitch) + } else { + Some(create) + }; + + Note { + pitch, + duration: self.duration, + articulation: self.articulation.clone(), + } + } + + /// Calculate note one quarter step up. + pub fn quarter_step_up(&self, create: (PitchClass, PitchOctave)) -> Note { + self.step_up(create) // FIXME + } + + /// Calculate note one quarter step down. + pub fn quarter_step_down(&self, create: (PitchClass, PitchOctave)) -> Note { + self.step_down(create) // FIXME + } + + /// Calculate note one half step up. + pub fn half_step_up(&self, create: (PitchClass, PitchOctave)) -> Note { + self.step_up(create) // FIXME + } + + /// Calculate note one half step down. + pub fn half_step_down(&self, create: (PitchClass, PitchOctave)) -> Note { + self.step_down(create) // FIXME + } + + /// Calculate note one step up within the key. + /// - `create`: Note that is generated from a rest. + pub fn step_up(&self, create: (PitchClass, PitchOctave)) -> Note { + self.move_step(create, &|pitch| { + let (pitch_class, offset) = match pitch.0.name { + PitchName::A => (PitchName::B, false), + PitchName::B => (PitchName::C, true), + PitchName::C => (PitchName::D, false), + PitchName::D => (PitchName::E, false), + PitchName::E => (PitchName::F, false), + PitchName::F => (PitchName::G, false), + PitchName::G => (PitchName::A, false), + }; + let pitch_octave = if offset { + pitch.1.raise() + } else { + Some(pitch.1) + }; + + if let Some(pitch_octave) = pitch_octave { + Some((PitchClass { + name: pitch_class, + accidental: pitch.0.accidental, + }, pitch_octave)) + } else { + Some((pitch.0, pitch.1)) + } + }) + } + + /// Calculate note one step down within the key. + /// - `create`: Note that is generated from a rest. + pub fn step_down(&self, create: (PitchClass, PitchOctave)) -> Note { + self.move_step(create, &|pitch| { + let (pitch_class, offset) = match pitch.0.name { + PitchName::A => (PitchName::G, false), + PitchName::B => (PitchName::A, false), + PitchName::C => (PitchName::B, true), + PitchName::D => (PitchName::C, false), + PitchName::E => (PitchName::D, false), + PitchName::F => (PitchName::E, false), + PitchName::G => (PitchName::F, false), + }; + let pitch_octave = if offset { + pitch.1.lower() + } else { + Some(pitch.1) + }; + + if let Some(pitch_octave) = pitch_octave { + Some((PitchClass { + name: pitch_class, + accidental: pitch.0.accidental, + }, pitch_octave)) + } else { + Some((pitch.0, pitch.1)) + } + }) + } +} diff --git a/src/note/pitch.rs b/src/note/pitch.rs new file mode 100644 index 0000000..b57b08b --- /dev/null +++ b/src/note/pitch.rs @@ -0,0 +1,131 @@ +use std::fmt; + +/// A Pitch Name. +#[derive(Copy, Clone)] +pub enum PitchName { + C, + D, + E, + F, + G, + A, + B, +} + +/// A Pitch Accidental. +#[derive(Copy, Clone)] +pub enum PitchAccidental { + /// + DoubleFlat, + /// + FlatQuarterFlat, + /// + Flat, + /// + QuarterFlat, + /// + Natural, + /// + QuarterSharp, + /// + Sharp, + /// + SharpQuarterSharp, + /// + DoubleSharp, +} + +/// A Pitch Class +#[derive(Copy, Clone)] +pub struct PitchClass { + pub name: PitchName, + pub accidental: Option, +} + +/// A Pitch Octave +#[derive(Copy, Clone)] +#[repr(i8)] +pub enum PitchOctave { + /// Octave -1 + Octave_ = -1, + /// Octave 0 + Octave0 = 0, + /// Octave 1 + Octave1 = 1, + /// Octave 2 + Octave2 = 2, + /// Octave 3 + Octave3 = 3, + /// Octave 4 + Octave4 = 4, + /// Octave 5 + Octave5 = 5, + /// Octave 6 + Octave6 = 6, + /// Octave 7 + Octave7 = 7, + /// Octave 8 + Octave8 = 8, + /// Octave 9 + Octave9 = 9, +} + +impl PitchOctave { + /// Calculate a lower octave. + pub fn lower(self) -> Option { + use PitchOctave::*; + + match self { + Octave_ => None, + Octave0 => Some(Octave_), + Octave1 => Some(Octave0), + Octave2 => Some(Octave1), + Octave3 => Some(Octave2), + Octave4 => Some(Octave3), + Octave5 => Some(Octave4), + Octave6 => Some(Octave5), + Octave7 => Some(Octave6), + Octave8 => Some(Octave7), + Octave9 => Some(Octave8), + } + } + + /// Calculate a higher octave. + pub fn raise(self) -> Option { + use PitchOctave::*; + + match self { + Octave_ => Some(Octave0), + Octave0 => Some(Octave1), + Octave1 => Some(Octave2), + Octave2 => Some(Octave3), + Octave3 => Some(Octave4), + Octave4 => Some(Octave5), + Octave5 => Some(Octave6), + Octave6 => Some(Octave7), + Octave7 => Some(Octave8), + Octave8 => Some(Octave9), + Octave9 => None, + } + } +} + +impl fmt::Display for PitchOctave { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use PitchOctave::*; + + match self { + Octave_ => write!(f, "-"), + Octave0 => write!(f, "0"), + Octave1 => write!(f, "1"), + Octave2 => write!(f, "2"), + Octave3 => write!(f, "3"), + Octave4 => write!(f, "4"), + Octave5 => write!(f, "5"), + Octave6 => write!(f, "6"), + Octave7 => write!(f, "7"), + Octave8 => write!(f, "8"), + Octave9 => write!(f, "9"), + } + } +}