use super::{
duration::Duration,
notation::{
Articulation, Barline, BeamState, ChordSymbol, Clef, CrossStaff, Dynamic, GuitarTechnique,
HairpinKind, KeySignature, Lyric, NoteHead, OttavaKind, StyledText, TablatureConfig,
TimeSignature, TupletInfo,
},
pitch::Pitch,
};
use crate::Error;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreMetadata {
pub title: String,
pub composer: String,
pub lyricist: String,
pub copyright: String,
pub work_number: String,
pub movement_title: String,
}
impl Default for ScoreMetadata {
fn default() -> Self {
Self {
title: "Untitled Score".to_string(),
composer: String::new(),
lyricist: String::new(),
copyright: String::new(),
work_number: String::new(),
movement_title: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreSettings {
pub tempo_bpm: u16,
pub time_signature: TimeSignature,
pub key_signature: KeySignature,
}
impl Default for ScoreSettings {
fn default() -> Self {
Self {
tempo_bpm: 120,
time_signature: TimeSignature::default(),
key_signature: KeySignature::default(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PartGroupSymbol {
Bracket, Brace, Line, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartGroup {
pub first_part: usize,
pub last_part: usize,
pub symbol: PartGroupSymbol,
#[serde(default)]
pub barlines_connect: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Score {
pub id: String,
#[serde(default)]
pub schema_version: u32,
pub metadata: ScoreMetadata,
pub settings: ScoreSettings,
pub parts: Vec<Part>,
#[serde(default)]
pub part_groups: Vec<PartGroup>,
#[serde(default)]
pub texts: Vec<StyledText>,
}
impl Default for Score {
fn default() -> Self {
let mut part = Part::new("Piano", "Pno.");
part.staves.push(Staff::new(Clef::Treble));
for _ in 0..4 {
part.staves[0].measures.push(Measure::empty(4, 4));
}
Self {
id: Uuid::new_v4().to_string(),
schema_version: 1,
metadata: ScoreMetadata::default(),
settings: ScoreSettings::default(),
parts: vec![part],
part_groups: Vec::new(),
texts: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScoreTemplate {
Solo,
Piano,
StringQuartet,
StringOrchestra,
BrassQuintet,
}
impl Score {
pub fn new(
title: &str,
tempo_bpm: u16,
numerator: u8,
denominator: u8,
fifths: i8,
measure_count: u32,
) -> Self {
let mut score = Score::default();
score.metadata.title = title.to_string();
score.settings.tempo_bpm = tempo_bpm;
score.settings.time_signature = TimeSignature {
numerator,
denominator,
};
score.settings.key_signature = KeySignature {
fifths,
mode: "major".to_string(),
};
score.parts[0].staves[0].measures.clear();
for i in 0..measure_count {
let mut m = Measure::empty(numerator, denominator);
m.number = i + 1;
score.parts[0].staves[0].measures.push(m);
}
score
}
pub fn template(kind: ScoreTemplate) -> Self {
fn measures(num: u8, den: u8, count: u32) -> Vec<Measure> {
(0..count)
.map(|i| {
let mut m = Measure::empty(num, den);
m.number = i + 1;
m
})
.collect()
}
fn part(name: &str, short: &str, clef: Clef, program: u8) -> Part {
let mut p = Part::new(name, short);
p.midi_program = program;
let mut s = Staff::new(clef);
s.measures = measures(4, 4, 4);
p.staves.push(s);
p
}
let mut score = Score {
id: uuid::Uuid::new_v4().to_string(),
schema_version: 1,
metadata: ScoreMetadata::default(),
settings: ScoreSettings::default(),
parts: Vec::new(),
part_groups: Vec::new(),
texts: Vec::new(),
};
match kind {
ScoreTemplate::Solo => {
score.parts.push(part("Piano", "Pno.", Clef::Treble, 0));
}
ScoreTemplate::Piano => {
let mut p = Part::new("Piano", "Pno.");
p.midi_program = 0;
let mut treble = Staff::new(Clef::Treble);
treble.measures = measures(4, 4, 4);
let mut bass = Staff::new(Clef::Bass);
bass.measures = measures(4, 4, 4);
p.staves.push(treble);
p.staves.push(bass);
score.parts.push(p);
}
ScoreTemplate::StringQuartet => {
score
.parts
.push(part("Violin I", "Vn. I", Clef::Treble, 40));
score
.parts
.push(part("Violin II", "Vn. II", Clef::Treble, 40));
score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
}
ScoreTemplate::StringOrchestra => {
score
.parts
.push(part("Violin I", "Vn. I", Clef::Treble, 40));
score
.parts
.push(part("Violin II", "Vn. II", Clef::Treble, 40));
score.parts.push(part("Viola", "Va.", Clef::Alto, 41));
score.parts.push(part("Cello", "Vc.", Clef::Bass, 42));
score.parts.push(part("Contrabass", "Cb.", Clef::Bass, 43));
}
ScoreTemplate::BrassQuintet => {
score
.parts
.push(part("Trumpet I", "Tpt. I", Clef::Treble, 56));
score
.parts
.push(part("Trumpet II", "Tpt. II", Clef::Treble, 56));
score
.parts
.push(part("French Horn", "Hn.", Clef::Treble, 60));
score.parts.push(part("Trombone", "Tbn.", Clef::Bass, 57));
score.parts.push(part("Tuba", "Tba.", Clef::Bass, 58));
}
}
score
}
pub fn measure_count(&self) -> usize {
self.parts
.first()
.and_then(|p| p.staves.first())
.map(|s| s.measures.len())
.unwrap_or(0)
}
pub fn statistics(&self) -> ScoreStats {
let measure_count = self.measure_count();
let part_count = self.parts.len();
let seq = measure_sequence(self);
let total_beats: f64 = self
.parts
.first()
.and_then(|p| p.staves.first())
.map(|s| {
seq.iter()
.filter_map(|&idx| s.measures.get(idx))
.flat_map(|m| m.voices.iter().flat_map(|v| v.iter()))
.map(|n| n.beats())
.sum()
})
.unwrap_or(0.0);
let mut note_count = 0usize;
let mut rest_count = 0usize;
for part in &self.parts {
for staff in &part.staves {
for measure in &staff.measures {
for voice in &measure.voices {
for note in voice {
if note.is_rest {
rest_count += 1;
} else {
note_count += 1;
}
}
}
}
}
}
let bpm = self.settings.tempo_bpm as f64;
let estimated_duration_secs = if bpm > 0.0 {
total_beats / bpm * 60.0
} else {
0.0
};
ScoreStats {
measure_count,
note_count,
rest_count,
part_count,
estimated_duration_secs,
}
}
pub fn extract_part(&self, part_index: usize) -> Option<Score> {
let part = self.parts.get(part_index)?.clone();
Some(Score {
id: Uuid::new_v4().to_string(),
schema_version: 1,
metadata: self.metadata.clone(),
settings: self.settings.clone(),
parts: vec![part],
part_groups: Vec::new(),
texts: self.texts.clone(),
})
}
pub fn merge(&self, other: &Score) -> Score {
let self_count = self.measure_count();
let other_count = other.measure_count();
let max_count = self_count.max(other_count);
let ts = self.settings.time_signature.clone();
let pad = |mut part: Part, from: usize| -> Part {
for staff in &mut part.staves {
for i in from..max_count {
let mut m = Measure::empty(ts.numerator, ts.denominator);
m.number = i as u32 + 1;
staff.measures.push(m);
}
}
part
};
let mut parts: Vec<Part> = self
.parts
.iter()
.cloned()
.map(|p| pad(p, self_count))
.collect();
for p in &other.parts {
parts.push(pad(p.clone(), other_count));
}
Score {
id: Uuid::new_v4().to_string(),
schema_version: 1,
metadata: self.metadata.clone(),
settings: self.settings.clone(),
parts,
part_groups: Vec::new(),
texts: self.texts.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScoreStats {
pub measure_count: usize,
pub note_count: usize,
pub rest_count: usize,
pub part_count: usize,
pub estimated_duration_secs: f64,
}
use super::pitch::Step;
use super::repeat::measure_sequence;
pub fn transpose(score: &Score, semitones: i8) -> Score {
if semitones == 0 {
return score.clone();
}
let mut out = score.clone();
out.settings.key_signature.fifths = transpose_fifths(
score.settings.key_signature.fifths,
&score.settings.key_signature.mode,
semitones,
);
for part in &mut out.parts {
for staff in &mut part.staves {
for measure in &mut staff.measures {
if let Some(ref mut ks) = measure.key_sig {
ks.fifths = transpose_fifths(ks.fifths, &ks.mode, semitones);
}
for voice in &mut measure.voices {
for note in voice.iter_mut() {
for pitch in note.pitches.iter_mut() {
*pitch = transpose_pitch(pitch, semitones);
}
}
}
}
}
}
out
}
fn transpose_pitch(pitch: &Pitch, semitones: i8) -> Pitch {
let new_midi = (pitch.to_midi() + semitones as i16).clamp(0, 127) as u8;
let pc = new_midi % 12;
let oct = (new_midi / 12) as i8 - 1;
let (step, alter): (Step, i8) = if semitones >= 0 {
match pc {
0 => (Step::C, 0),
1 => (Step::C, 1),
2 => (Step::D, 0),
3 => (Step::D, 1),
4 => (Step::E, 0),
5 => (Step::F, 0),
6 => (Step::F, 1),
7 => (Step::G, 0),
8 => (Step::G, 1),
9 => (Step::A, 0),
10 => (Step::A, 1),
11 => (Step::B, 0),
_ => (Step::C, 0),
}
} else {
match pc {
0 => (Step::C, 0),
1 => (Step::D, -1),
2 => (Step::D, 0),
3 => (Step::E, -1),
4 => (Step::E, 0),
5 => (Step::F, 0),
6 => (Step::G, -1),
7 => (Step::G, 0),
8 => (Step::A, -1),
9 => (Step::A, 0),
10 => (Step::B, -1),
11 => (Step::B, 0),
_ => (Step::C, 0),
}
};
Pitch::with_microtone(step, oct, alter, pitch.microtone_cents)
}
fn transpose_fifths(fifths: i8, mode: &str, semitones: i8) -> i8 {
let tonic_major_pc = ((fifths as i32 * 7).rem_euclid(12)) as u8;
let tonic_pc = if mode == "minor" {
((tonic_major_pc as i32 + 9).rem_euclid(12)) as u8
} else {
tonic_major_pc
};
let new_tonic = ((tonic_pc as i32 + semitones as i32).rem_euclid(12)) as u8;
let major_tonic = if mode == "minor" {
((new_tonic as i32 + 3).rem_euclid(12)) as u8
} else {
new_tonic
};
let raw = ((major_tonic as i32 * 7).rem_euclid(12)) as i8;
if raw > 6 { raw - 12 } else { raw }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Part {
pub id: String,
pub name: String,
pub short_name: String,
pub staves: Vec<Staff>,
#[serde(default)]
pub midi_channel: u8,
#[serde(default)]
pub midi_program: u8,
}
impl Part {
pub fn new(name: &str, short_name: &str) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name: name.to_string(),
short_name: short_name.to_string(),
staves: Vec::new(),
midi_channel: 0,
midi_program: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Staff {
pub clef: Clef,
pub measures: Vec<Measure>,
#[serde(default)]
pub transpose_semitones: i8,
#[serde(default)]
pub tablature: Option<TablatureConfig>,
}
impl Staff {
pub fn new(clef: Clef) -> Self {
Self {
clef,
measures: Vec::new(),
transpose_semitones: 0,
tablature: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VoltaBracket {
pub number: u8,
pub kind: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Measure {
pub number: u32,
pub time_sig: Option<TimeSignature>,
pub key_sig: Option<KeySignature>,
pub clef: Option<Clef>,
pub tempo: Option<u16>,
pub barline_left: Barline,
pub barline_right: Barline,
#[serde(default)]
pub volta: Option<VoltaBracket>,
#[serde(default)]
pub tempo_text: Option<String>,
#[serde(default)]
pub rehearsal: Option<String>,
#[serde(default)]
pub navigation: Option<String>,
#[serde(default)]
pub expression_text: Option<String>,
#[serde(default)]
pub texts: Vec<StyledText>,
#[serde(default)]
pub multi_rest_count: Option<u8>,
#[serde(default)]
pub system_break: bool,
#[serde(default)]
pub page_break: bool,
pub voices: [Vec<Note>; 4],
}
impl Measure {
pub fn empty(numerator: u8, denominator: u8) -> Self {
let total_beats = TimeSignature {
numerator,
denominator,
}
.total_beats();
let mut voice0: Vec<Note> = Vec::new();
let mut remaining = total_beats;
while remaining > 1e-9 {
let dur = Duration::whole_filling_beats(remaining);
remaining -= dur.beats(0);
voice0.push(Note::rest(dur));
}
Self {
number: 0,
time_sig: None,
key_sig: None,
clef: None,
tempo: None,
barline_left: Barline::Normal,
barline_right: Barline::Normal,
volta: None,
tempo_text: None,
rehearsal: None,
navigation: None,
expression_text: None,
texts: Vec::new(),
multi_rest_count: None,
system_break: false,
page_break: false,
voices: [voice0, vec![], vec![], vec![]],
}
}
pub fn renumber(&mut self, n: u32) {
self.number = n;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Note {
pub id: String,
pub is_rest: bool,
pub pitches: Vec<Pitch>,
#[serde(default)]
pub tab_position: Option<super::notation::TabPosition>,
pub duration: Duration,
pub dot_count: u8,
pub tie_start: bool,
pub tie_end: bool,
pub beam: BeamState,
pub articulations: Vec<Articulation>,
pub dynamic: Option<Dynamic>,
pub stem_up: Option<bool>,
#[serde(default)]
pub hairpin_start: Option<HairpinKind>,
#[serde(default)]
pub hairpin_end: bool,
#[serde(default)]
pub tuplet: Option<TupletInfo>,
#[serde(default)]
pub chord_symbol: Option<ChordSymbol>,
#[serde(default)]
pub is_grace: bool,
#[serde(default)]
pub grace_slash: bool,
#[serde(default)]
pub ottava_start: Option<OttavaKind>,
#[serde(default)]
pub ottava_end: bool,
#[serde(default)]
pub lyric: Option<Lyric>,
#[serde(default)]
pub pedal_start: bool,
#[serde(default)]
pub pedal_end: bool,
#[serde(default)]
pub slur_start: bool,
#[serde(default)]
pub slur_end: bool,
#[serde(default)]
pub arpeggiate: Option<bool>,
#[serde(default)]
pub technique_text: Option<String>,
#[serde(default)]
pub glissando_start: bool,
#[serde(default)]
pub glissando_end: bool,
#[serde(default)]
pub cross_staff: Option<CrossStaff>,
#[serde(default)]
pub fingering: Option<u8>,
#[serde(default)]
pub string_number: Option<u8>,
#[serde(default)]
pub note_head: NoteHead,
#[serde(default)]
pub is_cue: bool,
#[serde(default)]
pub trill_line_start: bool,
#[serde(default)]
pub trill_line_end: bool,
#[serde(default)]
pub guitar_technique: Option<GuitarTechnique>,
}
impl Note {
pub fn new(pitch: Pitch, duration: Duration) -> Self {
Self {
id: Uuid::new_v4().to_string(),
is_rest: false,
pitches: vec![pitch],
tab_position: None,
duration,
dot_count: 0,
tie_start: false,
tie_end: false,
beam: BeamState::None,
articulations: Vec::new(),
dynamic: None,
stem_up: None,
hairpin_start: None,
hairpin_end: false,
tuplet: None,
chord_symbol: None,
is_grace: false,
grace_slash: false,
ottava_start: None,
ottava_end: false,
lyric: None,
pedal_start: false,
pedal_end: false,
slur_start: false,
slur_end: false,
arpeggiate: None,
technique_text: None,
glissando_start: false,
glissando_end: false,
cross_staff: None,
fingering: None,
string_number: None,
note_head: NoteHead::Normal,
is_cue: false,
trill_line_start: false,
trill_line_end: false,
guitar_technique: None,
}
}
pub fn rest(duration: Duration) -> Self {
Self {
id: Uuid::new_v4().to_string(),
is_rest: true,
pitches: Vec::new(),
tab_position: None,
duration,
dot_count: 0,
tie_start: false,
tie_end: false,
beam: BeamState::None,
articulations: Vec::new(),
dynamic: None,
stem_up: None,
hairpin_start: None,
hairpin_end: false,
tuplet: None,
chord_symbol: None,
is_grace: false,
grace_slash: false,
ottava_start: None,
ottava_end: false,
lyric: None,
pedal_start: false,
pedal_end: false,
slur_start: false,
slur_end: false,
arpeggiate: None,
technique_text: None,
glissando_start: false,
glissando_end: false,
cross_staff: None,
fingering: None,
string_number: None,
note_head: NoteHead::Normal,
is_cue: false,
trill_line_start: false,
trill_line_end: false,
guitar_technique: None,
}
}
pub fn beats(&self) -> f64 {
if self.is_grace || self.is_cue {
return 0.0;
}
let base = self.duration.beats(self.dot_count);
if let Some(ref t) = self.tuplet {
base * (t.normal_notes as f64) / (t.actual_notes as f64)
} else {
base
}
}
}
impl Duration {
pub fn whole_filling_beats(beats: f64) -> Duration {
if beats >= 4.0 {
Duration::Whole
} else if beats >= 2.0 {
Duration::Half
} else if beats >= 1.0 {
Duration::Quarter
} else if beats >= 0.5 {
Duration::Eighth
} else if beats >= 0.25 {
Duration::Sixteenth
} else if beats >= 0.125 {
Duration::ThirtySecond
} else {
Duration::SixtyFourth
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoteAddr {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub voice: usize,
pub note: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ScoreChange {
MetadataChanged {
field: String,
old: String,
new: String,
},
TempoChanged {
old: u16,
new: u16,
},
KeySignatureChanged {
old: KeySignature,
new: KeySignature,
},
PartAdded {
part_index: usize,
},
PartRemoved {
part_index: usize,
name: String,
},
NoteAdded {
part: usize,
staff: usize,
measure: usize,
voice: usize,
note_index: usize,
},
NoteRemoved {
part: usize,
staff: usize,
measure: usize,
voice: usize,
note: Box<Note>,
},
NoteModified {
part: usize,
staff: usize,
measure: usize,
voice: usize,
note_index: usize,
old: Box<Note>,
new: Box<Note>,
},
TimeSigChanged {
part: usize,
staff: usize,
measure: usize,
old: Option<TimeSignature>,
new: Option<TimeSignature>,
},
MeasureTempoChanged {
part: usize,
staff: usize,
measure: usize,
old: Option<u16>,
new: Option<u16>,
},
BarlineChanged {
part: usize,
staff: usize,
measure: usize,
},
RehearsalMarkChanged {
part: usize,
staff: usize,
measure: usize,
old: Option<String>,
new: Option<String>,
},
VoltaChanged {
part: usize,
staff: usize,
measure: usize,
},
}
pub fn diff(a: &Score, b: &Score) -> Vec<ScoreChange> {
let mut changes: Vec<ScoreChange> = Vec::new();
macro_rules! meta {
($field:ident, $name:literal) => {
if a.metadata.$field != b.metadata.$field {
changes.push(ScoreChange::MetadataChanged {
field: $name.to_string(),
old: a.metadata.$field.clone(),
new: b.metadata.$field.clone(),
});
}
};
}
meta!(title, "title");
meta!(composer, "composer");
meta!(lyricist, "lyricist");
meta!(copyright, "copyright");
meta!(work_number, "work_number");
meta!(movement_title, "movement_title");
if a.settings.tempo_bpm != b.settings.tempo_bpm {
changes.push(ScoreChange::TempoChanged {
old: a.settings.tempo_bpm,
new: b.settings.tempo_bpm,
});
}
if a.settings.key_signature != b.settings.key_signature {
changes.push(ScoreChange::KeySignatureChanged {
old: a.settings.key_signature.clone(),
new: b.settings.key_signature.clone(),
});
}
let a_len = a.parts.len();
let b_len = b.parts.len();
for i in b_len..a_len {
changes.push(ScoreChange::PartRemoved {
part_index: i,
name: a.parts[i].name.clone(),
});
}
for i in a_len..b_len {
changes.push(ScoreChange::PartAdded { part_index: i });
}
for pi in 0..a_len.min(b_len) {
let ap = &a.parts[pi];
let bp = &b.parts[pi];
for si in 0..ap.staves.len().min(bp.staves.len()) {
let a_staff = &ap.staves[si];
let b_staff = &bp.staves[si];
for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
let am = &a_staff.measures[mi];
let bm = &b_staff.measures[mi];
for vi in 0..4usize {
let av = &am.voices[vi];
let bv = &bm.voices[vi];
for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
if !note_content_eq(a_note, b_note) {
changes.push(ScoreChange::NoteModified {
part: pi,
staff: si,
measure: mi,
voice: vi,
note_index: ni,
old: Box::new(a_note.clone()),
new: Box::new(b_note.clone()),
});
}
}
for note in av.iter().skip(bv.len()) {
changes.push(ScoreChange::NoteRemoved {
part: pi,
staff: si,
measure: mi,
voice: vi,
note: Box::new(note.clone()),
});
}
for ni in av.len()..bv.len() {
changes.push(ScoreChange::NoteAdded {
part: pi,
staff: si,
measure: mi,
voice: vi,
note_index: ni,
});
}
}
if am.time_sig != bm.time_sig {
changes.push(ScoreChange::TimeSigChanged {
part: pi,
staff: si,
measure: mi,
old: am.time_sig.clone(),
new: bm.time_sig.clone(),
});
}
if am.tempo != bm.tempo {
changes.push(ScoreChange::MeasureTempoChanged {
part: pi,
staff: si,
measure: mi,
old: am.tempo,
new: bm.tempo,
});
}
if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
changes.push(ScoreChange::BarlineChanged {
part: pi,
staff: si,
measure: mi,
});
}
if am.rehearsal != bm.rehearsal {
changes.push(ScoreChange::RehearsalMarkChanged {
part: pi,
staff: si,
measure: mi,
old: am.rehearsal.clone(),
new: bm.rehearsal.clone(),
});
}
if am.volta != bm.volta {
changes.push(ScoreChange::VoltaChanged {
part: pi,
staff: si,
measure: mi,
});
}
}
}
}
changes
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ScorePatch {
SetMetadata {
field: String,
value: String,
},
SetTempo {
value: u16,
},
SetKeySignature {
part: usize,
staff: usize,
measure: usize,
value: Option<KeySignature>,
},
SetTimeSignature {
part: usize,
staff: usize,
measure: usize,
value: Option<TimeSignature>,
},
SetBarlines {
part: usize,
staff: usize,
measure: usize,
left: Barline,
right: Barline,
},
SetRehearsal {
part: usize,
staff: usize,
measure: usize,
value: Option<String>,
},
SetVolta {
part: usize,
staff: usize,
measure: usize,
value: Option<VoltaBracket>,
},
AddNote {
part: usize,
staff: usize,
measure: usize,
voice: usize,
#[serde(default = "legacy_append_index")]
note_index: usize,
note: Box<Note>,
},
RemoveNote {
part: usize,
staff: usize,
measure: usize,
voice: usize,
note_index: usize,
},
ReplaceNote {
part: usize,
staff: usize,
measure: usize,
voice: usize,
note_index: usize,
note: Box<Note>,
},
SetMeasureTempo {
part: usize,
staff: usize,
measure: usize,
value: Option<u16>,
},
ReplaceScore {
score: Box<Score>,
},
}
fn legacy_append_index() -> usize {
usize::MAX
}
fn patch_requires_replace(a: &Score, b: &Score) -> bool {
if a.settings.time_signature != b.settings.time_signature
|| a.settings.key_signature != b.settings.key_signature
|| a.parts.len() != b.parts.len()
|| a.part_groups.len() != b.part_groups.len()
{
return true;
}
if a.part_groups.iter().zip(&b.part_groups).any(|(x, y)| {
x.first_part != y.first_part
|| x.last_part != y.last_part
|| x.symbol != y.symbol
|| x.barlines_connect != y.barlines_connect
}) {
return true;
}
for (ap, bp) in a.parts.iter().zip(&b.parts) {
if ap.name != bp.name
|| ap.short_name != bp.short_name
|| ap.midi_channel != bp.midi_channel
|| ap.midi_program != bp.midi_program
|| ap.staves.len() != bp.staves.len()
{
return true;
}
for (as_, bs) in ap.staves.iter().zip(&bp.staves) {
if as_.clef != bs.clef
|| as_.transpose_semitones != bs.transpose_semitones
|| as_.measures.len() != bs.measures.len()
{
return true;
}
for (am, bm) in as_.measures.iter().zip(&bs.measures) {
if am.number != bm.number
|| am.clef != bm.clef
|| am.tempo_text != bm.tempo_text
|| am.navigation != bm.navigation
|| am.expression_text != bm.expression_text
|| am.multi_rest_count != bm.multi_rest_count
|| am.system_break != bm.system_break
|| am.page_break != bm.page_break
{
return true;
}
}
}
}
false
}
pub fn score_patch(a: &Score, b: &Score) -> Vec<ScorePatch> {
let mut patches: Vec<ScorePatch> = Vec::new();
if patch_requires_replace(a, b) {
return vec![ScorePatch::ReplaceScore {
score: Box::new(b.clone()),
}];
}
macro_rules! meta {
($field:ident, $name:literal) => {
if a.metadata.$field != b.metadata.$field {
patches.push(ScorePatch::SetMetadata {
field: $name.to_string(),
value: b.metadata.$field.clone(),
});
}
};
}
meta!(title, "title");
meta!(composer, "composer");
meta!(lyricist, "lyricist");
meta!(copyright, "copyright");
meta!(work_number, "work_number");
meta!(movement_title, "movement_title");
if a.settings.tempo_bpm != b.settings.tempo_bpm {
patches.push(ScorePatch::SetTempo {
value: b.settings.tempo_bpm,
});
}
for pi in 0..a.parts.len().min(b.parts.len()) {
let ap = &a.parts[pi];
let bp = &b.parts[pi];
for si in 0..ap.staves.len().min(bp.staves.len()) {
let a_staff = &ap.staves[si];
let b_staff = &bp.staves[si];
for mi in 0..a_staff.measures.len().min(b_staff.measures.len()) {
let am = &a_staff.measures[mi];
let bm = &b_staff.measures[mi];
if am.key_sig != bm.key_sig {
patches.push(ScorePatch::SetKeySignature {
part: pi,
staff: si,
measure: mi,
value: bm.key_sig.clone(),
});
}
if am.time_sig != bm.time_sig {
patches.push(ScorePatch::SetTimeSignature {
part: pi,
staff: si,
measure: mi,
value: bm.time_sig.clone(),
});
}
if am.barline_left != bm.barline_left || am.barline_right != bm.barline_right {
patches.push(ScorePatch::SetBarlines {
part: pi,
staff: si,
measure: mi,
left: bm.barline_left.clone(),
right: bm.barline_right.clone(),
});
}
if am.rehearsal != bm.rehearsal {
patches.push(ScorePatch::SetRehearsal {
part: pi,
staff: si,
measure: mi,
value: bm.rehearsal.clone(),
});
}
if am.volta != bm.volta {
patches.push(ScorePatch::SetVolta {
part: pi,
staff: si,
measure: mi,
value: bm.volta.clone(),
});
}
if am.tempo != bm.tempo {
patches.push(ScorePatch::SetMeasureTempo {
part: pi,
staff: si,
measure: mi,
value: bm.tempo,
});
}
for vi in 0..4usize {
let av = &am.voices[vi];
let bv = &bm.voices[vi];
for (ni, (a_note, b_note)) in av.iter().zip(bv.iter()).enumerate() {
if !note_content_eq(a_note, b_note) {
patches.push(ScorePatch::ReplaceNote {
part: pi,
staff: si,
measure: mi,
voice: vi,
note_index: ni,
note: Box::new(b_note.clone()),
});
}
}
for ni in (bv.len()..av.len()).rev() {
patches.push(ScorePatch::RemoveNote {
part: pi,
staff: si,
measure: mi,
voice: vi,
note_index: ni,
});
}
for (offset, note) in bv.iter().skip(av.len()).enumerate() {
patches.push(ScorePatch::AddNote {
part: pi,
staff: si,
measure: mi,
voice: vi,
note_index: av.len() + offset,
note: Box::new(note.clone()),
});
}
}
}
}
}
patches
}
pub fn apply_patch(score: &Score, patches: &[ScorePatch]) -> Result<Score, Error> {
let mut s = score.clone();
for patch in patches {
match patch {
ScorePatch::ReplaceScore { score } => {
s = (**score).clone();
}
ScorePatch::SetMetadata { field, value } => match field.as_str() {
"title" => s.metadata.title = value.clone(),
"composer" => s.metadata.composer = value.clone(),
"lyricist" => s.metadata.lyricist = value.clone(),
"copyright" => s.metadata.copyright = value.clone(),
"work_number" => s.metadata.work_number = value.clone(),
"movement_title" => s.metadata.movement_title = value.clone(),
other => {
return Err(Error::InvalidPatch(format!(
"unknown metadata field: {other}"
)));
}
},
ScorePatch::SetTempo { value } => {
s.settings.tempo_bpm = *value;
}
ScorePatch::SetKeySignature {
part,
staff,
measure,
value,
} => {
s.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.key_sig = value.clone();
}
ScorePatch::SetTimeSignature {
part,
staff,
measure,
value,
} => {
s.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.time_sig = value.clone();
}
ScorePatch::SetBarlines {
part,
staff,
measure,
left,
right,
} => {
let m = s
.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| {
Error::InvalidPatch(format!("measure {measure} out of range"))
})?;
m.barline_left = left.clone();
m.barline_right = right.clone();
}
ScorePatch::SetRehearsal {
part,
staff,
measure,
value,
} => {
s.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.rehearsal = value.clone();
}
ScorePatch::SetVolta {
part,
staff,
measure,
value,
} => {
s.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.volta = value.clone();
}
ScorePatch::AddNote {
part,
staff,
measure,
voice,
note_index,
note,
} => {
let v = s
.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.voices
.get_mut(*voice)
.ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
let insert_at = if *note_index == usize::MAX {
v.len()
} else {
*note_index
};
if insert_at > v.len() {
return Err(Error::InvalidPatch(format!(
"note_index {note_index} out of range"
)));
}
v.insert(insert_at, *note.clone());
}
ScorePatch::RemoveNote {
part,
staff,
measure,
voice,
note_index,
} => {
let v = s
.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.voices
.get_mut(*voice)
.ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
if *note_index >= v.len() {
return Err(Error::InvalidPatch(format!(
"note_index {note_index} out of range"
)));
}
v.remove(*note_index);
}
ScorePatch::ReplaceNote {
part,
staff,
measure,
voice,
note_index,
note,
} => {
let v = s
.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.voices
.get_mut(*voice)
.ok_or_else(|| Error::InvalidPatch(format!("voice {voice} out of range")))?;
if *note_index >= v.len() {
return Err(Error::InvalidPatch(format!(
"note_index {note_index} out of range"
)));
}
v[*note_index] = *note.clone();
}
ScorePatch::SetMeasureTempo {
part,
staff,
measure,
value,
} => {
s.parts
.get_mut(*part)
.ok_or_else(|| Error::InvalidPatch(format!("part {part} out of range")))?
.staves
.get_mut(*staff)
.ok_or_else(|| Error::InvalidPatch(format!("staff {staff} out of range")))?
.measures
.get_mut(*measure)
.ok_or_else(|| Error::InvalidPatch(format!("measure {measure} out of range")))?
.tempo = *value;
}
}
}
Ok(s)
}
pub fn respell_score(score: &mut Score, prefer_flat: bool) {
for part in &mut score.parts {
for staff in &mut part.staves {
for measure in &mut staff.measures {
for voice in &mut measure.voices {
for note in voice.iter_mut() {
for pitch in &mut note.pitches {
*pitch = pitch.respell(prefer_flat);
}
}
}
}
}
}
}
pub fn respell_score_to_key(score: &mut Score) {
let prefer_flat = score.settings.key_signature.fifths < 0;
respell_score(score, prefer_flat);
}
pub fn score_duration_secs(score: &Score) -> f64 {
if score.settings.tempo_bpm == 0 {
return 0.0;
}
let seq = measure_sequence(score);
let mut total_secs = 0.0f64;
let mut current_bpm = score.settings.tempo_bpm as f64;
if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
for &idx in &seq {
if let Some(m) = staff.measures.get(idx) {
if let Some(b) = m.tempo {
current_bpm = b as f64;
}
if current_bpm == 0.0 {
continue;
}
let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
total_secs += beats / current_bpm * 60.0;
}
}
}
total_secs
}
pub fn score_duration_secs_region(score: &Score, region: (usize, usize)) -> f64 {
if score.settings.tempo_bpm == 0 {
return 0.0;
}
let seq: Vec<usize> = measure_sequence(score)
.into_iter()
.filter(|&idx| idx >= region.0 && idx <= region.1)
.collect();
let mut total_secs = 0.0f64;
let mut current_bpm = score.settings.tempo_bpm as f64;
if let Some(staff) = score.parts.first().and_then(|p| p.staves.first()) {
for &idx in &seq {
if let Some(m) = staff.measures.get(idx) {
if let Some(b) = m.tempo {
current_bpm = b as f64;
}
if current_bpm == 0.0 {
continue;
}
let beats: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
total_secs += beats / current_bpm * 60.0;
}
}
}
total_secs
}
pub fn measure_beats_remaining(
score: &Score,
part_index: usize,
staff_index: usize,
measure_index: usize,
voice_index: usize,
) -> Result<f64, Error> {
let part = score
.parts
.get(part_index)
.ok_or(Error::PartNotFound(part_index))?;
let staff = part
.staves
.get(staff_index)
.ok_or(Error::StaffNotFound(staff_index))?;
let measure = staff
.measures
.get(measure_index)
.ok_or(Error::MeasureNotFound(measure_index))?;
let voice = measure
.voices
.get(voice_index)
.ok_or(Error::VoiceOutOfRange(voice_index))?;
let ts = measure
.time_sig
.as_ref()
.unwrap_or(&score.settings.time_signature);
let used: f64 = voice.iter().map(|n| n.beats()).sum();
Ok((ts.total_beats() - used).max(0.0))
}
pub fn suggested_stem_up(pitches: &[Pitch], clef: &Clef) -> bool {
if pitches.is_empty() {
return true;
}
let avg = pitches.iter().map(|p| p.to_midi() as f64).sum::<f64>() / pitches.len() as f64;
avg < clef.middle_line_midi() as f64
}
fn beam_beat_size(ts: &TimeSignature) -> f64 {
if ts.numerator.is_multiple_of(3) && ts.numerator >= 6 && ts.denominator >= 8 {
3.0 * 4.0 / ts.denominator as f64
} else {
4.0 / ts.denominator as f64
}
}
pub fn compute_beams(notes: &[Note], time_sig: &TimeSignature) -> Vec<BeamState> {
let beat_size = beam_beat_size(time_sig);
let n = notes.len();
let mut result = vec![BeamState::None; n];
let is_beamable = |note: &Note| -> bool {
!note.is_rest
&& matches!(
note.duration,
Duration::Eighth
| Duration::Sixteenth
| Duration::ThirtySecond
| Duration::SixtyFourth
)
};
let mut starts = Vec::with_capacity(n);
let mut pos = 0.0f64;
for note in notes {
starts.push(pos);
pos += note.beats();
}
let group_id = |i: usize| -> i64 { (starts[i] / beat_size).floor() as i64 };
let mut i = 0;
while i < n {
if !is_beamable(¬es[i]) {
i += 1;
continue;
}
let g = group_id(i);
let mut j = i;
while j < n && is_beamable(¬es[j]) && group_id(j) == g {
j += 1;
}
let run = j - i;
if run == 1 {
result[i] = BeamState::None;
} else {
result[i] = BeamState::Begin;
result[i + 1..j - 1].fill(BeamState::Continue);
result[j - 1] = BeamState::End;
}
i = j;
}
result
}
fn note_content_eq(a: &Note, b: &Note) -> bool {
a.is_rest == b.is_rest
&& a.pitches == b.pitches
&& a.duration == b.duration
&& a.dot_count == b.dot_count
&& a.tie_start == b.tie_start
&& a.tie_end == b.tie_end
&& a.beam == b.beam
&& a.articulations == b.articulations
&& a.dynamic == b.dynamic
&& a.stem_up == b.stem_up
&& a.hairpin_start == b.hairpin_start
&& a.hairpin_end == b.hairpin_end
&& a.tuplet == b.tuplet
&& a.chord_symbol == b.chord_symbol
&& a.is_grace == b.is_grace
&& a.grace_slash == b.grace_slash
&& a.ottava_start == b.ottava_start
&& a.ottava_end == b.ottava_end
&& a.lyric == b.lyric
&& a.pedal_start == b.pedal_start
&& a.pedal_end == b.pedal_end
&& a.slur_start == b.slur_start
&& a.slur_end == b.slur_end
&& a.arpeggiate == b.arpeggiate
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::pitch::Step;
#[test]
fn default_score_has_one_part_four_measures() {
let score = Score::default();
assert_eq!(score.parts.len(), 1);
assert_eq!(score.parts[0].staves.len(), 1);
assert_eq!(score.parts[0].staves[0].measures.len(), 4);
}
#[test]
fn new_score_measure_count() {
let score = Score::new("Test", 120, 4, 4, 0, 8);
assert_eq!(score.measure_count(), 8);
}
#[test]
fn note_beats_quarter() {
let note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
assert!((note.beats() - 1.0).abs() < 1e-9);
}
#[test]
fn note_beats_dotted_quarter() {
let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
note.dot_count = 1;
assert!((note.beats() - 1.5).abs() < 1e-9);
}
#[test]
fn grace_note_beats_zero() {
let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Eighth);
note.is_grace = true;
assert_eq!(note.beats(), 0.0);
}
#[test]
fn measure_empty_4_4_fills_four_beats() {
let m = Measure::empty(4, 4);
let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
assert!((total - 4.0).abs() < 1e-9);
}
#[test]
fn measure_empty_3_4_fills_three_beats() {
let m = Measure::empty(3, 4);
let total: f64 = m.voices[0].iter().map(|n| n.beats()).sum();
assert!((total - 3.0).abs() < 1e-9);
}
#[test]
fn whole_filling_beats() {
assert_eq!(Duration::whole_filling_beats(4.0), Duration::Whole);
assert_eq!(Duration::whole_filling_beats(2.0), Duration::Half);
assert_eq!(Duration::whole_filling_beats(1.0), Duration::Quarter);
}
#[test]
fn statistics_default_score_all_rests() {
let score = Score::default();
let s = score.statistics();
assert_eq!(s.part_count, 1);
assert_eq!(s.measure_count, 4);
assert_eq!(s.note_count, 0);
assert!(s.rest_count > 0);
}
#[test]
fn statistics_duration_estimate() {
let score = Score::new("T", 120, 4, 4, 0, 1);
let s = score.statistics();
assert!((s.estimated_duration_secs - 2.0).abs() < 0.01);
}
#[test]
fn score_duration_secs_matches_statistics() {
use super::score_duration_secs;
let score = Score::new("T", 120, 4, 4, 0, 4);
let secs = score_duration_secs(&score);
assert!((secs - 8.0).abs() < 0.01, "expected ~8.0 s, got {secs}");
}
#[test]
fn score_duration_secs_zero_bpm_returns_zero() {
use super::score_duration_secs;
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.settings.tempo_bpm = 0;
assert_eq!(score_duration_secs(&score), 0.0);
}
#[test]
fn score_duration_secs_per_measure_tempo() {
use super::score_duration_secs;
let mut score = Score::new("T", 120, 4, 4, 0, 2);
score.parts[0].staves[0].measures[1].tempo = Some(60);
let secs = score_duration_secs(&score);
assert!((secs - 6.0).abs() < 0.01, "expected ~6.0 s, got {secs}");
}
#[test]
fn extract_part_returns_single_part_score() {
let mut score = Score::default();
let mut p2 = Part::new("Violin", "Vln.");
p2.staves.push(Staff::new(Clef::Treble));
score.parts.push(p2);
let ex = score.extract_part(0).unwrap();
assert_eq!(ex.parts.len(), 1);
assert_ne!(ex.id, score.id);
assert_eq!(ex.metadata.title, score.metadata.title);
}
#[test]
fn extract_part_out_of_range_is_none() {
let score = Score::default();
assert!(score.extract_part(99).is_none());
}
#[test]
fn transpose_zero_is_clone() {
let score = Score::new("T", 120, 4, 4, 0, 1);
let t = transpose(&score, 0);
assert_eq!(t.settings.key_signature.fifths, 0);
}
#[test]
fn transpose_c_major_up_2_to_d_major() {
let score = Score::new("T", 120, 4, 4, 0, 1);
assert_eq!(transpose(&score, 2).settings.key_signature.fifths, 2);
}
#[test]
fn transpose_d_major_up_5_to_g_major() {
let score = Score::new("T", 120, 4, 4, 2, 1);
assert_eq!(transpose(&score, 5).settings.key_signature.fifths, 1);
}
#[test]
fn transpose_c4_up_1_to_csharp4() {
let p = transpose_pitch(&Pitch::new(Step::C, 4), 1);
assert_eq!(p.to_midi(), 61);
assert_eq!(p.step, Step::C);
assert_eq!(p.alter, 1);
}
#[test]
fn transpose_c4_down_1_to_b3() {
let p = transpose_pitch(&Pitch::new(Step::C, 4), -1);
assert_eq!(p.to_midi(), 59);
assert_eq!(p.step, Step::B);
assert_eq!(p.alter, 0);
}
#[test]
fn transpose_up_octave_keeps_step() {
let p = transpose_pitch(&Pitch::new(Step::A, 4), 12);
assert_eq!(p.to_midi(), 81);
assert_eq!(p.step, Step::A);
assert_eq!(p.octave, 5);
}
#[test]
fn statistics_with_repeat_doubles_duration() {
let mut score = Score::new("T", 120, 4, 4, 0, 2);
score.parts[0].staves[0].measures[0].barline_left =
crate::model::notation::Barline::RepeatStart;
score.parts[0].staves[0].measures[1].barline_right =
crate::model::notation::Barline::RepeatEnd;
let s = score.statistics();
assert!((s.estimated_duration_secs - 8.0).abs() < 0.01);
}
#[test]
fn transpose_octave_boundary_b4_to_c5() {
let p = transpose_pitch(&Pitch::new(Step::B, 4), 1);
assert_eq!(p.to_midi(), 72);
assert_eq!(p.step, Step::C);
assert_eq!(p.octave, 5);
}
#[test]
fn transpose_clamp_at_midi_127() {
let p = transpose_pitch(&Pitch::new(Step::G, 9), 3);
assert_eq!(p.to_midi(), 127);
}
#[test]
fn merge_combines_parts() {
let mut a = Score::new("A", 120, 4, 4, 0, 2);
let b = Score::new("B", 120, 4, 4, 0, 2);
let mut p2 = Part::new("Violin", "Vln.");
p2.staves.push(Staff::new(Clef::Treble));
for i in 0..2usize {
let mut m = Measure::empty(4, 4);
m.number = i as u32 + 1;
p2.staves[0].measures.push(m);
}
a.parts.push(p2);
let merged = a.merge(&b);
assert_eq!(merged.parts.len(), 3);
}
#[test]
fn merge_pads_shorter_score() {
let a = Score::new("A", 120, 4, 4, 0, 4);
let b = Score::new("B", 120, 4, 4, 0, 2);
let merged = a.merge(&b);
assert_eq!(merged.parts[0].staves[0].measures.len(), 4);
assert_eq!(merged.parts[1].staves[0].measures.len(), 4);
}
#[test]
fn merge_uses_self_metadata() {
let mut a = Score::new("Title A", 120, 4, 4, 0, 2);
a.metadata.composer = "Composer A".to_string();
let b = Score::new("Title B", 120, 4, 4, 0, 2);
let merged = a.merge(&b);
assert_eq!(merged.metadata.title, "Title A");
assert_eq!(merged.metadata.composer, "Composer A");
}
#[test]
fn merge_new_id_differs_from_both() {
let a = Score::new("A", 120, 4, 4, 0, 2);
let b = Score::new("B", 120, 4, 4, 0, 2);
let merged = a.merge(&b);
assert_ne!(merged.id, a.id);
assert_ne!(merged.id, b.id);
}
#[test]
fn staff_default_transpose_is_zero() {
let s = Staff::new(Clef::Treble);
assert_eq!(s.transpose_semitones, 0);
}
#[test]
fn score_default_has_schema_version_1() {
let score = Score::default();
assert_eq!(score.schema_version, 1);
}
#[test]
fn score_new_has_schema_version_1() {
let score = Score::new("T", 120, 4, 4, 0, 4);
assert_eq!(score.schema_version, 1);
}
#[test]
fn score_without_schema_version_deserializes_to_zero() {
let json = r#"{"id":"abc","metadata":{"title":"T","composer":"","lyricist":"","copyright":"","work_number":"","movement_title":""},"settings":{"tempo_bpm":120,"time_signature":{"numerator":4,"denominator":4},"key_signature":{"fifths":0,"mode":"major"}},"parts":[]}"#;
let score: Score = serde_json::from_str(json).unwrap();
assert_eq!(score.schema_version, 0);
}
#[test]
fn score_template_solo_has_one_part_treble() {
let score = Score::template(ScoreTemplate::Solo);
assert_eq!(score.parts.len(), 1);
assert_eq!(score.parts[0].staves.len(), 1);
assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
assert_eq!(score.parts[0].midi_program, 0);
}
#[test]
fn score_template_piano_has_two_staves() {
let score = Score::template(ScoreTemplate::Piano);
assert_eq!(score.parts.len(), 1);
assert_eq!(score.parts[0].staves.len(), 2);
assert_eq!(score.parts[0].staves[0].clef, Clef::Treble);
assert_eq!(score.parts[0].staves[1].clef, Clef::Bass);
}
#[test]
fn score_template_string_quartet_has_four_parts() {
let score = Score::template(ScoreTemplate::StringQuartet);
assert_eq!(score.parts.len(), 4);
assert_eq!(score.parts[2].staves[0].clef, Clef::Alto); assert_eq!(score.parts[3].staves[0].clef, Clef::Bass); assert_eq!(score.parts[0].midi_program, 40);
assert_eq!(score.parts[3].midi_program, 42);
}
#[test]
fn score_template_string_orchestra_has_five_parts() {
let score = Score::template(ScoreTemplate::StringOrchestra);
assert_eq!(score.parts.len(), 5);
assert_eq!(score.parts[4].midi_program, 43); }
#[test]
fn score_template_brass_quintet_has_five_parts() {
let score = Score::template(ScoreTemplate::BrassQuintet);
assert_eq!(score.parts.len(), 5);
assert_eq!(score.parts[2].midi_program, 60); }
#[test]
fn score_template_default_measures_are_four() {
let score = Score::template(ScoreTemplate::StringQuartet);
for part in &score.parts {
for staff in &part.staves {
assert_eq!(staff.measures.len(), 4);
}
}
}
#[test]
fn measure_empty_has_no_breaks() {
let m = Measure::empty(4, 4);
assert!(!m.system_break);
assert!(!m.page_break);
}
#[test]
fn system_break_survives_json_roundtrip() {
let mut m = Measure::empty(4, 4);
m.system_break = true;
let json = serde_json::to_string(&m).unwrap();
let m2: Measure = serde_json::from_str(&json).unwrap();
assert!(m2.system_break);
assert!(!m2.page_break);
}
#[test]
fn diff_identical_scores_is_empty() {
let s = Score::new("T", 120, 4, 4, 0, 2);
assert!(diff(&s, &s).is_empty());
}
#[test]
fn score_patch_covers_measure_semantics_and_note_insert_index() {
let a = Score::new("T", 120, 4, 4, 0, 1);
let mut b = a.clone();
let measure = &mut b.parts[0].staves[0].measures[0];
measure.key_sig = Some(KeySignature {
fifths: -2,
mode: "major".to_string(),
});
measure.time_sig = Some(TimeSignature {
numerator: 3,
denominator: 4,
});
measure.barline_left = Barline::RepeatStart;
measure.barline_right = Barline::RepeatEnd;
measure.rehearsal = Some("A".to_string());
measure.volta = Some(VoltaBracket {
number: 1,
kind: "begin_end".to_string(),
});
measure.voices[0].insert(0, Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
let expected = b.parts[0].staves[0].measures[0].clone();
let patches = score_patch(&a, &b);
assert!(
patches
.iter()
.any(|p| matches!(p, ScorePatch::SetTimeSignature { .. }))
);
assert!(
patches
.iter()
.any(|p| matches!(p, ScorePatch::SetBarlines { .. }))
);
assert!(
patches
.iter()
.any(|p| matches!(p, ScorePatch::SetRehearsal { .. }))
);
assert!(
patches
.iter()
.any(|p| matches!(p, ScorePatch::SetVolta { .. }))
);
let result = apply_patch(&a, &patches).expect("patch application failed");
let result_measure = &result.parts[0].staves[0].measures[0];
assert_eq!(result_measure.key_sig, expected.key_sig);
assert_eq!(result_measure.time_sig, expected.time_sig);
assert_eq!(result_measure.barline_left, expected.barline_left);
assert_eq!(result_measure.barline_right, expected.barline_right);
assert_eq!(result_measure.rehearsal, expected.rehearsal);
assert_eq!(result_measure.volta, expected.volta);
assert_eq!(result_measure.voices[0].len(), expected.voices[0].len());
}
#[test]
fn score_patch_replaces_when_structure_or_uncovered_fields_change() {
let a = Score::new("T", 120, 4, 4, 0, 1);
let mut b = a.clone();
b.parts[0].name = "Piano".to_string();
b.parts[0].staves[0].measures[0].expression_text = Some("dolce".to_string());
let patches = score_patch(&a, &b);
assert!(matches!(
patches.as_slice(),
[ScorePatch::ReplaceScore { .. }]
));
let result = apply_patch(&a, &patches).expect("replacement failed");
assert_eq!(result.parts[0].name, "Piano");
assert_eq!(
result.parts[0].staves[0].measures[0].expression_text,
Some("dolce".to_string())
);
}
#[test]
fn diff_detects_tempo_change() {
let a = Score::new("T", 120, 4, 4, 0, 1);
let mut b = a.clone();
b.settings.tempo_bpm = 90;
let changes = diff(&a, &b);
assert_eq!(changes.len(), 1);
assert!(matches!(
changes[0],
ScoreChange::TempoChanged { old: 120, new: 90 }
));
}
#[test]
fn diff_detects_title_change() {
let a = Score::new("Old Title", 120, 4, 4, 0, 1);
let mut b = a.clone();
b.metadata.title = "New Title".to_string();
let changes = diff(&a, &b);
assert!(
changes.iter().any(
|c| matches!(c, ScoreChange::MetadataChanged { field, .. } if field == "title")
)
);
}
#[test]
fn diff_detects_note_modification() {
let mut a = Score::new("T", 120, 4, 4, 0, 1);
a.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Quarter)];
let mut b = a.clone();
b.parts[0].staves[0].measures[0].voices[0][0] =
Note::new(Pitch::new(Step::D, 4), Duration::Quarter);
let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::NoteModified { .. }))
);
}
#[test]
fn diff_detects_part_added() {
let a = Score::new("T", 120, 4, 4, 0, 1);
let mut b = a.clone();
let mut p = Part::new("Violin", "Vln.");
p.staves.push(Staff::new(Clef::Treble));
b.parts.push(p);
let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::PartAdded { part_index: 1 }))
);
}
#[test]
fn diff_detects_measure_tempo_change() {
let a = Score::new("T", 120, 4, 4, 0, 2);
let mut b = a.clone();
b.parts[0].staves[0].measures[1].tempo = Some(60);
let changes = diff(&a, &b);
assert!(changes.iter().any(|c| matches!(
c,
ScoreChange::MeasureTempoChanged {
measure: 1,
old: None,
new: Some(60),
..
}
)));
}
#[test]
fn diff_detects_barline_change() {
use crate::model::notation::Barline;
let a = Score::new("T", 120, 4, 4, 0, 2);
let mut b = a.clone();
b.parts[0].staves[0].measures[0].barline_left = Barline::RepeatStart;
let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::BarlineChanged { measure: 0, .. }))
);
}
#[test]
fn diff_detects_rehearsal_change() {
let a = Score::new("T", 120, 4, 4, 0, 2);
let mut b = a.clone();
b.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::RehearsalMarkChanged { measure: 0, .. }))
);
}
#[test]
fn diff_detects_volta_change() {
use super::VoltaBracket;
let a = Score::new("T", 120, 4, 4, 0, 2);
let mut b = a.clone();
b.parts[0].staves[0].measures[0].volta = Some(VoltaBracket {
number: 1,
kind: "begin_end".into(),
});
let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::VoltaChanged { measure: 0, .. }))
);
}
#[test]
fn diff_detects_key_signature_change() {
let a = Score::new("T", 120, 4, 4, 0, 1);
let mut b = a.clone();
b.settings.key_signature.fifths = 2; let changes = diff(&a, &b);
assert!(
changes
.iter()
.any(|c| matches!(c, ScoreChange::KeySignatureChanged { .. }))
);
}
#[test]
fn diff_same_key_signature_no_change() {
let a = Score::new("T", 120, 4, 4, 2, 1);
let changes = diff(&a, &a);
assert!(changes.is_empty());
}
#[test]
fn score_duration_secs_region_partial() {
use super::score_duration_secs_region;
let score = Score::new("T", 120, 4, 4, 0, 4);
let secs = score_duration_secs_region(&score, (1, 2));
assert!((secs - 4.0).abs() < 0.01, "expected ~4.0 s, got {secs}");
}
#[test]
fn score_duration_secs_region_single_measure() {
use super::score_duration_secs_region;
let score = Score::new("T", 120, 4, 4, 0, 4);
let secs = score_duration_secs_region(&score, (0, 0));
assert!((secs - 2.0).abs() < 0.01, "expected ~2.0 s, got {secs}");
}
#[test]
fn measure_beats_remaining_empty_voice_returns_full() {
use super::measure_beats_remaining;
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0].clear();
let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
assert!(
(rem - 4.0).abs() < 1e-9,
"expected 4.0 remaining, got {rem}"
);
}
#[test]
fn measure_beats_remaining_half_full_returns_half() {
use super::measure_beats_remaining;
use crate::model::pitch::Step;
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0] = vec![
Note::new(Pitch::new(Step::C, 4), Duration::Quarter),
Note::new(Pitch::new(Step::D, 4), Duration::Quarter),
];
let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
assert!(
(rem - 2.0).abs() < 1e-9,
"expected 2.0 remaining, got {rem}"
);
}
#[test]
fn measure_beats_remaining_full_voice_returns_zero() {
use super::measure_beats_remaining;
use crate::model::pitch::Step;
let mut score = Score::new("T", 120, 4, 4, 0, 1);
score.parts[0].staves[0].measures[0].voices[0] =
vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
assert!((rem).abs() < 1e-9, "expected 0.0 remaining, got {rem}");
}
#[test]
fn measure_beats_remaining_tuplet_accounting() {
use super::measure_beats_remaining;
use crate::model::notation::TupletInfo;
use crate::model::pitch::Step;
let mut score = Score::new("T", 120, 4, 4, 0, 1);
let tuplet = TupletInfo {
actual_notes: 3,
normal_notes: 2,
};
let mk = |step| {
let mut n = Note::new(Pitch::new(step, 4), Duration::Quarter);
n.tuplet = Some(tuplet.clone());
n
};
score.parts[0].staves[0].measures[0].voices[0] =
vec![mk(Step::C), mk(Step::D), mk(Step::E)];
let rem = measure_beats_remaining(&score, 0, 0, 0, 0).unwrap();
assert!(
(rem - 2.0).abs() < 1e-9,
"expected 2.0 remaining (triplets used 2.0), got {rem}"
);
}
#[test]
fn measure_beats_remaining_out_of_range_returns_err() {
use super::measure_beats_remaining;
let score = Score::new("T", 120, 4, 4, 0, 1);
assert!(measure_beats_remaining(&score, 99, 0, 0, 0).is_err());
assert!(measure_beats_remaining(&score, 0, 99, 0, 0).is_err());
assert!(measure_beats_remaining(&score, 0, 0, 99, 0).is_err());
assert!(measure_beats_remaining(&score, 0, 0, 0, 4).is_err());
}
#[test]
fn note_content_eq_ignores_id() {
use crate::model::pitch::Step;
let mut a = Note::new(Pitch::new(Step::C, 4), Duration::Quarter);
let mut b = a.clone();
b.id = "different-id".to_string();
assert!(note_content_eq(&a, &b));
b.pitches[0] = Pitch::new(Step::D, 4);
assert!(!note_content_eq(&a, &b));
let mut c = a.clone();
a.stem_up = Some(true);
c.stem_up = Some(false);
assert!(!note_content_eq(&a, &c));
}
#[test]
fn suggested_stem_up_below_middle() {
use crate::model::notation::Clef;
let pitches = vec![Pitch::new(Step::C, 4)];
assert!(suggested_stem_up(&pitches, &Clef::Treble));
}
#[test]
fn suggested_stem_up_above_middle() {
use crate::model::notation::Clef;
let pitches = vec![Pitch::new(Step::G, 5)];
assert!(!suggested_stem_up(&pitches, &Clef::Treble));
}
#[test]
fn suggested_stem_up_at_middle_line() {
use crate::model::notation::Clef;
let pitches = vec![Pitch::new(Step::B, 4)];
assert!(!suggested_stem_up(&pitches, &Clef::Treble));
}
#[test]
fn suggested_stem_up_chord() {
use crate::model::notation::Clef;
let pitches = vec![Pitch::new(Step::C, 4), Pitch::new(Step::G, 4)];
assert!(suggested_stem_up(&pitches, &Clef::Treble));
}
#[test]
fn suggested_stem_up_bass_clef() {
use crate::model::notation::Clef;
let pitches = vec![Pitch::new(Step::D, 3)];
assert!(!suggested_stem_up(&pitches, &Clef::Bass));
let pitches2 = vec![Pitch::new(Step::C, 3)];
assert!(suggested_stem_up(&pitches2, &Clef::Bass));
}
#[test]
fn suggested_stem_up_empty_pitches() {
use crate::model::notation::Clef;
assert!(suggested_stem_up(&[], &Clef::Treble));
}
fn eighth(pitch: Pitch) -> Note {
Note::new(pitch, Duration::Eighth)
}
fn quarter(pitch: Pitch) -> Note {
Note::new(pitch, Duration::Quarter)
}
fn rest_eighth() -> Note {
Note::rest(Duration::Eighth)
}
#[test]
fn compute_beams_4_4_four_eighths() {
use crate::model::notation::{Clef, TimeSignature};
let _ = Clef::Treble; let ts = TimeSignature {
numerator: 4,
denominator: 4,
};
let c4 = Pitch::new(Step::C, 4);
let notes = vec![
eighth(c4.clone()),
eighth(c4.clone()),
eighth(c4.clone()),
eighth(c4.clone()),
];
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::Begin);
assert_eq!(beams[1], BeamState::End);
assert_eq!(beams[2], BeamState::Begin);
assert_eq!(beams[3], BeamState::End);
}
#[test]
fn compute_beams_4_4_all_eighth_one_group() {
use crate::model::notation::TimeSignature;
let ts = TimeSignature {
numerator: 4,
denominator: 4,
};
let c4 = Pitch::new(Step::C, 4);
let notes = vec![eighth(c4.clone()), eighth(c4.clone())];
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::Begin);
assert_eq!(beams[1], BeamState::End);
}
#[test]
fn compute_beams_quarter_not_beamed() {
use crate::model::notation::TimeSignature;
let ts = TimeSignature {
numerator: 4,
denominator: 4,
};
let c4 = Pitch::new(Step::C, 4);
let notes = vec![quarter(c4.clone()), quarter(c4.clone())];
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::None);
assert_eq!(beams[1], BeamState::None);
}
#[test]
fn compute_beams_rest_breaks_beam() {
use crate::model::notation::TimeSignature;
let ts = TimeSignature {
numerator: 4,
denominator: 4,
};
let c4 = Pitch::new(Step::C, 4);
let notes = vec![eighth(c4.clone()), rest_eighth(), eighth(c4.clone())];
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::None);
assert_eq!(beams[1], BeamState::None);
assert_eq!(beams[2], BeamState::None);
}
#[test]
fn compute_beams_6_8_compound() {
use crate::model::notation::TimeSignature;
let ts = TimeSignature {
numerator: 6,
denominator: 8,
};
let c4 = Pitch::new(Step::C, 4);
let notes: Vec<Note> = (0..6).map(|_| eighth(c4.clone())).collect();
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::Begin);
assert_eq!(beams[1], BeamState::Continue);
assert_eq!(beams[2], BeamState::End);
assert_eq!(beams[3], BeamState::Begin);
assert_eq!(beams[4], BeamState::Continue);
assert_eq!(beams[5], BeamState::End);
}
#[test]
fn compute_beams_single_eighth() {
use crate::model::notation::TimeSignature;
let ts = TimeSignature {
numerator: 4,
denominator: 4,
};
let c4 = Pitch::new(Step::C, 4);
let notes = vec![eighth(c4.clone())];
let beams = compute_beams(¬es, &ts);
assert_eq!(beams[0], BeamState::None);
}
}