1use sim_lib_discrete_graph::{
4 Assignment, AssignmentOperation, AssignmentPolicy, CostMatrix, GraphError, verify_assignment,
5};
6pub use sim_lib_discrete_graph::{AssignmentCertificate, VoiceCrossingPolicy};
7use sim_lib_music_core::{ObjectId, Staff, Time};
8use sim_lib_pitch_core::Pitch;
9
10use crate::TransformError;
11
12#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ExactVoiceNote {
15 pub voice_id: ObjectId,
17 pub note_id: ObjectId,
19 pub event_id: ObjectId,
21 pub pitch: Pitch,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct ExactVoicing {
28 pub at: Time,
30 pub notes: Vec<ExactVoiceNote>,
32}
33
34impl ExactVoicing {
35 pub fn from_staff(staff: &Staff, at: Time) -> Result<Self, TransformError> {
40 if at < Time::from_integer(0) || at > staff.duration() {
41 return Err(TransformError::InvalidTransformOutput {
42 transform: "exact-voicing",
43 reason: "voicing boundary lies outside the staff",
44 });
45 }
46 let mut notes = staff
47 .notes()
48 .filter(|note| note.onset <= at && at < note.end())
49 .map(|note| ExactVoiceNote {
50 voice_id: note.voice_id.clone(),
51 note_id: note.note_id.clone(),
52 event_id: note.event_id.clone(),
53 pitch: note.note.pitch,
54 })
55 .collect::<Vec<_>>();
56 notes.sort_by(|left, right| {
57 left.pitch
58 .cmp(&right.pitch)
59 .then_with(|| left.voice_id.cmp(&right.voice_id))
60 .then_with(|| left.event_id.cmp(&right.event_id))
61 });
62 Ok(Self { at, notes })
63 }
64}
65
66#[derive(Copy, Clone, Debug, PartialEq, Eq)]
68pub enum VoiceLeadingMetric {
69 AbsoluteSemitones,
71 SquaredSemitones,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct VoiceLeadingPolicy {
79 pub entrance_cost: i64,
81 pub departure_cost: i64,
83 pub doubling_cost: Option<i64>,
85 pub voice_crossing: VoiceCrossingPolicy,
87 pub metric: VoiceLeadingMetric,
89}
90
91impl VoiceLeadingPolicy {
92 pub fn new(entrance_cost: i64, departure_cost: i64) -> Self {
94 Self {
95 entrance_cost,
96 departure_cost,
97 doubling_cost: None,
98 voice_crossing: VoiceCrossingPolicy::Allow,
99 metric: VoiceLeadingMetric::SquaredSemitones,
100 }
101 }
102
103 pub fn with_doubling(mut self, cost: i64) -> Self {
105 self.doubling_cost = Some(cost);
106 self
107 }
108
109 pub fn with_voice_crossing(mut self, policy: VoiceCrossingPolicy) -> Self {
111 self.voice_crossing = policy;
112 self
113 }
114
115 pub fn with_metric(mut self, metric: VoiceLeadingMetric) -> Self {
117 self.metric = metric;
118 self
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
124pub enum VoiceLeadingMotion {
125 Move {
127 source: ExactVoiceNote,
129 target: ExactVoiceNote,
131 semitones: i64,
133 cost: i64,
135 },
136 Double {
138 source: ExactVoiceNote,
140 target: ExactVoiceNote,
142 semitones: i64,
144 cost: i64,
146 },
147 Enter {
149 target: ExactVoiceNote,
151 cost: i64,
153 },
154 Leave {
156 source: ExactVoiceNote,
158 cost: i64,
160 },
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct VoiceLeading {
166 pub source: ExactVoicing,
168 pub target: ExactVoicing,
170 pub assignment: Assignment<i64>,
172 pub motions: Vec<VoiceLeadingMotion>,
174}
175
176pub fn voice_leading(
179 source: &ExactVoicing,
180 target: &ExactVoicing,
181 policy: &VoiceLeadingPolicy,
182) -> Result<VoiceLeading, TransformError> {
183 let costs = voice_costs(source, target, policy.metric)?;
184 let assignment_policy = assignment_policy(source, target, policy);
185 let assignment =
186 sim_lib_discrete_graph::min_cost_assignment(&costs, assignment_policy.clone())?;
187 let motions = resolve_motions(source, target, &assignment);
188 let leading = VoiceLeading {
189 source: source.clone(),
190 target: target.clone(),
191 assignment,
192 motions,
193 };
194 verify_voice_leading(&leading, policy)?;
195 Ok(leading)
196}
197
198pub fn verify_voice_leading(
201 leading: &VoiceLeading,
202 policy: &VoiceLeadingPolicy,
203) -> Result<(), TransformError> {
204 let costs = voice_costs(&leading.source, &leading.target, policy.metric)?;
205 let assignment_policy = assignment_policy(&leading.source, &leading.target, policy);
206 verify_assignment(&costs, &assignment_policy, &leading.assignment)?;
207 if leading.motions != resolve_motions(&leading.source, &leading.target, &leading.assignment) {
208 return Err(TransformError::InvalidTransformOutput {
209 transform: "voice-leading",
210 reason: "identity-resolved motions disagree with the assignment",
211 });
212 }
213 Ok(())
214}
215
216#[derive(Clone, Debug, PartialEq, Eq)]
218pub struct VoiceLeadingPathCertificate {
219 pub leg_costs: Vec<i64>,
221 pub total_cost: i64,
223}
224
225#[derive(Clone, Debug, PartialEq, Eq)]
227pub struct VoiceLeadingPath {
228 pub voicings: Vec<ExactVoicing>,
230 pub legs: Vec<VoiceLeading>,
232 pub certificate: VoiceLeadingPathCertificate,
234}
235
236pub fn voice_leading_path(
238 voicings: &[ExactVoicing],
239 policy: &VoiceLeadingPolicy,
240) -> Result<VoiceLeadingPath, TransformError> {
241 if voicings.windows(2).any(|pair| pair[0].at > pair[1].at) {
242 return Err(TransformError::InvalidTransformOutput {
243 transform: "voice-leading-path",
244 reason: "voicings must be in non-decreasing exact time order",
245 });
246 }
247 let mut legs = Vec::with_capacity(voicings.len().saturating_sub(1));
248 let mut leg_costs = Vec::with_capacity(voicings.len().saturating_sub(1));
249 let mut total_cost = 0_i64;
250 for pair in voicings.windows(2) {
251 let leg = voice_leading(&pair[0], &pair[1], policy)?;
252 total_cost = total_cost
253 .checked_add(leg.assignment.total_cost)
254 .ok_or_else(|| GraphError::WeightOverflow("voice-leading path total".to_owned()))?;
255 leg_costs.push(leg.assignment.total_cost);
256 legs.push(leg);
257 }
258 let path = VoiceLeadingPath {
259 voicings: voicings.to_vec(),
260 legs,
261 certificate: VoiceLeadingPathCertificate {
262 leg_costs,
263 total_cost,
264 },
265 };
266 verify_voice_leading_path(&path, policy)?;
267 Ok(path)
268}
269
270pub fn verify_voice_leading_path(
272 path: &VoiceLeadingPath,
273 policy: &VoiceLeadingPolicy,
274) -> Result<(), TransformError> {
275 if path.legs.len() != path.voicings.len().saturating_sub(1)
276 || path.certificate.leg_costs.len() != path.legs.len()
277 {
278 return Err(TransformError::InvalidTransformOutput {
279 transform: "voice-leading-path",
280 reason: "path dimensions do not agree",
281 });
282 }
283 let mut total = 0_i64;
284 for (index, leg) in path.legs.iter().enumerate() {
285 if leg.source != path.voicings[index] || leg.target != path.voicings[index + 1] {
286 return Err(TransformError::InvalidTransformOutput {
287 transform: "voice-leading-path",
288 reason: "path leg endpoints do not join",
289 });
290 }
291 verify_voice_leading(leg, policy)?;
292 if path.certificate.leg_costs[index] != leg.assignment.total_cost {
293 return Err(TransformError::InvalidTransformOutput {
294 transform: "voice-leading-path",
295 reason: "path leg cost disagrees with its assignment",
296 });
297 }
298 total = total
299 .checked_add(leg.assignment.total_cost)
300 .ok_or_else(|| GraphError::WeightOverflow("voice-leading path total".to_owned()))?;
301 }
302 if total != path.certificate.total_cost {
303 return Err(TransformError::InvalidTransformOutput {
304 transform: "voice-leading-path",
305 reason: "path total disagrees with its certified legs",
306 });
307 }
308 Ok(())
309}
310
311#[derive(Clone, Debug, PartialEq, Eq)]
313pub struct VoicingChange {
314 pub source: usize,
316 pub target: usize,
318 pub leading: VoiceLeading,
320}
321
322#[derive(Clone, Debug, PartialEq, Eq)]
324pub struct VoicingChangePalette {
325 pub voicings: Vec<ExactVoicing>,
327 pub changes: Vec<VoicingChange>,
329}
330
331impl VoicingChangePalette {
332 pub fn outgoing(&self, source: usize) -> impl Iterator<Item = &VoicingChange> {
335 self.changes
336 .iter()
337 .filter(move |change| change.source == source)
338 }
339}
340
341pub fn voicing_change_palette(
344 voicings: &[ExactVoicing],
345 policy: &VoiceLeadingPolicy,
346) -> Result<VoicingChangePalette, TransformError> {
347 let mut changes = Vec::new();
348 for source in 0..voicings.len() {
349 for target in 0..voicings.len() {
350 if source == target {
351 continue;
352 }
353 changes.push(VoicingChange {
354 source,
355 target,
356 leading: voice_leading(&voicings[source], &voicings[target], policy)?,
357 });
358 }
359 }
360 Ok(VoicingChangePalette {
361 voicings: voicings.to_vec(),
362 changes,
363 })
364}
365
366fn voice_costs(
367 source: &ExactVoicing,
368 target: &ExactVoicing,
369 metric: VoiceLeadingMetric,
370) -> Result<CostMatrix<i64>, TransformError> {
371 let mut values = Vec::with_capacity(source.notes.len() * target.notes.len());
372 for from in &source.notes {
373 for to in &target.notes {
374 let distance = i64::from(to.pitch.semitone()) - i64::from(from.pitch.semitone());
375 let absolute = distance.abs();
376 values.push(match metric {
377 VoiceLeadingMetric::AbsoluteSemitones => absolute,
378 VoiceLeadingMetric::SquaredSemitones => {
379 absolute.checked_mul(absolute).ok_or_else(|| {
380 GraphError::WeightOverflow("squared voice-leading distance".to_owned())
381 })?
382 }
383 });
384 }
385 }
386 Ok(CostMatrix::new(
387 source.notes.len(),
388 target.notes.len(),
389 values,
390 )?)
391}
392
393fn assignment_policy(
394 source: &ExactVoicing,
395 target: &ExactVoicing,
396 policy: &VoiceLeadingPolicy,
397) -> AssignmentPolicy<i64> {
398 let assignment = AssignmentPolicy::new(
399 vec![policy.entrance_cost; target.notes.len()],
400 vec![policy.departure_cost; source.notes.len()],
401 )
402 .with_voice_crossing(policy.voice_crossing);
403 match policy.doubling_cost {
404 Some(cost) => assignment.with_doubling(vec![cost; source.notes.len()]),
405 None => assignment,
406 }
407}
408
409fn resolve_motions(
410 source: &ExactVoicing,
411 target: &ExactVoicing,
412 assignment: &Assignment<i64>,
413) -> Vec<VoiceLeadingMotion> {
414 assignment
415 .operations
416 .iter()
417 .map(|operation| match operation {
418 AssignmentOperation::Match {
419 source: from,
420 target: to,
421 cost,
422 } => VoiceLeadingMotion::Move {
423 source: source.notes[*from].clone(),
424 target: target.notes[*to].clone(),
425 semitones: i64::from(target.notes[*to].pitch.semitone())
426 - i64::from(source.notes[*from].pitch.semitone()),
427 cost: *cost,
428 },
429 AssignmentOperation::Double {
430 source: from,
431 target: to,
432 cost,
433 } => VoiceLeadingMotion::Double {
434 source: source.notes[*from].clone(),
435 target: target.notes[*to].clone(),
436 semitones: i64::from(target.notes[*to].pitch.semitone())
437 - i64::from(source.notes[*from].pitch.semitone()),
438 cost: *cost,
439 },
440 AssignmentOperation::Insert { target: to, cost } => VoiceLeadingMotion::Enter {
441 target: target.notes[*to].clone(),
442 cost: *cost,
443 },
444 AssignmentOperation::Delete { source: from, cost } => VoiceLeadingMotion::Leave {
445 source: source.notes[*from].clone(),
446 cost: *cost,
447 },
448 })
449 .collect()
450}