1use super::{
3 chord_plan::{CHORD_PLAN_CONTRACT_VERSION, ChordPlan, ChordPlanEntry},
4 duration::Duration,
5 notation::{BeamState, KeySignature},
6 pitch::{Pitch, Step},
7 score::{Note, NoteAddr, ObjectStyleTarget, Score},
8 validate::validate,
9};
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16pub enum MelodicInsertionKind {
17 Passing,
18 UpperNeighbor,
19 LowerNeighbor,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "kebab-case")]
24pub enum MelodicInsertionSource {
25 ShortenNote,
26 FillRest,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MelodicInsertionOptions {
32 pub part: usize,
33 pub staff: usize,
34 pub voice: usize,
35 pub start_measure: usize,
36 pub end_measure: usize,
37 pub key: KeySignature,
38 pub kind: MelodicInsertionKind,
39 pub source: MelodicInsertionSource,
40 pub max_insertions: usize,
41 pub min_midi: u8,
42 pub max_midi: u8,
43 pub max_leap_semitones: u8,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct MelodicSourceMapping {
48 pub source: NoteAddr,
49 pub output: NoteAddr,
50}
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct MelodicInsertion {
53 pub source_anchor: NoteAddr,
54 pub output_anchor: NoteAddr,
55 pub inserted: NoteAddr,
56 pub original_duration: Duration,
57 pub original_dot_count: u8,
58 pub pitch: Pitch,
59}
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct MelodicInsertionDiagnostic {
62 pub code: String,
63 pub measure: Option<usize>,
64 pub message: String,
65}
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct MelodicInsertionResult {
68 pub score: Option<Score>,
69 pub options: MelodicInsertionOptions,
70 pub source_mapping: Vec<MelodicSourceMapping>,
71 pub insertions: Vec<MelodicInsertion>,
72 pub diagnostics: Vec<MelodicInsertionDiagnostic>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "kebab-case")]
77pub enum MelodicInsertionOutcome {
78 Invalid,
79 NoChange,
80 Partial,
81 Applied,
82}
83
84impl MelodicInsertionResult {
85 pub fn outcome(&self) -> MelodicInsertionOutcome {
86 if self.score.is_none() {
87 MelodicInsertionOutcome::Invalid
88 } else if self.insertions.is_empty() {
89 MelodicInsertionOutcome::NoChange
90 } else if self.diagnostics.is_empty() {
91 MelodicInsertionOutcome::Applied
92 } else {
93 MelodicInsertionOutcome::Partial
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct MelodicLocalKey {
101 pub start_measure: usize,
102 pub key: KeySignature,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct MelodicInsertionSelection {
107 pub source_anchor: NoteAddr,
108 pub pitch: Pitch,
109}
110
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
113#[serde(default)]
114pub struct MelodicInsertionContext {
115 pub chord_plan: Option<ChordPlan>,
116 pub local_keys: Vec<MelodicLocalKey>,
117 pub phrase_end_measures: Vec<usize>,
118 pub selections: Vec<MelodicInsertionSelection>,
119 pub max_insertions_per_measure: Option<usize>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct MelodicInsertionCandidate {
124 pub source_anchor: NoteAddr,
125 pub beat: f64,
126 pub pitch: Pitch,
127 pub kind: MelodicInsertionKind,
128 pub score: i32,
129 pub reason_codes: Vec<String>,
130 pub constraint_violations: Vec<String>,
131 pub selected: bool,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MelodicInsertionContextResult {
136 pub insertion: MelodicInsertionResult,
137 pub candidates: Vec<MelodicInsertionCandidate>,
138 pub outcome: MelodicInsertionOutcome,
139}
140
141fn diagnostic(code: &str, measure: Option<usize>, message: &str) -> MelodicInsertionDiagnostic {
142 MelodicInsertionDiagnostic {
143 code: code.into(),
144 measure,
145 message: message.into(),
146 }
147}
148fn safe_anchor(note: &Note) -> bool {
149 !note.is_unpitched
150 && !note.is_grace
151 && !note.is_cue
152 && !note.tie_start
153 && !note.tie_end
154 && note.pitch_tie_starts.is_empty()
155 && note.pitch_tie_ends.is_empty()
156 && !note.pitch_incoming_partial_ties.iter().any(|tied| *tied)
157 && !note.pitch_tie_sources.iter().any(Option::is_some)
158 && note.tuplet.is_none()
159 && note.tuplet_boundary.is_none()
160 && note.beam == BeamState::None
161 && !note
162 .articulations
163 .contains(&super::notation::Articulation::Fermata)
164 && note.tremolo_pair.is_none()
165 && note.cross_staff.is_none()
166 && note.guitar_technique.is_none()
167 && !note.hidden
168}
169
170fn remap_address(address: &mut NoteAddr, mapping: &[MelodicSourceMapping]) {
171 if let Some(found) = mapping.iter().find(|entry| entry.source == *address) {
172 *address = found.output.clone();
173 }
174}
175
176fn remap_references(score: &mut Score, mapping: &[MelodicSourceMapping]) {
177 for spanner in &mut score.spanners {
178 remap_address(&mut spanner.start, mapping);
179 remap_address(&mut spanner.end, mapping);
180 }
181 for style in &mut score.object_style_overrides {
182 if let ObjectStyleTarget::Note { address } = &mut style.target {
183 remap_address(address, mapping);
184 }
185 }
186 for note in score
187 .parts
188 .iter_mut()
189 .flat_map(|part| &mut part.staves)
190 .flat_map(|staff| &mut staff.measures)
191 .flat_map(|measure| &mut measure.voices)
192 .flatten()
193 {
194 if let Some(end) = note
195 .chord_symbol
196 .as_mut()
197 .and_then(|harmony| harmony.range_end.as_mut())
198 {
199 remap_address(end, mapping);
200 }
201 }
202}
203fn step_index(step: &Step) -> i16 {
204 match step {
205 Step::C => 0,
206 Step::D => 1,
207 Step::E => 2,
208 Step::F => 3,
209 Step::G => 4,
210 Step::A => 5,
211 Step::B => 6,
212 }
213}
214fn diatonic_position(p: &Pitch) -> i16 {
215 i16::from(p.octave) * 7 + step_index(&p.step)
216}
217fn pitch_at(position: i16, key: &KeySignature) -> Option<Pitch> {
218 let octave = i8::try_from(position.div_euclid(7)).ok()?;
219 let step = match position.rem_euclid(7) {
220 0 => Step::C,
221 1 => Step::D,
222 2 => Step::E,
223 3 => Step::F,
224 4 => Step::G,
225 5 => Step::A,
226 _ => Step::B,
227 };
228 let alter = key.alter_for_step(&step);
229 Some(Pitch::with_alter(step, octave, alter))
230}
231fn candidate(
232 left: &Pitch,
233 right: &Pitch,
234 key: &KeySignature,
235 options: &MelodicInsertionOptions,
236) -> Option<Pitch> {
237 if left.microtone_cents != 0
238 || right.microtone_cents != 0
239 || !key.contains_pitch(left)
240 || !key.contains_pitch(right)
241 {
242 return None;
243 }
244 let (a, b) = (diatonic_position(left), diatonic_position(right));
245 let position = match options.kind {
246 MelodicInsertionKind::Passing if (b - a).abs() == 2 => a + (b - a).signum(),
247 MelodicInsertionKind::UpperNeighbor if a == b && left.to_midi() == right.to_midi() => a + 1,
248 MelodicInsertionKind::LowerNeighbor if a == b && left.to_midi() == right.to_midi() => a - 1,
249 _ => return None,
250 };
251 let pitch = pitch_at(position, key)?;
252 Some(pitch)
253}
254
255fn constraint_violations(
256 pitch: &Pitch,
257 left: &Pitch,
258 right: &Pitch,
259 options: &MelodicInsertionOptions,
260) -> Vec<String> {
261 let midi = pitch.to_midi();
262 let mut violations = Vec::new();
263 if !(i16::from(options.min_midi)..=i16::from(options.max_midi)).contains(&midi) {
264 violations.push("pitch-range".into());
265 }
266 if (midi - left.to_midi()).abs() > i16::from(options.max_leap_semitones)
267 || (midi - right.to_midi()).abs() > i16::from(options.max_leap_semitones)
268 {
269 violations.push("leap-limit".into());
270 }
271 violations
272}
273
274fn chord_tones(entry: &ChordPlanEntry) -> Option<Vec<i16>> {
275 let root = entry.chord.root.as_bytes();
276 let base = match root.first()?.to_ascii_uppercase() {
277 b'C' => 0,
278 b'D' => 2,
279 b'E' => 4,
280 b'F' => 5,
281 b'G' => 7,
282 b'A' => 9,
283 b'B' => 11,
284 _ => return None,
285 };
286 let alter = match root.get(1..) {
287 Some(b"") => 0,
288 Some(b"#") => 1,
289 Some(b"b") => -1,
290 _ => return None,
291 };
292 let intervals: &[i16] = match entry.chord.kind.as_str() {
293 "major" => &[0, 4, 7],
294 "minor" => &[0, 3, 7],
295 "diminished" => &[0, 3, 6],
296 "augmented" => &[0, 4, 8],
297 "dominant" | "dominant-seventh" => &[0, 4, 7, 10],
298 "major-seventh" => &[0, 4, 7, 11],
299 "minor-seventh" => &[0, 3, 7, 10],
300 "half-diminished" => &[0, 3, 6, 10],
301 "diminished-seventh" => &[0, 3, 6, 9],
302 _ => return None,
303 };
304 Some(
305 intervals
306 .iter()
307 .map(|interval| (base + alter + *interval).rem_euclid(12))
308 .collect(),
309 )
310}
311
312fn harmony_at(plan: &ChordPlan, measure: usize, beat: f64) -> Option<&ChordPlanEntry> {
313 plan.entries
314 .iter()
315 .filter(|entry| {
316 entry.position.offset.denominator != 0
317 && (entry.position.measure_index < measure
318 || (entry.position.measure_index == measure
319 && entry.position.offset.beats() <= beat + 1e-8))
320 })
321 .max_by(|a, b| {
322 a.position
323 .measure_index
324 .cmp(&b.position.measure_index)
325 .then_with(|| {
326 a.position
327 .offset
328 .beats()
329 .total_cmp(&b.position.offset.beats())
330 })
331 })
332}
333
334struct CandidateEvaluation<'a> {
335 base: Pitch,
336 left: &'a Pitch,
337 right: &'a Pitch,
338 beat: f64,
339 source: NoteAddr,
340 options: &'a MelodicInsertionOptions,
341 harmony: Option<&'a ChordPlanEntry>,
342 resolves: bool,
343 phrase_end: bool,
344}
345
346fn ranked_candidates(evaluation: CandidateEvaluation<'_>) -> Vec<MelodicInsertionCandidate> {
347 let CandidateEvaluation {
348 base,
349 left,
350 right,
351 beat,
352 source,
353 options,
354 harmony,
355 resolves,
356 phrase_end,
357 } = evaluation;
358 let chord_pitches = harmony.and_then(chord_tones);
359 let mut candidates = Vec::new();
360 for delta in if harmony.is_some() {
361 [-1, 0, 1].as_slice()
362 } else {
363 [0].as_slice()
364 } {
365 let mut pitch = base.clone();
366 pitch.alter += delta;
367 let midi = pitch.to_midi();
368 let violations = constraint_violations(&pitch, left, right, options);
369 let chord_tone = chord_pitches
370 .as_ref()
371 .is_some_and(|tones| tones.contains(&midi.rem_euclid(12)));
372 let weak_beat = (beat - beat.round()).abs() > 1e-8;
373 let mut score = if chord_tone {
374 10
375 } else if harmony.is_none() {
376 5
377 } else if weak_beat && resolves {
378 6
379 } else {
380 -5
381 };
382 let mut reason_codes = vec![
383 if chord_tone {
384 "chord-tone"
385 } else if harmony.is_none() {
386 "diatonic"
387 } else if weak_beat && resolves {
388 "resolved-weak-beat-nonchord-tone"
389 } else {
390 "unresolved-or-strong-beat-dissonance"
391 }
392 .to_string(),
393 ];
394 if *delta != 0 {
395 score -= 2;
396 reason_codes.push("chromatic-alteration".into());
397 }
398 if phrase_end && !chord_tone && harmony.is_some() {
399 score -= 8;
400 reason_codes.push("phrase-ending-nonchord-tone".into());
401 }
402 if !violations.is_empty() {
403 score = -100;
404 }
405 candidates.push(MelodicInsertionCandidate {
406 source_anchor: source.clone(),
407 beat,
408 pitch,
409 kind: options.kind,
410 score,
411 reason_codes,
412 constraint_violations: violations,
413 selected: false,
414 });
415 }
416 candidates.sort_by(|a, b| {
417 b.score
418 .cmp(&a.score)
419 .then_with(|| {
420 (a.pitch.alter - base.alter)
421 .abs()
422 .cmp(&(b.pitch.alter - base.alter).abs())
423 })
424 .then_with(|| a.pitch.to_midi().cmp(&b.pitch.to_midi()))
425 });
426 candidates
427}
428fn generated_id(
429 score: &Score,
430 anchor: &Note,
431 measure: usize,
432 index: usize,
433 kind: MelodicInsertionKind,
434 source: MelodicInsertionSource,
435) -> String {
436 let input = format!(
437 "melodic-insertion-v1:{}:{}:{measure}:{index}:{kind:?}:{source:?}",
438 score.id, anchor.id
439 );
440 let hash = |seed: u64| {
441 input
442 .bytes()
443 .fold(seed, |h, b| (h ^ u64::from(b)).wrapping_mul(0x100000001b3))
444 };
445 Uuid::from_u128(
446 (u128::from(hash(0xcbf29ce484222325)) << 64) | u128::from(hash(0x84222325cbf29ce4)),
447 )
448 .to_string()
449}
450
451pub fn insert_melodic_notes(
454 score: &Score,
455 options: MelodicInsertionOptions,
456) -> MelodicInsertionResult {
457 insert_melodic_notes_impl(score, options, None).insertion
458}
459
460pub fn insert_melodic_notes_with_context(
463 score: &Score,
464 options: MelodicInsertionOptions,
465 context: &MelodicInsertionContext,
466) -> MelodicInsertionContextResult {
467 insert_melodic_notes_impl(score, options, Some(context))
468}
469
470fn insert_melodic_notes_impl(
471 score: &Score,
472 options: MelodicInsertionOptions,
473 context: Option<&MelodicInsertionContext>,
474) -> MelodicInsertionContextResult {
475 let mut result = MelodicInsertionResult {
476 score: None,
477 options: options.clone(),
478 source_mapping: Vec::new(),
479 insertions: Vec::new(),
480 diagnostics: Vec::new(),
481 };
482 let mut candidates = Vec::new();
483 macro_rules! done {
484 () => {
485 return MelodicInsertionContextResult {
486 outcome: result.outcome(),
487 insertion: result,
488 candidates,
489 }
490 };
491 }
492 if !validate(score).is_valid()
493 || !(-7..=7).contains(&options.key.fifths)
494 || !matches!(options.key.mode.as_str(), "major" | "minor")
495 || options.min_midi > options.max_midi
496 || options.max_midi > 127
497 || options.max_leap_semitones == 0
498 || options.max_leap_semitones > 12
499 || options.max_insertions > 128
500 || options.start_measure >= options.end_measure
501 || options.end_measure - options.start_measure > 128
502 {
503 result.diagnostics.push(diagnostic(
504 "invalid-options",
505 None,
506 "invalid score, range, key, pitch or density limit",
507 ));
508 done!();
509 }
510 let Some(staff) = score
511 .parts
512 .get(options.part)
513 .and_then(|p| p.staves.get(options.staff))
514 else {
515 result.diagnostics.push(diagnostic(
516 "invalid-address",
517 None,
518 "part or staff does not exist",
519 ));
520 done!();
521 };
522 if options.end_measure > staff.measures.len() || options.voice >= 4 {
523 result.diagnostics.push(diagnostic(
524 "invalid-address",
525 None,
526 "measure or voice does not exist",
527 ));
528 done!();
529 }
530 if let Some(context) = context {
531 let valid_keys = context.local_keys.iter().all(|entry| {
532 entry.start_measure < options.end_measure
533 && (-7..=7).contains(&entry.key.fifths)
534 && matches!(entry.key.mode.as_str(), "major" | "minor")
535 }) && context
536 .local_keys
537 .windows(2)
538 .all(|pair| pair[0].start_measure < pair[1].start_measure);
539 let valid_plan = context.chord_plan.as_ref().is_none_or(|plan| {
540 plan.contract_version == CHORD_PLAN_CONTRACT_VERSION
541 && !plan.entries.is_empty()
542 && plan.entries.len() <= 512
543 && plan.entries.iter().all(|entry| {
544 entry.position.offset.denominator != 0
545 && entry.offset_beats.is_finite()
546 && entry.offset_beats <= 32.0
547 && (entry.offset_beats - entry.position.offset.beats()).abs() < 1e-8
548 })
549 && plan.entries.windows(2).all(|pair| {
550 pair[0].position.measure_index < pair[1].position.measure_index
551 || (pair[0].position.measure_index == pair[1].position.measure_index
552 && pair[0].position.offset.beats() < pair[1].position.offset.beats())
553 })
554 });
555 let valid_selections = context.selections.len() <= 128
556 && context
557 .selections
558 .iter()
559 .enumerate()
560 .all(|(index, selection)| {
561 !context.selections[..index]
562 .iter()
563 .any(|previous| previous.source_anchor == selection.source_anchor)
564 });
565 if !valid_keys
566 || !valid_plan
567 || !valid_selections
568 || context
569 .max_insertions_per_measure
570 .is_some_and(|limit| limit > 16)
571 {
572 result.diagnostics.push(diagnostic(
573 "invalid-context",
574 None,
575 "invalid local key, chord plan or selection limit",
576 ));
577 done!();
578 }
579 }
580 let mut used: HashSet<String> = score
581 .parts
582 .iter()
583 .flat_map(|p| &p.staves)
584 .flat_map(|s| &s.measures)
585 .flat_map(|m| &m.voices)
586 .flatten()
587 .map(|n| n.id.clone())
588 .collect();
589 let mut output = score.clone();
590 let mut key = score.settings.key_signature.clone();
591 for mi in 0..options.end_measure {
592 let measure = &staff.measures[mi];
593 if let Some(k) = &measure.key_sig {
594 key = k.clone();
595 }
596 if mi < options.start_measure {
597 continue;
598 }
599 let voice = &measure.voices[options.voice];
600 let measure_start_insertions = result.insertions.len();
601 let explicit_local_key = context.and_then(|ctx| {
602 ctx.local_keys
603 .iter()
604 .rev()
605 .find(|entry| entry.start_measure <= mi)
606 });
607 let local_key = explicit_local_key.map_or(&key, |entry| &entry.key);
608 let incompatible = (explicit_local_key.is_none() && key != options.key)
609 || measure
610 .voices
611 .iter()
612 .enumerate()
613 .any(|(i, v)| i != options.voice && v.iter().any(|n| !n.is_rest));
614 if incompatible {
615 result.diagnostics.push(diagnostic(
616 "unsupported-measure",
617 Some(mi),
618 &format!(
619 "part {} staff {} measure {} voice {}: key differs or the measure contains polyphony",
620 options.part, options.staff, mi, options.voice
621 ),
622 ));
623 }
624 let mut next_voice = Vec::with_capacity(voice.len() + 1);
625 let mut changed = false;
626 for (i, note) in voice.iter().enumerate() {
627 let addr = |note| NoteAddr {
628 part: options.part,
629 staff: options.staff,
630 measure: mi,
631 voice: options.voice,
632 note,
633 };
634 let output_index = next_voice.len();
635 result.source_mapping.push(MelodicSourceMapping {
636 source: addr(i),
637 output: addr(output_index),
638 });
639 let mut anchor = note.clone();
640 let split = if note.dot_count == 0 {
641 match note.duration {
642 Duration::Half => Some((Duration::Quarter, Duration::Eighth)),
643 Duration::Quarter => Some((Duration::Eighth, Duration::Sixteenth)),
644 _ => None,
645 }
646 } else {
647 None
648 };
649 let endpoints = match options.source {
650 MelodicInsertionSource::ShortenNote if !note.is_rest && note.pitches.len() == 1 => {
651 voice
652 .get(i + 1)
653 .filter(|n| {
654 !n.is_rest
655 && !n.is_grace
656 && !n.is_cue
657 && !n.is_unpitched
658 && n.pitches.len() == 1
659 })
660 .map(|n| (¬e.pitches[0], &n.pitches[0]))
661 }
662 MelodicInsertionSource::FillRest if note.is_rest && i > 0 => voice
663 .get(i - 1)
664 .zip(voice.get(i + 1))
665 .filter(|(a, b)| {
666 !a.is_rest
667 && !b.is_rest
668 && !a.is_grace
669 && !b.is_grace
670 && !a.is_cue
671 && !b.is_cue
672 && !a.is_unpitched
673 && !b.is_unpitched
674 && a.pitches.len() == 1
675 && b.pitches.len() == 1
676 })
677 .map(|(a, b)| (&a.pitches[0], &b.pitches[0])),
678 _ => None,
679 };
680 let attempted = !incompatible
681 && result.insertions.len() < options.max_insertions
682 && context.is_none_or(|ctx| {
683 ctx.max_insertions_per_measure.is_none_or(|limit| {
684 result.insertions.len() - measure_start_insertions < limit
685 })
686 })
687 && endpoints.is_some();
688 let selected_pitch = if attempted && split.is_some() {
689 endpoints.filter(|_| safe_anchor(note)).and_then(|(a, b)| {
690 let base = candidate(a, b, local_key, &options)?;
691 let Some(context) = context else {
692 return constraint_violations(&base, a, b, &options)
693 .is_empty()
694 .then_some(base);
695 };
696 let beat = voice[..i].iter().map(Note::beats).sum::<f64>()
697 + split.as_ref().map_or(0.0, |(short, _)| {
698 let mut shortened = note.clone();
699 shortened.duration = short.clone();
700 shortened.dot_count = 1;
701 shortened.beats()
702 });
703 let harmony = context.chord_plan.as_ref().and_then(|plan| harmony_at(plan, mi, beat));
704 if context.chord_plan.is_some() && harmony.is_none() {
705 result.diagnostics.push(diagnostic("uncovered-harmony", Some(mi),
706 &format!("part {} staff {} measure {} voice {} note {}: no explicit chord at insertion beat", options.part, options.staff, mi, options.voice, i)));
707 return None;
708 }
709 if harmony.is_some_and(|entry| chord_tones(entry).is_none()) {
710 result.diagnostics.push(diagnostic("unsupported-harmony", Some(mi),
711 &format!("part {} staff {} measure {} voice {} note {}: unsupported chord root or kind", options.part, options.staff, mi, options.voice, i)));
712 return None;
713 }
714 let right_beat = voice[..=i].iter().map(Note::beats).sum::<f64>();
715 let resolves = context.chord_plan.as_ref()
716 .and_then(|plan| harmony_at(plan, mi, right_beat))
717 .and_then(chord_tones)
718 .is_some_and(|tones| tones.contains(&b.to_midi().rem_euclid(12)));
719 let mut ranked = ranked_candidates(CandidateEvaluation {
720 base, left: a, right: b, beat, source: addr(i), options: &options,
721 harmony, resolves,
722 phrase_end: context.phrase_end_measures.contains(&mi) && i + 1 == voice.len() - 1,
723 });
724 let selected = if let Some(choice) = context.selections.iter().find(|choice| choice.source_anchor == addr(i)) {
725 ranked.iter().position(|item| item.pitch == choice.pitch && item.score >= 0
726 && item.constraint_violations.is_empty())
727 } else {
728 ranked.iter().position(|item| item.score >= 0
729 && item.constraint_violations.is_empty())
730 };
731 let pitch = selected.map(|index| {
732 ranked[index].selected = true;
733 ranked[index].pitch.clone()
734 });
735 if pitch.is_none() {
736 result.diagnostics.push(diagnostic("no-safe-candidate", Some(mi),
737 &format!("part {} staff {} measure {} voice {} note {}: no allowed pitch satisfies the musical context", options.part, options.staff, mi, options.voice, i)));
738 }
739 candidates.extend(ranked);
740 pitch
741 })
742 } else {
743 None
744 };
745 if attempted && (split.is_none() || selected_pitch.is_none()) {
746 result.diagnostics.push(diagnostic(
747 "ineligible-candidate",
748 Some(mi),
749 &format!(
750 "part {} staff {} measure {} voice {} note {}: interval, pitch limits, references or written duration cannot be edited safely",
751 options.part, options.staff, mi, options.voice, i
752 ),
753 ));
754 }
755 if let (Some((short, added)), Some(pitch)) = (split, selected_pitch) {
756 let id = generated_id(score, note, mi, i, options.kind, options.source);
757 if !used.insert(id.clone()) {
758 result.diagnostics.push(diagnostic(
759 "id-conflict",
760 Some(mi),
761 "generated note identity conflicts with an existing note",
762 ));
763 } else {
764 anchor.duration = short;
765 anchor.dot_count = 1;
766 let mut inserted = Note::new(pitch.clone(), added);
767 inserted.id = id;
768 result.insertions.push(MelodicInsertion {
769 source_anchor: addr(i),
770 output_anchor: addr(output_index),
771 inserted: addr(output_index + 1),
772 original_duration: note.duration.clone(),
773 original_dot_count: note.dot_count,
774 pitch,
775 });
776 next_voice.push(anchor);
777 next_voice.push(inserted);
778 changed = true;
779 continue;
780 }
781 }
782 next_voice.push(anchor);
783 }
784 if !changed && !incompatible {
785 result.diagnostics.push(diagnostic(
786 "no-candidate",
787 Some(mi),
788 "no eligible insertion at the selected density and musical constraints",
789 ));
790 }
791 output.parts[options.part].staves[options.staff].measures[mi].voices[options.voice] =
792 next_voice;
793 }
794 remap_references(&mut output, &result.source_mapping);
795 if !validate(&output).is_valid() {
796 result.diagnostics.push(diagnostic(
797 "invalid-output",
798 None,
799 "generated score failed validation",
800 ));
801 result.source_mapping.clear();
802 result.insertions.clear();
803 done!();
804 }
805 result.score = Some(output);
806 MelodicInsertionContextResult {
807 outcome: result.outcome(),
808 insertion: result,
809 candidates,
810 }
811}