1use super::{
3 duration::Duration,
4 notation::{BeamState, KeySignature},
5 pitch::{Pitch, Step},
6 score::{Note, NoteAddr, Score},
7 validate::validate,
8};
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum MelodicInsertionKind {
16 Passing,
17 UpperNeighbor,
18 LowerNeighbor,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum MelodicInsertionSource {
24 ShortenNote,
25 FillRest,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct MelodicInsertionOptions {
31 pub part: usize,
32 pub staff: usize,
33 pub voice: usize,
34 pub start_measure: usize,
35 pub end_measure: usize,
36 pub key: KeySignature,
37 pub kind: MelodicInsertionKind,
38 pub source: MelodicInsertionSource,
39 pub max_insertions: usize,
40 pub min_midi: u8,
41 pub max_midi: u8,
42 pub max_leap_semitones: u8,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct MelodicSourceMapping {
47 pub source: NoteAddr,
48 pub output: NoteAddr,
49}
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct MelodicInsertion {
52 pub source_anchor: NoteAddr,
53 pub output_anchor: NoteAddr,
54 pub inserted: NoteAddr,
55 pub original_duration: Duration,
56 pub original_dot_count: u8,
57 pub pitch: Pitch,
58}
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct MelodicInsertionDiagnostic {
61 pub code: String,
62 pub measure: Option<usize>,
63 pub message: String,
64}
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct MelodicInsertionResult {
67 pub score: Option<Score>,
68 pub options: MelodicInsertionOptions,
69 pub source_mapping: Vec<MelodicSourceMapping>,
70 pub insertions: Vec<MelodicInsertion>,
71 pub diagnostics: Vec<MelodicInsertionDiagnostic>,
72}
73
74fn diagnostic(code: &str, measure: Option<usize>, message: &str) -> MelodicInsertionDiagnostic {
75 MelodicInsertionDiagnostic {
76 code: code.into(),
77 measure,
78 message: message.into(),
79 }
80}
81fn plain(note: &Note) -> bool {
82 !note.is_unpitched
83 && !note.is_grace
84 && !note.is_cue
85 && !note.tie_start
86 && !note.tie_end
87 && note.pitch_tie_starts.is_empty()
88 && note.pitch_tie_ends.is_empty()
89 && note.tuplet.is_none()
90 && note.tuplet_boundary.is_none()
91 && note.beam == BeamState::None
92 && note.articulations.is_empty()
93 && note.tremolo_pair.is_none()
94 && note.hairpin_start.is_none()
95 && !note.hairpin_end
96 && note.ottava_start.is_none()
97 && !note.ottava_end
98 && !note.pedal_start
99 && !note.pedal_end
100 && note.cross_staff.is_none()
101 && !note.slur_start
102 && !note.slur_end
103 && !note.glissando_start
104 && !note.glissando_end
105 && !note.trill_line_start
106 && !note.trill_line_end
107}
108fn step_index(step: &Step) -> i16 {
109 match step {
110 Step::C => 0,
111 Step::D => 1,
112 Step::E => 2,
113 Step::F => 3,
114 Step::G => 4,
115 Step::A => 5,
116 Step::B => 6,
117 }
118}
119fn diatonic_position(p: &Pitch) -> i16 {
120 i16::from(p.octave) * 7 + step_index(&p.step)
121}
122fn pitch_at(position: i16, key: &KeySignature) -> Option<Pitch> {
123 let octave = i8::try_from(position.div_euclid(7)).ok()?;
124 let step = match position.rem_euclid(7) {
125 0 => Step::C,
126 1 => Step::D,
127 2 => Step::E,
128 3 => Step::F,
129 4 => Step::G,
130 5 => Step::A,
131 _ => Step::B,
132 };
133 let alter = key.alter_for_step(&step);
134 Some(Pitch::with_alter(step, octave, alter))
135}
136fn candidate(left: &Pitch, right: &Pitch, options: &MelodicInsertionOptions) -> Option<Pitch> {
137 if left.microtone_cents != 0
138 || right.microtone_cents != 0
139 || !options.key.contains_pitch(left)
140 || !options.key.contains_pitch(right)
141 {
142 return None;
143 }
144 let (a, b) = (diatonic_position(left), diatonic_position(right));
145 let position = match options.kind {
146 MelodicInsertionKind::Passing if (b - a).abs() == 2 => a + (b - a).signum(),
147 MelodicInsertionKind::UpperNeighbor if a == b && left.to_midi() == right.to_midi() => a + 1,
148 MelodicInsertionKind::LowerNeighbor if a == b && left.to_midi() == right.to_midi() => a - 1,
149 _ => return None,
150 };
151 let pitch = pitch_at(position, &options.key)?;
152 let midi = pitch.to_midi();
153 if !(i16::from(options.min_midi)..=i16::from(options.max_midi)).contains(&midi)
154 || (midi - left.to_midi()).abs() > i16::from(options.max_leap_semitones)
155 || (midi - right.to_midi()).abs() > i16::from(options.max_leap_semitones)
156 {
157 return None;
158 }
159 Some(pitch)
160}
161fn generated_id(
162 score: &Score,
163 anchor: &Note,
164 measure: usize,
165 index: usize,
166 kind: MelodicInsertionKind,
167 source: MelodicInsertionSource,
168) -> String {
169 let input = format!(
170 "melodic-insertion-v1:{}:{}:{measure}:{index}:{kind:?}:{source:?}",
171 score.id, anchor.id
172 );
173 let hash = |seed: u64| {
174 input
175 .bytes()
176 .fold(seed, |h, b| (h ^ u64::from(b)).wrapping_mul(0x100000001b3))
177 };
178 Uuid::from_u128(
179 (u128::from(hash(0xcbf29ce484222325)) << 64) | u128::from(hash(0x84222325cbf29ce4)),
180 )
181 .to_string()
182}
183
184pub fn insert_melodic_notes(
187 score: &Score,
188 options: MelodicInsertionOptions,
189) -> MelodicInsertionResult {
190 let mut result = MelodicInsertionResult {
191 score: None,
192 options: options.clone(),
193 source_mapping: Vec::new(),
194 insertions: Vec::new(),
195 diagnostics: Vec::new(),
196 };
197 if !validate(score).is_valid()
198 || !(-7..=7).contains(&options.key.fifths)
199 || !matches!(options.key.mode.as_str(), "major" | "minor")
200 || options.min_midi > options.max_midi
201 || options.max_midi > 127
202 || options.max_leap_semitones == 0
203 || options.max_leap_semitones > 12
204 || options.max_insertions > 128
205 || options.start_measure >= options.end_measure
206 || options.end_measure - options.start_measure > 128
207 {
208 result.diagnostics.push(diagnostic(
209 "invalid-options",
210 None,
211 "invalid score, range, key, pitch or density limit",
212 ));
213 return result;
214 }
215 let Some(staff) = score
216 .parts
217 .get(options.part)
218 .and_then(|p| p.staves.get(options.staff))
219 else {
220 result.diagnostics.push(diagnostic(
221 "invalid-address",
222 None,
223 "part or staff does not exist",
224 ));
225 return result;
226 };
227 if options.end_measure > staff.measures.len() || options.voice >= 4 {
228 result.diagnostics.push(diagnostic(
229 "invalid-address",
230 None,
231 "measure or voice does not exist",
232 ));
233 return result;
234 }
235 let has_harmony_ranges = score
236 .parts
237 .iter()
238 .flat_map(|p| &p.staves)
239 .flat_map(|s| &s.measures)
240 .flat_map(|m| &m.voices)
241 .flatten()
242 .any(|n| {
243 n.chord_symbol
244 .as_ref()
245 .is_some_and(|h| h.range_end.is_some())
246 });
247 if !score.spanners.is_empty()
248 || !score.object_style_overrides.is_empty()
249 || !score.views.is_empty()
250 || has_harmony_ranges
251 {
252 result.diagnostics.push(diagnostic(
253 "unsupported-references",
254 None,
255 "typed spans, harmony ranges, object overrides and score views require address remapping",
256 ));
257 return result;
258 }
259 let mut used: HashSet<String> = score
260 .parts
261 .iter()
262 .flat_map(|p| &p.staves)
263 .flat_map(|s| &s.measures)
264 .flat_map(|m| &m.voices)
265 .flatten()
266 .map(|n| n.id.clone())
267 .collect();
268 let mut output = score.clone();
269 let mut key = score.settings.key_signature.clone();
270 for mi in 0..options.end_measure {
271 let measure = &staff.measures[mi];
272 if let Some(k) = &measure.key_sig {
273 key = k.clone();
274 }
275 if mi < options.start_measure {
276 continue;
277 }
278 let voice = &measure.voices[options.voice];
279 let incompatible = key != options.key
280 || measure
281 .voices
282 .iter()
283 .enumerate()
284 .any(|(i, v)| i != options.voice && v.iter().any(|n| !n.is_rest))
285 || voice.iter().any(|n| !plain(n));
286 if incompatible {
287 result.diagnostics.push(diagnostic(
288 "unsupported-measure",
289 Some(mi),
290 "key differs or the measure contains polyphony, ties, tuplets, articulations, beams or spans",
291 ));
292 }
293 let mut next_voice = Vec::with_capacity(voice.len() + 1);
294 let mut changed = false;
295 for (i, note) in voice.iter().enumerate() {
296 let addr = |note| NoteAddr {
297 part: options.part,
298 staff: options.staff,
299 measure: mi,
300 voice: options.voice,
301 note,
302 };
303 let output_index = next_voice.len();
304 result.source_mapping.push(MelodicSourceMapping {
305 source: addr(i),
306 output: addr(output_index),
307 });
308 let mut anchor = note.clone();
309 let split = if note.dot_count == 0 {
310 match note.duration {
311 Duration::Half => Some((Duration::Quarter, Duration::Eighth)),
312 Duration::Quarter => Some((Duration::Eighth, Duration::Sixteenth)),
313 _ => None,
314 }
315 } else {
316 None
317 };
318 let endpoints = match options.source {
319 MelodicInsertionSource::ShortenNote if !note.is_rest && note.pitches.len() == 1 => {
320 voice
321 .get(i + 1)
322 .filter(|n| !n.is_rest && n.pitches.len() == 1)
323 .map(|n| (¬e.pitches[0], &n.pitches[0]))
324 }
325 MelodicInsertionSource::FillRest if note.is_rest && i > 0 => voice
326 .get(i - 1)
327 .zip(voice.get(i + 1))
328 .filter(|(a, b)| {
329 !a.is_rest && !b.is_rest && a.pitches.len() == 1 && b.pitches.len() == 1
330 })
331 .map(|(a, b)| (&a.pitches[0], &b.pitches[0])),
332 _ => None,
333 };
334 let attempted = !incompatible
335 && result.insertions.len() < options.max_insertions
336 && endpoints.is_some();
337 let candidate = if attempted {
338 endpoints.and_then(|(a, b)| candidate(a, b, &options))
339 } else {
340 None
341 };
342 if attempted && (split.is_none() || candidate.is_none()) {
343 result.diagnostics.push(diagnostic(
344 "ineligible-candidate",
345 Some(mi),
346 "the interval, pitch limits or written duration cannot be edited safely",
347 ));
348 }
349 if let (Some((short, added)), Some(pitch)) = (split, candidate) {
350 let id = generated_id(score, note, mi, i, options.kind, options.source);
351 if !used.insert(id.clone()) {
352 result.diagnostics.push(diagnostic(
353 "id-conflict",
354 Some(mi),
355 "generated note identity conflicts with an existing note",
356 ));
357 } else {
358 anchor.duration = short;
359 anchor.dot_count = 1;
360 let mut inserted = Note::new(pitch.clone(), added);
361 inserted.id = id;
362 result.insertions.push(MelodicInsertion {
363 source_anchor: addr(i),
364 output_anchor: addr(output_index),
365 inserted: addr(output_index + 1),
366 original_duration: note.duration.clone(),
367 original_dot_count: note.dot_count,
368 pitch,
369 });
370 next_voice.push(anchor);
371 next_voice.push(inserted);
372 changed = true;
373 continue;
374 }
375 }
376 next_voice.push(anchor);
377 }
378 if !changed && !incompatible {
379 result.diagnostics.push(diagnostic(
380 "no-candidate",
381 Some(mi),
382 "no eligible insertion at the selected density and musical constraints",
383 ));
384 }
385 output.parts[options.part].staves[options.staff].measures[mi].voices[options.voice] =
386 next_voice;
387 }
388 if !validate(&output).is_valid() {
389 result.diagnostics.push(diagnostic(
390 "invalid-output",
391 None,
392 "generated score failed validation",
393 ));
394 result.source_mapping.clear();
395 result.insertions.clear();
396 return result;
397 }
398 result.score = Some(output);
399 result
400}