1use crate::model::{
6 AlignedLyric, AnnotationPlacementModel, ClefChangeModel, EventAttachments, Fraction,
7 HarmonyKindText, KeySignatureModel, Measure, MeterModel, SlurRole, TempoBeatRole, TempoModel,
8 TieRole, TupletAttachment, TupletRole,
9};
10use crate::lower::voice::{clef_octave_shift, voice_octave_param, voice_octave_shift};
16use crate::{Accidental, BarlineKind, Pitch, Rational, RestVisibility, Score, TimedEventKind};
17
18#[derive(Debug, Clone, Copy, Default)]
19pub struct AbcWriteOptions {
20 pub legend: bool,
25}
26
27pub(crate) const LEGEND_LINES: [(&str, &str); 8] = [
31 (
32 "dp",
33 "[I:cr dp=<a|b>] direction placement (above/below)",
34 ),
35 (
36 "ht",
37 "[I:cr ht text=\"…\"|text-hex=…] chord-symbol text (text-hex when it contains ']', '%', or a control character)",
38 ),
39 (
40 "htx",
41 "[I:cr htx] chord symbol with no text",
42 ),
43 (
44 "le",
45 "[I:cr le=<n>] lyric extend (melisma), verse n",
46 ),
47 ("mr", "[I:cr mr] meter restatement"),
48 ("kr", "[I:cr kr] key restatement"),
49 (
50 "ec",
51 "[I:cr ec t=<s|d> l=<l|r> n=\"…\"] repeat-ending close",
52 ),
53 ("mf", "[I:cr mf] MusicXML <forward>"),
54];
55
56fn used_carrier_codes(abc: &str) -> Vec<&'static str> {
61 const NEEDLE: &str = "[I:cr ";
62 let mut found = [false; LEGEND_LINES.len()];
63 let mut search_from = 0;
64 while let Some(relative) = abc[search_from..].find(NEEDLE) {
65 let start = search_from + relative + NEEDLE.len();
66 let rest = &abc[start..];
67 let end = rest.find([']', ' ', '=']).unwrap_or(rest.len());
68 let code = &rest[..end];
69 if let Some(index) = LEGEND_LINES
70 .iter()
71 .position(|(registered, _)| *registered == code)
72 {
73 found[index] = true;
74 }
75 search_from = start;
78 }
79 LEGEND_LINES
80 .iter()
81 .zip(found)
82 .filter_map(|((code, _), used)| used.then_some(*code))
83 .collect()
84}
85
86fn legend_block(codes: &[&'static str]) -> String {
89 if codes.is_empty() {
90 return String::new();
91 }
92 let mut out = String::from("% croma carriers used in this file:\n");
93 for code in codes {
94 let (_, line) = LEGEND_LINES
95 .iter()
96 .find(|(registered, _)| registered == code)
97 .expect("code came from LEGEND_LINES");
98 out.push_str(&format!("% {line}\n"));
99 }
100 out
101}
102
103pub fn write_abc(score: &Score, options: AbcWriteOptions) -> String {
105 let mut out = String::new();
106 let meta = &score.metadata;
107 out.push_str(&format!("X:{}\n", meta.reference.text.trim()));
108 for directive in &meta.directives {
111 out.push_str(&format!("%%score {}\n", directive.value.text.trim()));
112 }
113 if let Some(title) = &meta.title {
114 out.push_str(&format!(
115 "T:{}\n",
116 crate::text::encode_text_string(title.text.trim(), false)
117 ));
118 }
119 for composer in &meta.composers {
124 out.push_str(&format!(
125 "C:{}\n",
126 crate::text::encode_text_string(composer.text.trim(), false)
127 ));
128 }
129 if let Some(meter) = &meta.meter {
132 if let Some(instruction) = time_symbol_instruction(meter) {
133 out.push_str(&format!("I:{instruction}\n"));
134 }
135 out.push_str(&format!("M:{}\n", meter.display));
136 }
137 let unit = unit_length(score);
138 out.push_str(&format!("L:{}/{}\n", unit.numerator, unit.denominator));
139 if let Some(q) = tempo_field(score) {
140 out.push_str(&format!("Q:{q}\n"));
141 }
142 let key_display = meta
143 .key
144 .as_ref()
145 .map(|k| k.display.clone())
146 .unwrap_or_else(|| "C".to_string());
147 out.push_str(&format!("K:{key_display}\n"));
148 out.push_str(&write_body(score, unit));
149 for line in &meta.post_tune_lyrics {
151 out.push_str(&format!(
152 "W:{}\n",
153 crate::text::encode_text_string(&line.text, false)
154 ));
155 }
156 if !out.ends_with('\n') {
157 out.push('\n');
158 }
159 if options.legend {
160 let legend = legend_block(&used_carrier_codes(&out));
161 if !legend.is_empty() {
162 out.insert_str(0, &legend);
163 }
164 }
165 out
166}
167
168fn tempo_display(tempo: &crate::model::TempoModel) -> String {
172 let mut out = String::new();
173 if let Some(text) = &tempo.text {
174 out.push('"');
175 out.push_str(&abc_quoted_text(text));
176 out.push('"');
177 }
178 if let Some(beat) = &tempo.beat {
179 if !out.is_empty() {
180 out.push(' ');
181 }
182 out.push_str(&format!(
183 "{}/{}={}",
184 beat.beat_numerator, beat.beat_denominator, beat.bpm
185 ));
186 }
187 out
188}
189
190fn sound_tempo_instruction(tempo: &TempoModel) -> Option<String> {
191 let beat = tempo.beat?;
192 let mut out = format!(
193 "croma-sound-tempo bpm={} beat-n={} beat-d={}",
194 beat.bpm, beat.beat_numerator, beat.beat_denominator
195 );
196 if let Some(text) = &tempo.text {
197 if needs_hex_inline_carrier(text) {
198 out.push_str(&format!(" text-hex={}", crate::hex::encode_hex_utf8(text)));
199 } else {
200 out.push_str(&format!(" text=\"{}\"", abc_carrier_quoted(text)));
201 }
202 }
203 Some(out)
204}
205
206fn tempo_instruction(tempo: &TempoModel) -> Option<String> {
207 if tempo.text.is_none() && tempo.beat.is_none() {
208 return None;
209 }
210 let role = match tempo.beat_role {
211 TempoBeatRole::PrintedMetronome => "printed",
212 TempoBeatRole::PlaybackSoundOnly => "sound",
213 };
214 let mut out = format!("croma-tempo role={role}");
215 if let Some(text) = &tempo.text {
216 if needs_hex_inline_carrier(text) {
217 out.push_str(&format!(" text-hex={}", crate::hex::encode_hex_utf8(text)));
218 } else {
219 out.push_str(&format!(" text=\"{}\"", abc_carrier_quoted(text)));
220 }
221 }
222 if let Some(beat) = tempo.beat {
223 out.push_str(&format!(
224 " bpm={} beat-n={} beat-d={}",
225 beat.bpm, beat.beat_numerator, beat.beat_denominator
226 ));
227 }
228 Some(out)
229}
230
231fn tempo_needs_instruction(tempo: &TempoModel) -> bool {
232 tempo.text.as_deref().is_some_and(needs_hex_inline_carrier)
233}
234
235fn harmony_text_instruction(kind_text: &HarmonyKindText) -> Option<String> {
236 match kind_text {
237 HarmonyKindText::AbcNative => None,
240 HarmonyKindText::Textless => Some("cr htx".to_owned()),
241 HarmonyKindText::Text(value) => {
242 if needs_hex_inline_carrier(value) {
243 Some(format!(
244 "cr ht text-hex={}",
245 crate::hex::encode_hex_utf8(value)
246 ))
247 } else {
248 Some(format!("cr ht text=\"{}\"", abc_carrier_quoted(value)))
249 }
250 }
251 }
252}
253
254fn direction_placement_instruction(placement: AnnotationPlacementModel) -> String {
255 let placement = match placement {
256 AnnotationPlacementModel::Below => "b",
257 AnnotationPlacementModel::Above
258 | AnnotationPlacementModel::Left
259 | AnnotationPlacementModel::Right
260 | AnnotationPlacementModel::Free => "a",
261 };
262 format!("cr dp={placement}")
263}
264
265fn lyric_extend_instruction(verse: u32) -> String {
266 format!("cr le={verse}")
267}
268
269fn lyric_duplicate_instruction(lyric: &AlignedLyric) -> String {
270 let mut out = format!("croma-lyric-duplicate verse={}", lyric.verse);
271 if needs_hex_inline_carrier(&lyric.text) {
272 out.push_str(&format!(
273 " text-hex={}",
274 crate::hex::encode_hex_utf8(&lyric.text)
275 ));
276 } else {
277 out.push_str(&format!(" text=\"{}\"", abc_carrier_quoted(&lyric.text)));
278 }
279 if lyric.same_note_extend {
280 out.push_str(" extend=1");
281 }
282 out
283}
284
285fn needs_hex_inline_carrier(text: &str) -> bool {
286 text.chars().any(|c| c == ']' || c == '%' || c.is_control())
287}
288
289fn meter_restatement_instruction() -> &'static str {
290 "cr mr"
291}
292
293fn key_restatement_instruction() -> &'static str {
294 "cr kr"
295}
296
297fn time_symbol_instruction(meter: &MeterModel) -> Option<String> {
298 let symbol = meter.time_symbol.as_deref()?;
299 if !matches!(symbol, "common" | "cut") {
300 return None;
301 }
302 if (symbol == "common" && meter.display.trim() == "C")
303 || (symbol == "cut" && meter.display.trim() == "C|")
304 {
305 return None;
306 }
307 Some(format!("croma-time-symbol symbol={symbol}"))
308}
309
310fn musicxml_forward_instruction() -> &'static str {
311 "cr mf"
312}
313
314fn musicxml_sequence_backup_instruction(duration: Fraction) -> String {
315 format!(
316 "croma-musicxml-sequence-backup n={} d={}",
317 duration.numerator, duration.denominator
318 )
319}
320
321fn abc_tuplet_actual_supported(actual_notes: u32) -> bool {
322 (2..=9).contains(&actual_notes)
323}
324
325fn musicxml_tuplet_instruction(tuplet: TupletAttachment) -> String {
326 let role = match tuplet.role {
327 TupletRole::Start => "start",
328 TupletRole::Continue => "continue",
329 TupletRole::Stop => "stop",
330 };
331 format!(
332 "croma-musicxml-tuplet id={} actual={} normal={} role={}",
333 tuplet.pair_id, tuplet.actual_notes, tuplet.normal_notes, role
334 )
335}
336
337fn musicxml_after_grace_instruction() -> &'static str {
338 "croma-after-grace"
339}
340
341fn clef_cursor_instruction(clef: &ClefChangeModel) -> Option<String> {
342 let cursor_back = clef.musicxml_cursor_back?;
343 let mut out = String::from("croma-clef-cursor");
344 if needs_hex_inline_carrier(&clef.clef.text) {
345 out.push_str(&format!(
346 " clef-hex={}",
347 crate::hex::encode_hex_utf8(&clef.clef.text)
348 ));
349 } else {
350 out.push_str(&format!(
351 " clef=\"{}\"",
352 abc_carrier_quoted(&clef.clef.text)
353 ));
354 }
355 out.push_str(&format!(
356 " back-n={} back-d={}",
357 cursor_back.numerator, cursor_back.denominator
358 ));
359 if let Some(pre_backup) = clef.musicxml_cursor_pre_backup {
360 out.push_str(&format!(
361 " pre-back-n={} pre-back-d={}",
362 pre_backup.numerator, pre_backup.denominator
363 ));
364 }
365 Some(out)
366}
367
368fn inline_clef_field_text(clef: &ClefChangeModel) -> Option<&str> {
375 let text = clef.clef.text.trim();
376 let writable = !text.is_empty() && !text.contains(']') && !text.chars().any(char::is_control);
377 writable.then_some(text)
378}
379
380fn barline_style_instruction(kind: BarlineKind) -> Option<&'static str> {
381 match kind {
382 BarlineKind::Dashed => Some("croma-barline-style style=dashed"),
383 _ => None,
384 }
385}
386
387fn xvoice_slur_instruction(pair: u32, role: SlurRole) -> String {
388 let role = match role {
389 SlurRole::Start => "start",
390 SlurRole::Stop => "stop",
391 };
392 format!("croma-xvoice-slur pair={pair} role={role}")
393}
394
395#[derive(Clone, Copy)]
399struct XvoiceSlurEmit {
400 slur_index: usize,
401 pair: u32,
402 role: SlurRole,
403}
404
405type XvoiceSlurMap = std::collections::HashMap<(usize, usize), Vec<XvoiceSlurEmit>>;
406
407fn detect_cross_voice_slurs(score: &Score) -> XvoiceSlurMap {
423 struct Dangler {
424 voice: usize,
425 event: usize,
426 slur_index: usize,
427 pair_id: u32,
428 measure: u32,
429 }
430 let mut dangling_starts: Vec<Dangler> = Vec::new();
431 let mut dangling_stops: Vec<Dangler> = Vec::new();
432 for (voice, voice_model) in score
433 .parts
434 .iter()
435 .flat_map(|part| part.voices.iter())
436 .enumerate()
437 {
438 let mut open: Vec<Dangler> = Vec::new();
439 for (event, timed) in voice_model.events.iter().enumerate() {
440 for (slur_index, slur) in timed.attachments.slurs.iter().enumerate() {
441 let dangler = Dangler {
442 voice,
443 event,
444 slur_index,
445 pair_id: slur.pair_id,
446 measure: timed.measure.index,
447 };
448 match slur.role {
449 SlurRole::Start => open.push(dangler),
450 SlurRole::Stop => {
451 if open.pop().is_none() {
452 dangling_stops.push(dangler);
453 }
454 }
455 }
456 }
457 }
458 dangling_starts.append(&mut open);
459 }
460 let mut map = XvoiceSlurMap::new();
461 if dangling_starts.is_empty() || dangling_stops.is_empty() {
462 return map;
463 }
464 let mut starts: std::collections::BTreeMap<u32, Vec<Dangler>> =
469 std::collections::BTreeMap::new();
470 for dangler in dangling_starts {
471 starts.entry(dangler.pair_id).or_default().push(dangler);
472 }
473 let mut stops: std::collections::BTreeMap<u32, Vec<Dangler>> =
474 std::collections::BTreeMap::new();
475 for dangler in dangling_stops {
476 stops.entry(dangler.pair_id).or_default().push(dangler);
477 }
478 let order = |d: &Dangler| (d.measure, d.voice, d.event);
479 let mut next_pair = 1u32;
480 for (pair_id, mut group_starts) in starts {
481 let Some(mut group_stops) = stops.remove(&pair_id) else {
482 continue;
483 };
484 group_starts.sort_by_key(&order);
485 group_stops.sort_by_key(&order);
486 for (start, stop) in group_starts.into_iter().zip(group_stops) {
487 let pair = next_pair;
488 next_pair += 1;
489 map.entry((start.voice, start.event))
490 .or_default()
491 .push(XvoiceSlurEmit {
492 slur_index: start.slur_index,
493 pair,
494 role: SlurRole::Start,
495 });
496 map.entry((stop.voice, stop.event))
497 .or_default()
498 .push(XvoiceSlurEmit {
499 slur_index: stop.slur_index,
500 pair,
501 role: SlurRole::Stop,
502 });
503 }
504 }
505 map
506}
507
508fn xvoice_slur_prefix(emits: &[XvoiceSlurEmit]) -> String {
511 let mut out = String::new();
512 for emit in emits {
513 out.push_str(&format!(
514 "[I:{}]",
515 xvoice_slur_instruction(emit.pair, emit.role)
516 ));
517 }
518 out
519}
520
521fn strip_xvoice_slurs(
524 attachments: &EventAttachments,
525 emits: &[XvoiceSlurEmit],
526) -> EventAttachments {
527 let mut stripped = attachments.clone();
528 let mut indexes: Vec<usize> = emits.iter().map(|emit| emit.slur_index).collect();
529 indexes.sort_unstable_by(|a, b| b.cmp(a));
530 for index in indexes {
531 if index < stripped.slurs.len() {
532 stripped.slurs.remove(index);
533 }
534 }
535 stripped
536}
537
538fn ending_close_instruction(model: &crate::model::RepeatEndingCloseModel) -> Option<String> {
539 let close_type = match model.close_type {
540 crate::model::RepeatEndingCloseType::Stop => "s",
541 crate::model::RepeatEndingCloseType::Discontinue => "d",
542 };
543 let location = match model.location {
544 crate::model::RepeatEndingCloseLocation::Left => "l",
545 crate::model::RepeatEndingCloseLocation::Right => "r",
546 };
547 let number = ending_number_value(&model.endings)?;
548 Some(format!("cr ec t={close_type} l={location} n=\"{number}\""))
549}
550
551fn ending_number_value(parts: &[crate::model::RepeatEndingPartModel]) -> Option<String> {
552 use crate::model::RepeatEndingPartModel::{Range, Single, Text};
553 let values = parts
554 .iter()
555 .map(|part| match part {
556 Single(value) => Some(value.to_string()),
557 Range { start, end } => Some(format!("{start}-{end}")),
558 Text(_) => None,
559 })
560 .collect::<Option<Vec<_>>>()?;
561 (!values.is_empty()).then(|| values.join(","))
562}
563
564fn initial_key_instruction(key: &KeySignatureModel) -> String {
565 let mut out = format!("croma-initial-key fifths={}", key.fifths);
566 if !key.explicit_accidentals.is_empty() {
567 let accidentals = key
568 .explicit_accidentals
569 .iter()
570 .map(|accidental| format!("{}:{}", accidental.step, accidental.accidental.alter()))
571 .collect::<Vec<_>>()
572 .join(",");
573 out.push_str(&format!(" accidentals={accidentals}"));
574 }
575 out
576}
577
578fn initial_key_carrier(score: &Score, key: &KeySignatureModel) -> Option<String> {
579 if score
580 .metadata
581 .key
582 .as_ref()
583 .is_some_and(|header| key.structurally_matches(header))
584 {
585 return None;
586 }
587 Some(initial_key_instruction(key))
588}
589
590fn initial_meter_instruction(meter: &MeterModel) -> String {
591 let mut out = format!(
592 "croma-initial-meter display=\"{}\"",
593 abc_carrier_quoted(&meter.display)
594 );
595 if let Some(symbol) = meter.time_symbol.as_deref()
596 && matches!(symbol, "common" | "cut")
597 {
598 out.push_str(&format!(" symbol={symbol}"));
599 }
600 out
601}
602
603fn initial_meter_carrier(score: &Score, meter: &MeterModel) -> Option<String> {
604 if score
605 .metadata
606 .meter
607 .as_ref()
608 .is_some_and(|header| meter.structurally_matches(header))
609 {
610 return None;
611 }
612 Some(initial_meter_instruction(meter))
613}
614
615fn measure_number_instruction(display_number: &str) -> String {
616 let mut out = "croma-measure-number".to_owned();
617 if needs_hex_inline_carrier(display_number) {
618 out.push_str(&format!(
619 " n-hex={}",
620 crate::hex::encode_hex_utf8(display_number)
621 ));
622 } else if display_number.chars().any(char::is_whitespace) {
623 out.push_str(&format!(" n=\"{}\"", abc_carrier_quoted(display_number)));
624 } else {
625 out.push_str(&format!(" n={display_number}"));
626 }
627 out
628}
629
630fn measure_number_carrier(measure: &Measure) -> Option<String> {
631 let display_number = measure.display_number.as_deref()?.trim();
632 if display_number.is_empty() {
633 return None;
634 }
635 let canonical_number = measure.id.index.saturating_add(1).to_string();
636 (display_number != canonical_number).then(|| measure_number_instruction(display_number))
637}
638
639fn barline_starts_measure(kind: BarlineKind) -> bool {
640 matches!(
641 kind,
642 BarlineKind::Regular | BarlineKind::Initial | BarlineKind::RepeatStart
643 )
644}
645
646fn carrier_follows_leading_barline(kind: &TimedEventKind, joined: Option<&str>) -> bool {
647 matches!(kind, TimedEventKind::Barline(barline) if joined.is_some() || barline_starts_measure(barline.kind))
648}
649
650fn abc_quoted_text(text: &str) -> String {
651 let collapsed = text.split_whitespace().collect::<Vec<_>>().join(" ");
652 crate::text::encode_text_string(&collapsed, true)
653}
654
655fn unit_length(score: &Score) -> Rational {
657 let small = Rational::new(1, 16);
658 let normal = Rational::new(1, 8);
659 match score.metadata.meter.as_ref().and_then(|m| m.duration) {
660 Some(dur) if (dur.numerator as u64) * 4 < (dur.denominator as u64) * 3 => small,
661 _ => normal,
662 }
663}
664
665fn tempo_field(score: &Score) -> Option<String> {
666 let tempo = score.metadata.tempo_model.as_ref()?;
667 if tempo.beat_role == TempoBeatRole::PlaybackSoundOnly || tempo_needs_instruction(tempo) {
668 return None;
669 }
670 let display = tempo_display(tempo);
671 (!display.is_empty()).then_some(display)
672}
673
674fn initial_tempo_instruction(score: &Score) -> Option<String> {
675 let tempo = score.metadata.tempo_model.as_ref()?;
676 if tempo.beat_role == TempoBeatRole::PlaybackSoundOnly || !tempo_needs_instruction(tempo) {
677 return None;
678 }
679 tempo_instruction(tempo)
680}
681
682fn write_body(score: &Score, unit: Rational) -> String {
683 let single = score.parts.len() == 1
685 && score.parts[0].voices.len() == 1
686 && score.parts[0].voices[0].id.value == "1"
687 && score.parts[0].voices[0].properties == crate::model::VoicePropertiesModel::default();
688 let xvoice = detect_cross_voice_slurs(score);
689 let mut body = String::new();
690 let mut voice_global_index = 0usize;
691 for (part_index, part) in score.parts.iter().enumerate() {
692 for (voice_index, voice) in part.voices.iter().enumerate() {
693 if !single {
694 body.push_str(&voice_header_line(voice));
695 }
696 if voice_index == 0 {
697 body.push_str(&musicxml_instrument_directive_lines(part));
698 }
699 body.push_str(&midi_directive_lines(voice));
700 if part_index == 0
701 && voice_index == 0
702 && let Some(instruction) = initial_tempo_instruction(score)
703 {
704 body.push_str(&format!("[I:{instruction}] "));
705 }
706 if let Some(key) = &voice.initial_key
707 && let Some(instruction) = initial_key_carrier(score, key)
708 {
709 body.push_str(&format!("[I:{instruction}] "));
710 }
711 if let Some(meter) = &voice.initial_meter
712 && let Some(instruction) = initial_meter_carrier(score, meter)
713 {
714 body.push_str(&format!("[I:{instruction}] "));
715 }
716 body.push_str(&write_voice(voice, unit, voice_global_index, &xvoice));
717 voice_global_index += 1;
718 }
719 }
720 body
721}
722
723fn voice_header_line(voice: &crate::model::Voice) -> String {
726 use crate::model::StemDirectionModel;
727 let p = &voice.properties;
728 let mut s = format!("V:{}", voice.id.value);
729 for (key, value) in [
730 ("name", &p.name),
731 ("nm", &p.nm),
732 ("subname", &p.subname),
733 ("snm", &p.snm),
734 ] {
735 if let Some(text) = value {
736 s.push_str(&format!(" {key}=\"{}\"", abc_carrier_quoted(&text.text)));
737 }
738 }
739 if let Some(clef) = &p.clef {
740 s.push_str(&format!(" clef={}", clef.text));
741 }
742 if let Some(stem) = &p.stem {
743 s.push_str(match stem {
744 StemDirectionModel::Up => " stem=up",
745 StemDirectionModel::Down => " stem=down",
746 });
747 }
748 if let Some(octave) = &p.octave {
749 s.push_str(&format!(" octave={}", octave.text));
750 }
751 if let Some(transpose) = &p.transpose {
752 s.push_str(&format!(" transpose={}", transpose.text));
753 }
754 if let Some(middle) = &p.middle {
755 s.push_str(&format!(" middle={}", middle.text));
756 }
757 s.push('\n');
758 s
759}
760
761fn midi_directive_lines(voice: &crate::model::Voice) -> String {
775 let mut s = String::new();
776 if let Some(midi) = &voice.midi_instrument {
777 match (midi.channel, midi.program) {
778 (Some(channel), Some(program)) => {
779 s.push_str(&format!("%%MIDI program {channel} {program}\n"));
780 }
781 (None, Some(program)) => s.push_str(&format!("%%MIDI program {program}\n")),
782 (Some(channel), None) => s.push_str(&format!("%%MIDI channel {channel}\n")),
783 (None, None) => {}
784 }
785 if let Some(volume) = midi.volume_cc {
786 s.push_str(&format!("%%MIDI control 7 {volume}\n"));
787 }
788 if let Some(pan) = midi.pan_cc {
789 s.push_str(&format!("%%MIDI control 10 {pan}\n"));
790 }
791 if let Some(unpitched) = midi.midi_unpitched {
792 s.push_str(&format!("%%MIDI midi-unpitched {unpitched}\n"));
793 }
794 }
795 if let Some(transpose) = voice.midi_transpose {
796 s.push_str(&format!("%%MIDI transpose {transpose}\n"));
797 }
798 s
799}
800
801fn musicxml_instrument_directive_lines(part: &crate::model::Part) -> String {
802 let mut s = String::new();
803 for instrument in &part.instruments {
804 if instrument.id.trim().is_empty() {
805 continue;
806 }
807 s.push_str("%%croma-musicxml-instrument");
808 s.push_str(&format!(
809 " id=\"{}\"",
810 abc_carrier_quoted(instrument.id.as_str())
811 ));
812 if let Some(name) = &instrument.name {
813 s.push_str(&format!(
814 " name=\"{}\"",
815 abc_carrier_quoted(name.text.as_str())
816 ));
817 }
818 if let Some(midi) = &instrument.midi {
819 if let Some(channel) = midi.channel {
820 s.push_str(&format!(" channel={channel}"));
821 }
822 if let Some(program) = midi.program {
823 s.push_str(&format!(" program={program}"));
824 }
825 if let Some(volume) = midi.volume_cc {
826 s.push_str(&format!(" volume-cc={volume}"));
827 }
828 if let Some(pan) = midi.pan_cc {
829 s.push_str(&format!(" pan-cc={pan}"));
830 }
831 if let Some(unpitched) = midi.midi_unpitched {
832 s.push_str(&format!(" midi-unpitched={unpitched}"));
833 }
834 }
835 s.push('\n');
836 }
837 s
838}
839
840fn abc_carrier_quoted(text: &str) -> String {
847 text.chars()
848 .map(|c| if c.is_control() { ' ' } else { c })
849 .collect::<String>()
850 .replace('\\', "\\\\")
851 .replace('"', "\\\"")
852}
853
854fn shifted(pitch: &Pitch, shift: i8) -> Pitch {
858 Pitch {
859 octave: pitch.octave.saturating_sub(shift),
860 ..*pitch
861 }
862}
863
864fn write_voice(
865 voice: &crate::model::Voice,
866 unit: Rational,
867 voice_global_index: usize,
868 xvoice: &XvoiceSlurMap,
869) -> String {
870 let mut out = String::new();
871 let shift = voice_octave_shift(&voice.properties);
872 let baseline_clef_shift = voice
878 .properties
879 .clef
880 .as_ref()
881 .map_or(0, |clef| i32::from(clef_octave_shift(clef.text.as_str())));
882 let baseline_octave_param = voice_octave_param(&voice.properties);
883 let mut effective_octave_param = baseline_octave_param;
884 let mut overlays: std::collections::BTreeMap<u32, Vec<&crate::model::OverlaySegment>> =
887 std::collections::BTreeMap::new();
888 for measure in &voice.measures {
889 for segment in &measure.overlays {
890 overlays
891 .entry(segment.measure_index)
892 .or_default()
893 .push(segment);
894 }
895 }
896 let (markers, scales) = tuplet_layout(&voice.events);
897 let measure_number_carriers = voice
898 .measures
899 .iter()
900 .filter_map(|measure| {
901 measure_number_carrier(measure).map(|carrier| (measure.id.index, carrier))
902 })
903 .collect::<std::collections::BTreeMap<_, _>>();
904 let mut joined: Vec<Option<&'static str>> = vec![None; voice.events.len()];
912 let mut skip = vec![false; voice.events.len()];
913 for i in 0..voice.events.len().saturating_sub(1) {
914 if skip[i] {
915 continue;
916 }
917 let (TimedEventKind::Barline(a), TimedEventKind::Barline(b)) =
918 (&voice.events[i].kind, &voice.events[i + 1].kind)
919 else {
920 continue;
921 };
922 if a.span != b.span {
923 continue;
924 }
925 match (a.kind, b.kind) {
926 (BarlineKind::Double, BarlineKind::RepeatStart) => {
927 joined[i] = Some("||:");
928 skip[i + 1] = true;
929 }
930 (BarlineKind::Initial, BarlineKind::RepeatStart) => {
931 joined[i] = Some("[|:");
932 skip[i + 1] = true;
933 }
934 _ => {}
935 }
936 }
937 let mut current_measure: Option<u32> = None;
938 let mut measure_event_seen = false;
939 for (idx, event) in voice.events.iter().enumerate() {
940 if skip[idx] {
941 continue;
942 }
943 let mut measure_carrier_after_barline = None;
944 let m = event.measure.index;
947 if current_measure != Some(m) {
948 if let Some(prev) = current_measure
949 && let Some(segments) = overlays.remove(&prev)
950 {
951 for segment in segments {
952 out.push_str(&overlay_str(segment, unit, shift));
953 }
954 }
955 let missing_start = current_measure.map_or(0, |prev| prev.saturating_add(1));
956 write_sparse_voice_measure_gaps(&mut out, missing_start, m);
957 current_measure = Some(m);
958 measure_event_seen = false;
959 if let Some(carrier) = measure_number_carriers.get(&m) {
960 if carrier_follows_leading_barline(&event.kind, joined[idx]) {
961 measure_carrier_after_barline = Some(carrier.as_str());
962 } else {
963 out.push_str(&format!("[I:{carrier}] "));
964 }
965 }
966 }
967 if measure_event_seen
970 && matches!(event.kind, TimedEventKind::Barline(_))
971 && let Some(segments) = overlays.remove(&m)
972 {
973 for segment in segments {
974 out.push_str(&overlay_str(segment, unit, shift));
975 }
976 }
977 measure_event_seen = true;
978 out.push_str(&markers[idx]);
981 let tuplet = scales[idx];
982 let xvoice_emits: &[XvoiceSlurEmit] = xvoice
986 .get(&(voice_global_index, idx))
987 .map_or(&[], Vec::as_slice);
988 out.push_str(&xvoice_slur_prefix(xvoice_emits));
989 let stripped_attachments;
990 let attachments: &EventAttachments = if xvoice_emits.is_empty() {
991 &event.attachments
992 } else {
993 stripped_attachments = strip_xvoice_slurs(&event.attachments, xvoice_emits);
994 &stripped_attachments
995 };
996 match &event.kind {
997 TimedEventKind::Note(note) => {
998 let written = shifted(¬e.pitch, shift);
999 out.push_str(&event_prefix(attachments));
1000 out.push_str(note_accidental(
1001 note.written_accidental.as_ref().map(|m| m.kind),
1002 ));
1003 out.push_str(&pitch_str(&written));
1004 out.push_str(&length_str(notated_duration(event.duration, tuplet), unit));
1005 out.push_str(&event_suffix(attachments));
1006 out.push(' ');
1007 }
1008 TimedEventKind::Rest(rest) => {
1009 out.push_str(&event_prefix(attachments));
1010 out.push(match rest.visibility {
1011 RestVisibility::Visible => 'z',
1012 RestVisibility::Invisible => 'x',
1013 });
1014 out.push_str(&length_str(notated_duration(event.duration, tuplet), unit));
1015 out.push_str(&event_suffix(attachments));
1016 out.push(' ');
1017 }
1018 TimedEventKind::Chord(chord) => {
1019 let mut merged = attachments.clone();
1027 merged.ties.clear();
1028 for member in &chord.members {
1029 for slur in &member.attachments.slurs {
1030 if !merged
1031 .slurs
1032 .iter()
1033 .any(|x| x.pair_id == slur.pair_id && x.role == slur.role)
1034 {
1035 merged.slurs.push(*slur);
1036 }
1037 }
1038 }
1039 out.push_str(&event_prefix(&merged));
1040 let lengths: Vec<String> = chord
1044 .members
1045 .iter()
1046 .map(|m| length_str(notated_duration(m.duration, tuplet), unit))
1047 .collect();
1048 let uniform = lengths.windows(2).all(|w| w[0] == w[1]);
1049 out.push('[');
1050 for (member_index, (member, len)) in chord.members.iter().zip(&lengths).enumerate()
1051 {
1052 let written = shifted(&member.pitch, shift);
1053 out.push_str(&chord_member_prefix(
1054 &member.attachments,
1055 (member_index == 0).then_some(&merged),
1056 ));
1057 out.push_str(note_accidental(
1058 member.written_accidental.as_ref().map(|m| m.kind),
1059 ));
1060 out.push_str(&pitch_str(&written));
1061 if !uniform {
1062 out.push_str(len);
1063 }
1064 if member
1065 .attachments
1066 .ties
1067 .iter()
1068 .any(|t| t.role == TieRole::Start)
1069 {
1070 out.push('-');
1071 }
1072 }
1073 out.push(']');
1074 if let Some(len) = lengths.first().filter(|_| uniform) {
1075 out.push_str(len);
1076 }
1077 out.push_str(&event_suffix(&merged));
1078 out.push(' ');
1079 }
1080 TimedEventKind::Barline(b) => {
1081 if let Some(instruction) = barline_style_instruction(b.kind) {
1082 out.push_str(&format!("[I:{instruction}] "));
1083 }
1084 out.push_str(joined[idx].unwrap_or_else(|| barline_str(b.kind)));
1085 out.push(' ');
1086 if let Some(carrier) = measure_carrier_after_barline {
1087 out.push_str(&format!("[I:{carrier}] "));
1088 }
1089 }
1090 TimedEventKind::RepeatEnding(r) => {
1091 out.push_str(&ending_str(r));
1092 out.push(' ');
1093 }
1094 TimedEventKind::RepeatEndingClose(close) => {
1095 if let Some(instruction) = ending_close_instruction(close) {
1096 out.push_str(&format!("[I:{instruction}] "));
1097 }
1098 }
1099 TimedEventKind::Spacer => {
1100 out.push_str(&event_prefix(attachments));
1101 out.push('y');
1102 out.push_str(&event_suffix(attachments));
1103 out.push(' ');
1104 }
1105 TimedEventKind::KeyChange(key) => {
1110 if key.preserve_restatement {
1111 out.push_str(&format!("[I:{}] ", key_restatement_instruction()));
1112 }
1113 out.push_str(&format!("[K:{}] ", key.display));
1114 }
1115 TimedEventKind::MeterChange(meter) => {
1116 if let Some(instruction) = time_symbol_instruction(meter) {
1117 out.push_str(&format!("[I:{instruction}] "));
1118 }
1119 if meter.preserve_restatement {
1120 out.push_str(&format!("[I:{}] ", meter_restatement_instruction()));
1121 }
1122 out.push_str(&format!("[M:{}] ", meter.display));
1123 }
1124 TimedEventKind::ClefChange(clef) => {
1125 if let Some(instruction) = clef_cursor_instruction(clef) {
1129 out.push_str(&format!("[I:{instruction}] "));
1130 } else if let Some(text) = inline_clef_field_text(clef) {
1131 let required = baseline_octave_param + baseline_clef_shift
1136 - i32::from(clef_octave_shift(text));
1137 if required == effective_octave_param {
1138 out.push_str(&format!("[K:clef={text}] "));
1139 } else if (-9..=9).contains(&required) {
1140 out.push_str(&format!("[K:clef={text} octave={required}] "));
1141 effective_octave_param = required;
1142 }
1143 }
1147 }
1148 TimedEventKind::TempoChange(tempo) => {
1149 if tempo.beat_role == TempoBeatRole::PlaybackSoundOnly
1150 && let Some(instruction) = sound_tempo_instruction(tempo)
1151 {
1152 out.push_str(&format!("[I:{instruction}] "));
1153 } else if tempo_needs_instruction(tempo)
1154 && let Some(instruction) = tempo_instruction(tempo)
1155 {
1156 out.push_str(&format!("[I:{instruction}] "));
1157 } else {
1158 out.push_str(&format!("[Q:{}] ", tempo_display(tempo)));
1159 }
1160 }
1161 TimedEventKind::SectionLabel(label) => {
1162 let label = label
1169 .chars()
1170 .map(|c| if c.is_control() { ' ' } else { c })
1171 .collect::<String>();
1172 let label = label.trim();
1173 if label.contains('[') || label.contains(']') {
1182 let trimmed = out.trim_end_matches(' ').to_owned();
1183 out.clear();
1184 if !trimmed.is_empty() {
1185 out.push_str(&trimmed);
1186 out.push('\n');
1187 }
1188 out.push_str(&format!(
1189 "P:{}\n",
1190 crate::text::encode_text_string(label, false)
1191 ));
1192 } else {
1193 out.push_str(&format!(
1194 "[P:{}] ",
1195 crate::text::encode_text_string(label, false)
1196 ));
1197 }
1198 }
1199 }
1200 }
1201 for (_index, segments) in std::mem::take(&mut overlays) {
1203 for segment in segments {
1204 out.push_str(&overlay_str(segment, unit, shift));
1205 }
1206 }
1207 let mut body = format!("{}\n", out.trim_end());
1208 let align_events = lyric_align_events(voice);
1212 for line in lyric_lines(&align_events) {
1213 body.push_str(&line);
1214 body.push('\n');
1215 }
1216 for line in symbol_lines(&align_events) {
1217 body.push_str(&line);
1218 body.push('\n');
1219 }
1220 body
1221}
1222
1223fn write_sparse_voice_measure_gaps(out: &mut String, start: u32, end: u32) {
1228 for _ in start..end {
1229 out.push_str("y | ");
1230 }
1231}
1232
1233type TupletMarkers = Vec<String>;
1235type TupletScales = Vec<Option<TupletScale>>;
1237
1238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1239enum TupletScale {
1240 Ratio(u32, u32),
1241 Overflow,
1242}
1243
1244fn tuplet_layout(events: &[crate::TimedEvent]) -> (TupletMarkers, TupletScales) {
1254 use std::collections::BTreeMap;
1255 let mut groups: BTreeMap<u32, (u32, u32, usize, usize)> = BTreeMap::new();
1257 for (i, event) in events.iter().enumerate() {
1258 for tuplet in &event.attachments.tuplets {
1259 let entry = groups.entry(tuplet.pair_id).or_insert((
1260 tuplet.actual_notes,
1261 tuplet.normal_notes,
1262 i,
1263 i,
1264 ));
1265 entry.0 = tuplet.actual_notes;
1266 entry.1 = tuplet.normal_notes;
1267 entry.2 = entry.2.min(i);
1268 entry.3 = entry.3.max(i);
1269 }
1270 }
1271 let mut markers = vec![String::new(); events.len()];
1272 let mut scales = vec![None; events.len()];
1273 for (_pid, (actual, normal, start, stop)) in groups {
1274 if !abc_tuplet_actual_supported(actual) {
1275 continue;
1276 }
1277 let span = events[start..=stop]
1280 .iter()
1281 .filter(|e| {
1282 matches!(
1283 e.kind,
1284 TimedEventKind::Note(_) | TimedEventKind::Rest(_) | TimedEventKind::Chord(_)
1285 )
1286 })
1287 .count();
1288 markers[start].push_str(&format!("({actual}:{normal}:{span}"));
1289 for slot in scales.iter_mut().take(stop + 1).skip(start) {
1290 multiply_tuplet_scale(slot, actual, normal);
1291 }
1292 }
1293 (markers, scales)
1294}
1295
1296fn multiply_tuplet_scale(slot: &mut Option<TupletScale>, actual: u32, normal: u32) {
1297 match slot {
1298 Some(TupletScale::Ratio(active_actual, active_normal)) => {
1299 *slot =
1300 crate::model::checked_ratio_product(*active_actual, *active_normal, actual, normal)
1301 .map(|(actual, normal)| TupletScale::Ratio(actual, normal))
1302 .or(Some(TupletScale::Overflow));
1303 }
1304 Some(TupletScale::Overflow) => {}
1305 None => *slot = Some(TupletScale::Ratio(actual, normal)),
1306 }
1307}
1308
1309fn event_prefix(attachments: &crate::EventAttachments) -> String {
1317 use crate::model::SlurRole;
1318 let mut out = String::new();
1319 for grace in &attachments.grace_groups {
1323 for slur in &grace.slurs {
1326 if slur.role == SlurRole::Start {
1327 out.push('(');
1328 }
1329 }
1330 out.push_str(&grace_str(grace));
1335 }
1336 if let Some(instrument) = &attachments.instrument {
1337 out.push_str(&format!(
1338 "[I:croma-note-instrument id=\"{}\"]",
1339 abc_carrier_quoted(instrument.id.as_str())
1340 ));
1341 }
1342 if attachments.musicxml_forward {
1343 out.push_str(&format!("[I:{}]", musicxml_forward_instruction()));
1344 }
1345 if let Some(duration) = attachments.musicxml_sequence_backup {
1346 out.push_str(&format!(
1347 "[I:{}]",
1348 musicxml_sequence_backup_instruction(duration)
1349 ));
1350 }
1351 let mut musicxml_tuplets: Vec<_> = attachments
1352 .tuplets
1353 .iter()
1354 .copied()
1355 .filter(|tuplet| !abc_tuplet_actual_supported(tuplet.actual_notes))
1356 .collect();
1357 musicxml_tuplets.sort_by_key(|tuplet| {
1358 let role_order = match tuplet.role {
1359 TupletRole::Start => 0u8,
1360 TupletRole::Continue => 1,
1361 TupletRole::Stop => 2,
1362 };
1363 (tuplet.pair_id, role_order)
1364 });
1365 for tuplet in musicxml_tuplets {
1366 out.push_str(&format!("[I:{}]", musicxml_tuplet_instruction(tuplet)));
1367 }
1368 let mut primary_lyric_verses = Vec::new();
1369 for lyric in &attachments.lyrics {
1370 if lyric.control != crate::model::LyricControl::Syllable {
1371 continue;
1372 }
1373 if primary_lyric_verses.contains(&lyric.verse) {
1374 out.push_str(&format!("[I:{}]", lyric_duplicate_instruction(lyric)));
1375 } else {
1376 primary_lyric_verses.push(lyric.verse);
1377 if lyric.same_note_extend {
1378 out.push_str(&format!("[I:{}]", lyric_extend_instruction(lyric.verse)));
1379 }
1380 }
1381 }
1382 for slur in &attachments.slurs {
1386 if slur.role == SlurRole::Start {
1387 out.push('(');
1388 }
1389 }
1390 for chord_symbol in &attachments.chord_symbols {
1395 if let Some(instruction) = harmony_text_instruction(&chord_symbol.musicxml_harmony_text) {
1396 out.push_str(&format!("[I:{instruction}]"));
1397 }
1398 out.push_str("ed_str(&chord_symbol.text));
1399 }
1400 for symbol in &attachments.symbols {
1407 if symbol.kind == crate::model::AlignedSymbolKind::ChordSymbol {
1408 out.push_str("ed_str(&symbol.text));
1409 }
1410 }
1411 for annotation in &attachments.annotations {
1412 out.push_str("ed_text_string(&annotation.text));
1413 }
1414 for deco in &attachments.decorations {
1415 if let Some(placement) = deco.placement {
1416 out.push_str(&format!(
1417 "[I:{}]",
1418 direction_placement_instruction(placement)
1419 ));
1420 }
1421 out.push_str(&decoration_str(&deco.name));
1422 }
1423 out
1424}
1425
1426fn quoted_str(text: &str) -> String {
1430 format!("\"{text}\"")
1431}
1432
1433fn quoted_text_string(text: &str) -> String {
1436 format!("\"{}\"", crate::text::encode_text_string(text, true))
1437}
1438
1439#[derive(Debug, Clone, Copy)]
1451struct LyricAlignEvent<'a> {
1452 source_start: usize,
1453 attachments: &'a EventAttachments,
1454}
1455
1456fn lyric_align_events(voice: &crate::model::Voice) -> Vec<LyricAlignEvent<'_>> {
1457 let mut alignable = Vec::new();
1458 for event in &voice.events {
1459 if matches!(
1460 event.kind,
1461 TimedEventKind::Note(_) | TimedEventKind::Chord(_)
1462 ) {
1463 alignable.push(LyricAlignEvent {
1464 source_start: event.source.start,
1465 attachments: &event.attachments,
1466 });
1467 }
1468 }
1469 for measure in &voice.measures {
1470 for segment in &measure.overlays {
1471 for event in &segment.events {
1472 if event.alignable {
1473 alignable.push(LyricAlignEvent {
1474 source_start: event.span.start,
1475 attachments: &event.attachments,
1476 });
1477 }
1478 }
1479 }
1480 }
1481 alignable.sort_by_key(|event| event.source_start);
1482 alignable
1483}
1484
1485fn lyric_lines(events: &[LyricAlignEvent<'_>]) -> Vec<String> {
1486 use crate::model::LyricControl;
1487 let total = events.len();
1488 let mut verses: std::collections::BTreeMap<u32, Vec<Option<String>>> =
1489 std::collections::BTreeMap::new();
1490 let mut max_verse = 0u32;
1491 for (pos, event) in events.iter().enumerate() {
1492 let mut syllable_verses = std::collections::BTreeSet::new();
1495 for lyric in &event.attachments.lyrics {
1496 max_verse = max_verse.max(lyric.verse);
1497 let slot = &mut verses
1498 .entry(lyric.verse)
1499 .or_insert_with(|| vec![None; total])[pos];
1500 match lyric.control {
1501 LyricControl::Syllable => {
1502 if syllable_verses.insert(lyric.verse) {
1503 slot.get_or_insert_with(String::new)
1504 .push_str(&lyric_escape(&lyric.text));
1505 }
1506 }
1507 LyricControl::Hyphen => {
1508 let token = slot.get_or_insert_with(String::new);
1513 if !token.ends_with('-') {
1514 token.push('-');
1515 }
1516 }
1517 LyricControl::Extender => *slot = Some("_".to_string()),
1518 LyricControl::Skip => {}
1521 }
1522 }
1523 }
1524 if verses.is_empty() {
1525 return Vec::new();
1526 }
1527 (1..=max_verse)
1528 .map(|v| match verses.get(&v) {
1529 Some(tokens) => {
1530 let body: Vec<String> = tokens
1531 .iter()
1532 .map(|t| match t {
1533 Some(tok) if tok.chars().all(|c| c == '-') => "*".to_string(),
1538 Some(tok) => tok.clone(),
1539 None => "*".to_string(),
1540 })
1541 .collect();
1542 format!("w:{}", body.join(" "))
1543 }
1544 None => "w:*".to_string(),
1546 })
1547 .collect()
1548}
1549
1550fn lyric_escape(text: &str) -> String {
1553 let mut out = String::with_capacity(text.len());
1554 let mut chars = text.chars().peekable();
1555 while let Some(c) = chars.next() {
1556 match c {
1557 c if c.is_ascii_whitespace() || c.is_ascii_control() => out.push('~'),
1563 '\\' | '-' | '*' | '_' | '|' | '~' | '%' => {
1564 out.push('\\');
1565 out.push(c);
1566 }
1567 '&' if chars
1569 .peek()
1570 .is_some_and(|next| next.is_ascii_alphabetic() || *next == '#') =>
1571 {
1572 out.push_str("\\&");
1573 }
1574 _ => out.push(c),
1575 }
1576 }
1577 out
1578}
1579
1580fn symbol_lines(events: &[LyricAlignEvent<'_>]) -> Vec<String> {
1591 use crate::model::AlignedSymbolKind;
1592 let mut layers: std::collections::BTreeMap<u32, Vec<Option<String>>> =
1594 std::collections::BTreeMap::new();
1595 let mut position = 0usize;
1596 let total = events.len();
1597 for event in events {
1598 for symbol in &event.attachments.symbols {
1601 let token = match symbol.kind {
1602 AlignedSymbolKind::ChordSymbol => continue, AlignedSymbolKind::Decoration => format!("!{}!", symbol.text),
1604 AlignedSymbolKind::Annotation => quoted_text_string(&symbol.text),
1605 AlignedSymbolKind::Raw => symbol.text.clone(),
1606 };
1607 layers
1608 .entry(symbol.layer)
1609 .or_insert_with(|| vec![None; total])[position] = Some(token);
1610 }
1611 position += 1;
1612 }
1613 layers
1614 .into_values()
1615 .map(|tokens| {
1616 let last = tokens
1617 .iter()
1618 .rposition(Option::is_some)
1619 .map_or(0, |i| i + 1);
1620 let body: Vec<String> = tokens[..last]
1621 .iter()
1622 .map(|t| t.clone().unwrap_or_else(|| "*".to_string()))
1623 .collect();
1624 format!("s:{}", body.join(" "))
1625 })
1626 .filter(|line| line != "s:")
1627 .collect()
1628}
1629
1630fn decoration_str(name: &str) -> String {
1635 if name == "." {
1636 ".".to_string()
1637 } else {
1638 format!("!{name}!")
1639 }
1640}
1641
1642fn chord_member_prefix(
1646 attachments: &crate::EventAttachments,
1647 event_attachments: Option<&crate::EventAttachments>,
1648) -> String {
1649 let mut out = String::new();
1650 for decoration in &attachments.decorations {
1651 if event_attachments.is_some_and(|event_attachments| {
1652 event_attachments
1653 .decorations
1654 .iter()
1655 .any(|event_decoration| event_decoration.name == decoration.name)
1656 }) {
1657 continue;
1658 }
1659 if let Some(placement) = decoration.placement {
1660 out.push_str(&format!(
1661 "[I:{}]",
1662 direction_placement_instruction(placement)
1663 ));
1664 }
1665 out.push_str(&decoration_str(&decoration.name));
1666 }
1667 out
1668}
1669
1670fn event_suffix(attachments: &crate::EventAttachments) -> String {
1673 use crate::model::SlurRole;
1674 let mut out = String::new();
1675 if attachments.ties.iter().any(|t| t.role == TieRole::Start) {
1676 out.push('-');
1677 }
1678 for slur in &attachments.slurs {
1679 if slur.role == SlurRole::Stop {
1680 out.push(')');
1681 }
1682 }
1683 for grace in &attachments.after_grace_groups {
1684 for slur in &grace.slurs {
1685 if slur.role == SlurRole::Start {
1686 out.push('(');
1687 }
1688 }
1689 out.push_str(&format!("[I:{}]", musicxml_after_grace_instruction()));
1690 out.push_str(&grace_str(grace));
1691 }
1692 out
1693}
1694
1695fn ending_str(model: &crate::model::RepeatEndingModel) -> String {
1697 use crate::model::RepeatEndingPartModel::{Range, Single, Text};
1698 let parts: Vec<String> = model
1699 .endings
1700 .iter()
1701 .map(|p| match p {
1702 Single(n) => n.to_string(),
1703 Range { start, end } => format!("{start}-{end}"),
1704 Text(text) => format!("\"{}\"", escape_abc_quotes(text)),
1705 })
1706 .collect();
1707 format!("[{}", parts.join(","))
1708}
1709
1710fn escape_abc_quotes(text: &str) -> String {
1711 text.replace('\\', "\\\\").replace('"', "\\\"")
1712}
1713
1714fn barline_str(kind: BarlineKind) -> &'static str {
1716 match kind {
1717 BarlineKind::Regular => "|",
1718 BarlineKind::Double => "||",
1719 BarlineKind::Final => "|]",
1720 BarlineKind::RepeatStart => "|:",
1721 BarlineKind::RepeatEnd => ":|",
1722 BarlineKind::RepeatBoth => "::",
1723 BarlineKind::Dotted => ".|",
1724 BarlineKind::Dashed => "|",
1725 BarlineKind::Invisible => "[|]",
1726 BarlineKind::Initial => "|",
1731 BarlineKind::Liberal => "|||",
1737 }
1738}
1739
1740fn note_accidental(written: Option<Accidental>) -> &'static str {
1746 written.map(Accidental::abc_sign).unwrap_or("")
1747}
1748
1749fn overlay_str(segment: &crate::model::OverlaySegment, unit: Rational, shift: i8) -> String {
1752 use crate::model::TimelineEventKind;
1753 let mut out = String::from("& ");
1754 let events = &segment.events;
1755 let (ov_markers, ov_scales) = overlay_tuplet_layout(events);
1756 let mut i = 0;
1757 while i < events.len() {
1758 let event = &events[i];
1759 out.push_str(&ov_markers[i]);
1760 let tuplet = ov_scales[i];
1761 match &event.kind {
1762 TimelineEventKind::Note { .. } => {
1763 let mut end = i + 1;
1764 while end < events.len() && overlay_same_chord(event, &events[end]) {
1765 end += 1;
1766 }
1767 let group = &events[i..end];
1768 let chord = group.len() > 1;
1769 let mut lead = event.attachments.clone();
1770 if chord {
1771 lead.ties.clear();
1772 }
1773 out.push_str(&event_prefix(&lead));
1774 let lengths: Vec<String> = group
1775 .iter()
1776 .map(|e| length_str(notated_duration(e.duration, tuplet), unit))
1777 .collect();
1778 let uniform = lengths.windows(2).all(|w| w[0] == w[1]);
1779 if chord {
1780 out.push('[');
1781 }
1782 for (e, len) in group.iter().zip(&lengths) {
1783 let TimelineEventKind::Note {
1784 step,
1785 octave,
1786 accidental,
1787 ..
1788 } = &e.kind
1789 else {
1790 continue;
1791 };
1792 out.push_str(note_accidental(*accidental));
1793 out.push_str(&pitch_letter_str(*step, *octave - shift));
1794 if !chord || !uniform {
1795 out.push_str(len);
1796 }
1797 if chord && e.attachments.ties.iter().any(|t| t.role == TieRole::Start) {
1798 out.push('-');
1799 }
1800 }
1801 if chord {
1802 out.push(']');
1803 if let Some(len) = lengths.first().filter(|_| uniform) {
1804 out.push_str(len);
1805 }
1806 }
1807 out.push_str(&event_suffix(&lead));
1808 out.push(' ');
1809 i = end;
1810 continue;
1811 }
1812 TimelineEventKind::Rest { visibility, .. } => {
1813 out.push_str(&event_prefix(&event.attachments));
1814 out.push(match visibility {
1815 RestVisibility::Visible => 'z',
1816 RestVisibility::Invisible => 'x',
1817 });
1818 out.push_str(&length_str(notated_duration(event.duration, tuplet), unit));
1819 out.push_str(&event_suffix(&event.attachments));
1820 out.push(' ');
1821 }
1822 TimelineEventKind::Spacer => {
1823 out.push_str(&event_prefix(&event.attachments));
1824 out.push('y');
1825 out.push_str(&event_suffix(&event.attachments));
1826 out.push(' ');
1827 }
1828 TimelineEventKind::Barline { kind } => {
1829 out.push_str(barline_str(*kind));
1830 out.push(' ');
1831 }
1832 TimelineEventKind::VariantEnding { .. }
1833 | TimelineEventKind::VariantEndingClose { .. }
1834 | TimelineEventKind::KeyChange(_)
1835 | TimelineEventKind::MeterChange(_)
1836 | TimelineEventKind::ClefChange(_)
1837 | TimelineEventKind::TempoChange(_)
1838 | TimelineEventKind::SectionLabel(_) => {}
1839 }
1840 i += 1;
1841 }
1842 out
1843}
1844
1845fn overlay_tuplet_layout(
1849 events: &[crate::model::VoiceTimedEvent],
1850) -> (TupletMarkers, TupletScales) {
1851 use crate::model::{TimelineEventKind, TupletRole};
1852 use std::collections::BTreeMap;
1853 let mut groups: BTreeMap<u32, (u32, u32, usize, usize, bool)> = BTreeMap::new();
1854 for (i, event) in events.iter().enumerate() {
1855 for tuplet in &event.attachments.tuplets {
1856 let entry = groups.entry(tuplet.pair_id).or_insert((
1857 tuplet.actual_notes,
1858 tuplet.normal_notes,
1859 i,
1860 i,
1861 false,
1862 ));
1863 entry.0 = tuplet.actual_notes;
1864 entry.1 = tuplet.normal_notes;
1865 entry.2 = entry.2.min(i);
1866 entry.3 = entry.3.max(i);
1867 if tuplet.role == TupletRole::Start {
1868 entry.4 = true;
1869 }
1870 }
1871 }
1872 let mut markers = vec![String::new(); events.len()];
1873 let mut scales = vec![None; events.len()];
1874 for (_pid, (actual, normal, start, stop, has_start)) in groups {
1875 if !has_start || !abc_tuplet_actual_supported(actual) {
1882 continue;
1883 }
1884 let span = events[start..=stop]
1885 .iter()
1886 .filter(|e| {
1887 matches!(
1888 e.kind,
1889 TimelineEventKind::Note { .. } | TimelineEventKind::Rest { .. }
1890 )
1891 })
1892 .count();
1893 markers[start].push_str(&format!("({actual}:{normal}:{span}"));
1894 for slot in scales.iter_mut().take(stop + 1).skip(start) {
1895 multiply_tuplet_scale(slot, actual, normal);
1896 }
1897 }
1898 (markers, scales)
1899}
1900
1901fn overlay_same_chord(
1904 first: &crate::model::VoiceTimedEvent,
1905 next: &crate::model::VoiceTimedEvent,
1906) -> bool {
1907 use crate::model::TimelineEventKind;
1908 first.source_order == next.source_order
1909 && first.onset == next.onset
1910 && matches!(next.kind, TimelineEventKind::Note { chord: true, .. })
1911}
1912
1913fn pitch_str(pitch: &Pitch) -> String {
1915 pitch_letter_str(pitch.step, pitch.octave)
1916}
1917
1918fn pitch_letter_str(step: char, octave: i8) -> String {
1921 let letter = step.to_ascii_uppercase();
1922 let octave = i32::from(octave);
1927 if octave >= 5 {
1928 let mut s = letter.to_ascii_lowercase().to_string();
1929 s.push_str(&"'".repeat((octave - 5) as usize));
1930 s
1931 } else {
1932 let mut s = letter.to_string();
1933 s.push_str(&",".repeat((4 - octave) as usize));
1934 s
1935 }
1936}
1937
1938fn length_str(duration: Rational, unit: Rational) -> String {
1940 let mult = Rational::new(
1941 duration.numerator.saturating_mul(unit.denominator),
1942 duration.denominator.saturating_mul(unit.numerator),
1943 );
1944 length_ratio_str(mult)
1945}
1946
1947fn notated_duration(duration: Rational, tuplet: Option<TupletScale>) -> Rational {
1952 match tuplet {
1953 Some(TupletScale::Ratio(actual, normal)) => {
1954 scaled_rational(duration, actual, normal).unwrap_or(duration)
1955 }
1956 Some(TupletScale::Overflow) => duration,
1957 None => duration,
1958 }
1959}
1960
1961fn scaled_rational(duration: Rational, actual: u32, normal: u32) -> Option<Rational> {
1962 let numerator = u64::from(duration.numerator) * u64::from(actual);
1963 let denominator = u64::from(duration.denominator) * u64::from(normal);
1964 crate::model::ratio_to_u32(numerator, denominator)
1965 .map(|(numerator, denominator)| Rational::new(numerator, denominator))
1966}
1967
1968fn length_ratio_str(mult: Rational) -> String {
1970 match (mult.numerator, mult.denominator) {
1971 (1, 1) => String::new(),
1972 (n, 1) => n.to_string(),
1973 (1, 2) => "/".to_string(),
1974 (1, d) => format!("/{d}"),
1975 (n, d) => format!("{n}/{d}"),
1976 }
1977}
1978
1979fn grace_str(group: &crate::model::GraceGroupAttachment) -> String {
1982 use crate::model::{GraceEventKind, SlurRole};
1983 let mut out = String::from("{");
1984 let mut trailing_slur_stops = String::new();
1985 if group.slash.is_some() {
1986 out.push('/');
1987 }
1988 for grace in &group.events {
1989 for slur in &grace.slurs {
1990 if slur.role == SlurRole::Start {
1991 out.push('(');
1992 }
1993 }
1994 match &grace.kind {
1995 GraceEventKind::Note(note) => {
1996 for deco in ¬e.decorations {
1997 out.push_str(&decoration_str(&deco.name));
1998 }
1999 out.push_str(note_accidental(
2000 note.written_accidental.as_ref().map(|m| m.kind),
2001 ));
2002 out.push_str(&pitch_str(¬e.pitch));
2003 out.push_str(&length_ratio_str(note.length_multiplier));
2004 }
2005 GraceEventKind::Rest(_) => out.push('z'),
2006 GraceEventKind::Chord(members) => {
2007 out.push('[');
2008 for note in members {
2009 for deco in ¬e.decorations {
2010 out.push_str(&decoration_str(&deco.name));
2011 }
2012 out.push_str(note_accidental(
2013 note.written_accidental.as_ref().map(|m| m.kind),
2014 ));
2015 out.push_str(&pitch_str(¬e.pitch));
2016 out.push_str(&length_ratio_str(note.length_multiplier));
2017 }
2018 out.push(']');
2019 }
2020 }
2021 for slur in &grace.slurs {
2022 if slur.role == SlurRole::Stop {
2023 if slur.span.start >= group.span.end {
2024 trailing_slur_stops.push(')');
2025 } else {
2026 out.push(')');
2027 }
2028 }
2029 }
2030 }
2031 out.push('}');
2032 out.push_str(&trailing_slur_stops);
2033 out
2034}
2035
2036#[cfg(test)]
2037#[path = "to_abc_tests.rs"]
2038mod tests;