use sim_lib_discrete_graph::{
Assignment, AssignmentOperation, AssignmentPolicy, CostMatrix, GraphError, verify_assignment,
};
pub use sim_lib_discrete_graph::{AssignmentCertificate, VoiceCrossingPolicy};
use sim_lib_music_core::{ObjectId, Staff, Time};
use sim_lib_pitch_core::Pitch;
use crate::TransformError;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExactVoiceNote {
pub voice_id: ObjectId,
pub note_id: ObjectId,
pub event_id: ObjectId,
pub pitch: Pitch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExactVoicing {
pub at: Time,
pub notes: Vec<ExactVoiceNote>,
}
impl ExactVoicing {
pub fn from_staff(staff: &Staff, at: Time) -> Result<Self, TransformError> {
if at < Time::from_integer(0) || at > staff.duration() {
return Err(TransformError::InvalidTransformOutput {
transform: "exact-voicing",
reason: "voicing boundary lies outside the staff",
});
}
let mut notes = staff
.notes()
.filter(|note| note.onset <= at && at < note.end())
.map(|note| ExactVoiceNote {
voice_id: note.voice_id.clone(),
note_id: note.note_id.clone(),
event_id: note.event_id.clone(),
pitch: note.note.pitch,
})
.collect::<Vec<_>>();
notes.sort_by(|left, right| {
left.pitch
.cmp(&right.pitch)
.then_with(|| left.voice_id.cmp(&right.voice_id))
.then_with(|| left.event_id.cmp(&right.event_id))
});
Ok(Self { at, notes })
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum VoiceLeadingMetric {
AbsoluteSemitones,
SquaredSemitones,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceLeadingPolicy {
pub entrance_cost: i64,
pub departure_cost: i64,
pub doubling_cost: Option<i64>,
pub voice_crossing: VoiceCrossingPolicy,
pub metric: VoiceLeadingMetric,
}
impl VoiceLeadingPolicy {
pub fn new(entrance_cost: i64, departure_cost: i64) -> Self {
Self {
entrance_cost,
departure_cost,
doubling_cost: None,
voice_crossing: VoiceCrossingPolicy::Allow,
metric: VoiceLeadingMetric::SquaredSemitones,
}
}
pub fn with_doubling(mut self, cost: i64) -> Self {
self.doubling_cost = Some(cost);
self
}
pub fn with_voice_crossing(mut self, policy: VoiceCrossingPolicy) -> Self {
self.voice_crossing = policy;
self
}
pub fn with_metric(mut self, metric: VoiceLeadingMetric) -> Self {
self.metric = metric;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VoiceLeadingMotion {
Move {
source: ExactVoiceNote,
target: ExactVoiceNote,
semitones: i64,
cost: i64,
},
Double {
source: ExactVoiceNote,
target: ExactVoiceNote,
semitones: i64,
cost: i64,
},
Enter {
target: ExactVoiceNote,
cost: i64,
},
Leave {
source: ExactVoiceNote,
cost: i64,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceLeading {
pub source: ExactVoicing,
pub target: ExactVoicing,
pub assignment: Assignment<i64>,
pub motions: Vec<VoiceLeadingMotion>,
}
pub fn voice_leading(
source: &ExactVoicing,
target: &ExactVoicing,
policy: &VoiceLeadingPolicy,
) -> Result<VoiceLeading, TransformError> {
let costs = voice_costs(source, target, policy.metric)?;
let assignment_policy = assignment_policy(source, target, policy);
let assignment =
sim_lib_discrete_graph::min_cost_assignment(&costs, assignment_policy.clone())?;
let motions = resolve_motions(source, target, &assignment);
let leading = VoiceLeading {
source: source.clone(),
target: target.clone(),
assignment,
motions,
};
verify_voice_leading(&leading, policy)?;
Ok(leading)
}
pub fn verify_voice_leading(
leading: &VoiceLeading,
policy: &VoiceLeadingPolicy,
) -> Result<(), TransformError> {
let costs = voice_costs(&leading.source, &leading.target, policy.metric)?;
let assignment_policy = assignment_policy(&leading.source, &leading.target, policy);
verify_assignment(&costs, &assignment_policy, &leading.assignment)?;
if leading.motions != resolve_motions(&leading.source, &leading.target, &leading.assignment) {
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading",
reason: "identity-resolved motions disagree with the assignment",
});
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceLeadingPathCertificate {
pub leg_costs: Vec<i64>,
pub total_cost: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoiceLeadingPath {
pub voicings: Vec<ExactVoicing>,
pub legs: Vec<VoiceLeading>,
pub certificate: VoiceLeadingPathCertificate,
}
pub fn voice_leading_path(
voicings: &[ExactVoicing],
policy: &VoiceLeadingPolicy,
) -> Result<VoiceLeadingPath, TransformError> {
if voicings.windows(2).any(|pair| pair[0].at > pair[1].at) {
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading-path",
reason: "voicings must be in non-decreasing exact time order",
});
}
let mut legs = Vec::with_capacity(voicings.len().saturating_sub(1));
let mut leg_costs = Vec::with_capacity(voicings.len().saturating_sub(1));
let mut total_cost = 0_i64;
for pair in voicings.windows(2) {
let leg = voice_leading(&pair[0], &pair[1], policy)?;
total_cost = total_cost
.checked_add(leg.assignment.total_cost)
.ok_or_else(|| GraphError::WeightOverflow("voice-leading path total".to_owned()))?;
leg_costs.push(leg.assignment.total_cost);
legs.push(leg);
}
let path = VoiceLeadingPath {
voicings: voicings.to_vec(),
legs,
certificate: VoiceLeadingPathCertificate {
leg_costs,
total_cost,
},
};
verify_voice_leading_path(&path, policy)?;
Ok(path)
}
pub fn verify_voice_leading_path(
path: &VoiceLeadingPath,
policy: &VoiceLeadingPolicy,
) -> Result<(), TransformError> {
if path.legs.len() != path.voicings.len().saturating_sub(1)
|| path.certificate.leg_costs.len() != path.legs.len()
{
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading-path",
reason: "path dimensions do not agree",
});
}
let mut total = 0_i64;
for (index, leg) in path.legs.iter().enumerate() {
if leg.source != path.voicings[index] || leg.target != path.voicings[index + 1] {
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading-path",
reason: "path leg endpoints do not join",
});
}
verify_voice_leading(leg, policy)?;
if path.certificate.leg_costs[index] != leg.assignment.total_cost {
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading-path",
reason: "path leg cost disagrees with its assignment",
});
}
total = total
.checked_add(leg.assignment.total_cost)
.ok_or_else(|| GraphError::WeightOverflow("voice-leading path total".to_owned()))?;
}
if total != path.certificate.total_cost {
return Err(TransformError::InvalidTransformOutput {
transform: "voice-leading-path",
reason: "path total disagrees with its certified legs",
});
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoicingChange {
pub source: usize,
pub target: usize,
pub leading: VoiceLeading,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VoicingChangePalette {
pub voicings: Vec<ExactVoicing>,
pub changes: Vec<VoicingChange>,
}
impl VoicingChangePalette {
pub fn outgoing(&self, source: usize) -> impl Iterator<Item = &VoicingChange> {
self.changes
.iter()
.filter(move |change| change.source == source)
}
}
pub fn voicing_change_palette(
voicings: &[ExactVoicing],
policy: &VoiceLeadingPolicy,
) -> Result<VoicingChangePalette, TransformError> {
let mut changes = Vec::new();
for source in 0..voicings.len() {
for target in 0..voicings.len() {
if source == target {
continue;
}
changes.push(VoicingChange {
source,
target,
leading: voice_leading(&voicings[source], &voicings[target], policy)?,
});
}
}
Ok(VoicingChangePalette {
voicings: voicings.to_vec(),
changes,
})
}
fn voice_costs(
source: &ExactVoicing,
target: &ExactVoicing,
metric: VoiceLeadingMetric,
) -> Result<CostMatrix<i64>, TransformError> {
let mut values = Vec::with_capacity(source.notes.len() * target.notes.len());
for from in &source.notes {
for to in &target.notes {
let distance = i64::from(to.pitch.semitone()) - i64::from(from.pitch.semitone());
let absolute = distance.abs();
values.push(match metric {
VoiceLeadingMetric::AbsoluteSemitones => absolute,
VoiceLeadingMetric::SquaredSemitones => {
absolute.checked_mul(absolute).ok_or_else(|| {
GraphError::WeightOverflow("squared voice-leading distance".to_owned())
})?
}
});
}
}
Ok(CostMatrix::new(
source.notes.len(),
target.notes.len(),
values,
)?)
}
fn assignment_policy(
source: &ExactVoicing,
target: &ExactVoicing,
policy: &VoiceLeadingPolicy,
) -> AssignmentPolicy<i64> {
let assignment = AssignmentPolicy::new(
vec![policy.entrance_cost; target.notes.len()],
vec![policy.departure_cost; source.notes.len()],
)
.with_voice_crossing(policy.voice_crossing);
match policy.doubling_cost {
Some(cost) => assignment.with_doubling(vec![cost; source.notes.len()]),
None => assignment,
}
}
fn resolve_motions(
source: &ExactVoicing,
target: &ExactVoicing,
assignment: &Assignment<i64>,
) -> Vec<VoiceLeadingMotion> {
assignment
.operations
.iter()
.map(|operation| match operation {
AssignmentOperation::Match {
source: from,
target: to,
cost,
} => VoiceLeadingMotion::Move {
source: source.notes[*from].clone(),
target: target.notes[*to].clone(),
semitones: i64::from(target.notes[*to].pitch.semitone())
- i64::from(source.notes[*from].pitch.semitone()),
cost: *cost,
},
AssignmentOperation::Double {
source: from,
target: to,
cost,
} => VoiceLeadingMotion::Double {
source: source.notes[*from].clone(),
target: target.notes[*to].clone(),
semitones: i64::from(target.notes[*to].pitch.semitone())
- i64::from(source.notes[*from].pitch.semitone()),
cost: *cost,
},
AssignmentOperation::Insert { target: to, cost } => VoiceLeadingMotion::Enter {
target: target.notes[*to].clone(),
cost: *cost,
},
AssignmentOperation::Delete { source: from, cost } => VoiceLeadingMotion::Leave {
source: source.notes[*from].clone(),
cost: *cost,
},
})
.collect()
}