mod convert;
use std::collections::BTreeSet;
use std::fmt;
use thiserror::Error;
use crate::{
AtomRef, Melody, MusicError, MusicObject, Note, PianoRoll, Progression, Time, TimedAtom,
};
use crate::{Chord, Counterpoint};
pub use convert::convert_score;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId(String);
impl ObjectId {
pub fn new(value: impl Into<String>) -> Result<Self, ConversionError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ConversionError::InvalidIdentity(
"object identity cannot be empty".to_owned(),
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn derived(kind: &str, path: impl fmt::Display) -> Self {
Self(format!("{kind}/{path}"))
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StaffNote {
pub voice_id: ObjectId,
pub note_id: ObjectId,
pub event_id: ObjectId,
pub onset: Time,
pub note: Note,
}
impl StaffNote {
pub fn end(&self) -> Time {
self.onset + self.note.duration
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StaffVoice {
pub id: ObjectId,
pub name: String,
pub duration: Time,
pub notes: Vec<StaffNote>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Staff {
pub voices: Vec<StaffVoice>,
}
impl Staff {
pub fn new(mut voices: Vec<StaffVoice>) -> Result<Self, ConversionError> {
let zero = Time::from_integer(0);
let mut identities = BTreeSet::new();
for voice in &mut voices {
if voice.duration < zero {
return Err(ConversionError::Music(MusicError::NegativeDuration));
}
if !identities.insert(voice.id.clone()) {
return Err(ConversionError::DuplicateIdentity(voice.id.clone()));
}
for note in &voice.notes {
if note.voice_id != voice.id {
return Err(ConversionError::InvalidIdentity(format!(
"event {} names voice {} but belongs to {}",
note.event_id, note.voice_id, voice.id
)));
}
if note.onset < zero {
return Err(ConversionError::Music(MusicError::NegativeOnset));
}
if note.note.duration < zero {
return Err(ConversionError::Music(MusicError::NegativeDuration));
}
if note.end() > voice.duration {
return Err(ConversionError::InvalidIdentity(format!(
"event {} ends after voice {}",
note.event_id, voice.id
)));
}
for id in [¬e.note_id, ¬e.event_id] {
if !identities.insert(id.clone()) {
return Err(ConversionError::DuplicateIdentity(id.clone()));
}
}
}
voice.notes.sort_by(staff_note_order);
}
voices.sort_by(|left, right| left.id.cmp(&right.id));
Ok(Self { voices })
}
pub fn duration(&self) -> Time {
self.voices
.iter()
.map(|voice| voice.duration)
.max()
.unwrap_or_else(|| Time::from_integer(0))
}
pub fn notes(&self) -> impl Iterator<Item = &StaffNote> {
self.voices.iter().flat_map(|voice| voice.notes.iter())
}
pub fn object_ids(&self) -> Vec<ObjectId> {
let mut ids = self
.voices
.iter()
.flat_map(|voice| {
std::iter::once(voice.id.clone()).chain(
voice
.notes
.iter()
.flat_map(|note| [note.note_id.clone(), note.event_id.clone()]),
)
})
.collect::<Vec<_>>();
ids.sort();
ids.dedup();
ids
}
}
impl MusicObject for Staff {
fn kind(&self) -> &'static str {
"Staff"
}
fn duration(&self) -> Time {
Staff::duration(self)
}
fn voices<'a>(&'a self, offset: Time, out: &mut Vec<TimedAtom<'a>>) {
for item in self.notes() {
out.push(TimedAtom {
onset: offset + item.onset,
atom: AtomRef::Note(item.note.clone()),
});
}
}
fn clone_box(&self) -> Box<dyn MusicObject> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MusicSnapshot {
pub at: Time,
pub sounding: Vec<StaffNote>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScoreVoice {
pub id: ObjectId,
pub name: String,
pub duration: Time,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SnapshotStream {
pub duration: Time,
pub voices: Vec<ScoreVoice>,
pub snapshots: Vec<MusicSnapshot>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MusicChange {
NoteStarted(StaffNote),
NoteEnded {
at: Time,
voice_id: ObjectId,
note_id: ObjectId,
event_id: ObjectId,
},
}
impl MusicChange {
pub fn at(&self) -> Time {
match self {
Self::NoteStarted(note) => note.onset,
Self::NoteEnded { at, .. } => *at,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MusicChangeStream {
pub duration: Time,
pub voices: Vec<ScoreVoice>,
pub changes: Vec<MusicChange>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScoreForm {
Melody(Melody),
Chord(Chord),
Staff(Staff),
Counterpoint(Counterpoint),
PianoRoll(PianoRoll),
Snapshot(SnapshotStream),
ChangeStream(MusicChangeStream),
Progression(Progression),
}
impl ScoreForm {
pub fn kind(&self) -> ScoreFormKind {
match self {
Self::Melody(_) => ScoreFormKind::Melody,
Self::Chord(_) => ScoreFormKind::Chord,
Self::Staff(_) => ScoreFormKind::Staff,
Self::Counterpoint(_) => ScoreFormKind::Counterpoint,
Self::PianoRoll(_) => ScoreFormKind::PianoRoll,
Self::Snapshot(_) => ScoreFormKind::Snapshot,
Self::ChangeStream(_) => ScoreFormKind::ChangeStream,
Self::Progression(_) => ScoreFormKind::Progression,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ScoreFormKind {
Melody,
Chord,
Staff,
Counterpoint,
PianoRoll,
Snapshot,
ChangeStream,
Progression,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AmbiguousConversionPolicy {
Reject,
KeepHighest,
KeepLowest,
KeepFirst,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConversionLossKind {
ExplicitRest,
HarmonicLabel,
KeyAnnotation,
PianoRollGrid,
NonNoteCell,
DiscardedVoice,
VoiceBoundary,
VoiceMetadata,
IdentityMetadata,
ZeroDurationSnapshot,
Silence,
InconsistentChange,
SynthesizedLabel,
SourceStructure,
RelativeAnchor,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ConversionLoss {
pub kind: ConversionLossKind,
pub object: Option<ObjectId>,
pub detail: String,
}
impl ConversionLoss {
pub fn new(
kind: ConversionLossKind,
object: Option<ObjectId>,
detail: impl Into<String>,
) -> Self {
Self {
kind,
object,
detail: detail.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MusicConversion<T> {
pub value: T,
pub preserved: Vec<ObjectId>,
pub losses: Vec<ConversionLoss>,
}
impl<T> MusicConversion<T> {
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> MusicConversion<U> {
MusicConversion {
value: f(self.value),
preserved: self.preserved,
losses: self.losses,
}
}
pub fn is_lossless(&self) -> bool {
self.losses.is_empty()
}
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ConversionError {
#[error(transparent)]
Music(#[from] MusicError),
#[error("ambiguous {from:?} to {to:?} conversion: {detail}")]
Ambiguous {
from: ScoreFormKind,
to: ScoreFormKind,
detail: String,
},
#[error("invalid score identity: {0}")]
InvalidIdentity(String),
#[error("duplicate score identity {0}")]
DuplicateIdentity(ObjectId),
}
pub(crate) fn staff_note_order(left: &StaffNote, right: &StaffNote) -> std::cmp::Ordering {
left.onset
.cmp(&right.onset)
.then_with(|| left.note.pitch.cmp(&right.note.pitch))
.then_with(|| left.event_id.cmp(&right.event_id))
}