1use std::collections::BTreeMap;
9use std::fmt::Write as _;
10
11use super::read::class_sort_order;
12use crate::articulations::{Articulation, ArticulationKind, Finger};
13use crate::bar::{Barline, BarlineType, Ending, RepeatDirection};
14use crate::chord::Chord;
15use crate::chordsymbol::ChordSymbol;
16use crate::clef::Clef;
17use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
18use crate::duration::{Duration, TupletType, limited_fraction};
19use crate::error::{Error, Result};
20use crate::expressions::{ArpeggioType, Expression, FermataType, OrnamentKind};
21use crate::key::KeySignature;
22use crate::metadata::Metadata;
23use crate::meter::TimeSignature;
24use crate::notation::{BeamType, Beams, Lyric, Placement, TieType};
25use crate::note::Note;
26use crate::pitch::Pitch;
27use crate::repeat::RepeatExpressionKind;
28use crate::rest::Rest;
29use crate::spanner::SpannerKind;
30use crate::stream::{Stream, StreamElement, StreamKind};
31use crate::tempo::MetronomeMark;
32
33fn abc_error(message: impl Into<String>) -> Error {
34 Error::abc(message.into())
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39#[non_exhaustive]
40pub struct ExportOptions {
41 pub unit_length: Option<(UnsignedIntegerType, UnsignedIntegerType)>,
45 pub measures_per_line: usize,
47}
48
49impl Default for ExportOptions {
50 fn default() -> Self {
51 Self {
52 unit_length: None,
53 measures_per_line: 4,
54 }
55 }
56}
57
58pub fn to_abc(stream: &Stream, options: &ExportOptions) -> Result<String> {
102 if stream.kind() == StreamKind::Opus {
103 let mut tunes = Vec::new();
104 for (index, event) in stream.events().iter().enumerate() {
105 let score = event
106 .element()
107 .as_stream()
108 .ok_or_else(|| abc_error("an opus holds something that is not a score"))?;
109 tunes.push(Tune::new(score, index as IntegerType + 1)?.write(options)?);
110 }
111 return Ok(tunes.join("\n"));
112 }
113 Tune::new(stream, 1)?.write(options)
114}
115
116const EPSILON: FloatType = 1e-6;
117
118fn fraction(value: FloatType) -> Result<(i128, i128)> {
120 limited_fraction(value, 4096)
121 .filter(|(numerator, denominator)| {
122 (*numerator as FloatType / *denominator as FloatType - value).abs() < 1e-7
123 })
124 .ok_or_else(|| {
125 abc_error(format!(
126 "ABC cannot write a length of {value} quarter notes"
127 ))
128 })
129}
130
131fn length_text(written: FloatType, unit: FloatType) -> Result<String> {
133 let (numerator, denominator) = fraction(written / unit)?;
134 Ok(match (numerator, denominator) {
135 (1, 1) => String::new(),
136 (numerator, 1) => numerator.to_string(),
137 (1, 2) => "/".to_string(),
138 (1, denominator) => format!("/{denominator}"),
139 (numerator, denominator) => format!("{numerator}/{denominator}"),
140 })
141}
142
143fn plain(text: &str) -> String {
145 text.replace(['\n', '\r'], " ").trim().to_string()
146}
147
148fn quoted(text: &str) -> String {
150 plain(text).replace('"', "'")
151}
152
153type Alters = [IntegerType; 7];
155
156fn letter_index(letter: char) -> usize {
157 match letter {
158 'C' => 0,
159 'D' => 1,
160 'E' => 2,
161 'F' => 3,
162 'G' => 4,
163 'A' => 5,
164 _ => 6,
165 }
166}
167
168fn alters_of(signature: &KeySignature) -> Alters {
169 let mut alters = [0; 7];
170 for pitch in signature.altered_pitches().unwrap_or_default() {
171 alters[letter_index(pitch.step().as_char())] = pitch.alter().round() as IntegerType;
172 }
173 alters
174}
175
176fn signature_of(element: &StreamElement) -> Option<KeySignature> {
178 match element {
179 StreamElement::Key(key) => Some(key.key_signature()),
180 StreamElement::KeySignature(signature) => Some(signature.clone()),
181 _ => None,
182 }
183}
184
185fn clef_name(clef: &Clef) -> Result<String> {
187 let mut name = match (clef.sign(), clef.line()) {
188 (Some("G"), Some(2) | None) => "treble".to_string(),
189 (Some("G"), Some(line)) => format!("treble{line}"),
190 (Some("F"), Some(4) | None) => "bass".to_string(),
191 (Some("F"), Some(line)) => format!("bass{line}"),
192 (Some("C"), Some(3) | None) => "alto".to_string(),
193 (Some("C"), Some(4)) => "tenor".to_string(),
194 (Some("C"), Some(line)) => format!("alto{line}"),
195 (Some("percussion"), _) => "perc".to_string(),
196 (Some("none"), _) | (None, _) => "none".to_string(),
197 (Some(sign), _) => {
198 return Err(abc_error(format!("ABC has no {sign} clef")));
199 }
200 };
201 match clef.octave_change() {
202 0 => {}
203 1 => name.push_str("+8"),
204 -1 => name.push_str("-8"),
205 other => {
206 return Err(abc_error(format!(
207 "ABC has no clef moved by {other} octaves"
208 )));
209 }
210 }
211 Ok(name)
212}
213
214fn key_field(element: &StreamElement, clef: Option<&Clef>) -> Result<String> {
217 let clef = clef.map(clef_name).transpose()?;
218 let mode = match element {
219 StreamElement::Key(key) => match key.mode() {
220 "major" => Some(""),
221 "minor" => Some("m"),
222 "dorian" => Some("dor"),
223 "phrygian" => Some("phr"),
224 "lydian" => Some("lyd"),
225 "mixolydian" => Some("mix"),
226 "aeolian" => Some("aeo"),
227 "ionian" => Some("ion"),
228 "locrian" => Some("loc"),
229 _ => None,
230 },
231 _ => None,
232 };
233 let mut text = match (element, mode) {
234 (StreamElement::Key(key), Some(mode)) => {
235 let mode = if mode.is_empty() && clef.is_some() {
237 " maj"
238 } else {
239 mode
240 };
241 format!("{}{mode}", key.tonic_pitch().name())
242 }
243 _ => {
244 let signature = signature_of(element)
247 .ok_or_else(|| abc_error("a key field written for what is no key"))?;
248 let tonic = if signature.is_non_traditional() {
250 "C".to_string()
251 } else {
252 let key = KeySignature::new(signature.sharps()).as_key("major");
253 key.tonic_pitch().name()
254 };
255 let mut text = format!("{tonic} exp");
256 for pitch in signature.altered_pitches()? {
257 let _ = write!(
258 text,
259 " {}{}",
260 accidental_marks(pitch.alter().round() as IntegerType)?,
261 pitch.step().as_char().to_ascii_lowercase()
262 );
263 }
264 text
265 }
266 };
267 if let Some(clef) = clef {
268 let _ = write!(text, " clef={clef}");
269 }
270 Ok(text)
271}
272
273fn accidental_marks(alter: IntegerType) -> Result<&'static str> {
274 Ok(match alter {
275 -2 => "__",
276 -1 => "_",
277 0 => "=",
278 1 => "^",
279 2 => "^^",
280 other => {
281 return Err(abc_error(format!(
282 "ABC has no accidental moving a note by {other} semitones"
283 )));
284 }
285 })
286}
287
288fn meter_field(meter: &TimeSignature) -> String {
289 match meter.symbol() {
290 Some("common") => "C".to_string(),
291 Some("cut") => "C|".to_string(),
292 _ => meter.ratio_string(),
293 }
294}
295
296fn number_text(number: FloatType) -> String {
297 if number.fract() == 0.0 {
298 format!("{number:.0}")
299 } else {
300 number.to_string()
301 }
302}
303
304fn tempo_field(mark: &MetronomeMark) -> Result<Option<String>> {
306 let mut parts = Vec::new();
307 if let Some(text) = mark.text().filter(|_| !mark.text_implicit()) {
308 parts.push(format!("\"{}\"", quoted(text)));
309 }
310 if let Some(number) = mark.number().filter(|_| !mark.number_implicit()) {
311 let (numerator, denominator) = fraction(mark.referent().quarter_length() / 4.0)?;
312 parts.push(format!("{numerator}/{denominator}={}", number_text(number)));
313 }
314 Ok((!parts.is_empty()).then(|| parts.join(" ")))
315}
316
317#[derive(Clone, Copy, Debug)]
320struct Held<'a> {
321 offset: FloatType,
322 element: &'a StreamElement,
323 leaf: usize,
324}
325
326#[derive(Clone, Debug, Default)]
328struct Bar<'a> {
329 measure: Option<&'a Stream>,
330 held: Vec<Held<'a>>,
331 filler: Option<FloatType>,
333}
334
335#[derive(Clone, Debug)]
337struct Voice<'a> {
338 part: &'a Stream,
339 bars: Vec<Bar<'a>>,
340 measured: bool,
341 within: usize,
343}
344
345fn program_of(part: &Stream) -> Option<u8> {
348 let leaves = part.leaves();
349 let first_note = leaves
350 .iter()
351 .filter(|(_, element)| is_sounding(element))
352 .map(|(offset, _)| *offset)
353 .fold(FloatType::INFINITY, FloatType::min);
354 leaves.iter().find_map(|(offset, element)| match element {
355 StreamElement::Instrument(instrument) if *offset <= first_note + EPSILON => {
356 instrument.midi_program()
357 }
358 _ => None,
359 })
360}
361
362fn is_sounding(element: &StreamElement) -> bool {
363 matches!(
364 element,
365 StreamElement::Note(_)
366 | StreamElement::Chord(_)
367 | StreamElement::Rest(_)
368 | StreamElement::Unpitched(_)
369 | StreamElement::PercussionChord(_)
370 )
371}
372
373fn is_grace(element: &StreamElement) -> bool {
374 is_sounding(element) && element.duration().is_some_and(Duration::is_grace)
375}
376
377fn gather<'a>(stream: &'a Stream, base: FloatType, leaf: &mut usize, out: &mut Vec<Held<'a>>) {
380 for event in stream.events() {
381 match event.element() {
382 StreamElement::Stream(inner) => gather(inner, base + event.offset(), leaf, out),
383 element => {
384 out.push(Held {
385 offset: base + event.offset(),
386 element,
387 leaf: *leaf,
388 });
389 *leaf += 1;
390 }
391 }
392 }
393}
394
395fn sort_held(held: &mut [Held<'_>]) {
396 held.sort_by(|left, right| {
397 left.offset
398 .total_cmp(&right.offset)
399 .then(class_sort_order(left.element).cmp(&class_sort_order(right.element)))
400 .then(is_grace(right.element).cmp(&is_grace(left.element)))
401 .then(left.leaf.cmp(&right.leaf))
402 });
403}
404
405fn voices_of<'a>(part: &'a Stream, leaf: &mut usize) -> Result<Vec<Voice<'a>>> {
407 let has_measures = part.events().iter().any(|event| {
408 event
409 .element()
410 .as_stream()
411 .is_some_and(|inner| inner.kind() == StreamKind::Measure)
412 });
413 if !has_measures {
414 let mut held = Vec::new();
415 gather(part, 0.0, leaf, &mut held);
416 return Ok(vec![Voice {
417 part,
418 bars: vec![Bar {
419 measure: None,
420 held,
421 filler: None,
422 }],
423 measured: false,
424 within: 0,
425 }]);
426 }
427
428 type Split<'a> = (&'a Stream, Vec<Held<'a>>, Vec<Vec<Held<'a>>>);
430 let mut measures: Vec<Split<'a>> = Vec::new();
431 let mut loose: Vec<Held<'a>> = Vec::new();
432 for event in part.events() {
433 match event.element() {
434 StreamElement::Stream(measure) => {
435 let mut common = Vec::new();
436 let mut voices = Vec::new();
437 for inner in measure.events() {
438 match inner.element() {
439 StreamElement::Stream(voice) => {
440 let mut held = Vec::new();
441 gather(voice, inner.offset(), leaf, &mut held);
442 voices.push(held);
443 }
444 element => {
445 common.push(Held {
446 offset: inner.offset(),
447 element,
448 leaf: *leaf,
449 });
450 *leaf += 1;
451 }
452 }
453 }
454 measures.push((measure, common, voices));
455 }
456 element => {
457 loose.push(Held {
458 offset: event.offset(),
459 element,
460 leaf: *leaf,
461 });
462 *leaf += 1;
463 }
464 }
465 }
466 if loose.iter().any(|held| is_sounding(held.element)) {
467 return Err(abc_error(
468 "a part holds notes outside its measures as well as inside them",
469 ));
470 }
471
472 let count = measures
473 .iter()
474 .map(|(_, _, voices)| voices.len())
475 .max()
476 .unwrap_or(0)
477 .max(1);
478 let mut voices: Vec<Voice<'a>> = (0..count)
479 .map(|within| Voice {
480 part,
481 bars: Vec::new(),
482 measured: true,
483 within,
484 })
485 .collect();
486 for (index, (measure, common, inner)) in measures.into_iter().enumerate() {
487 let length = common
488 .iter()
489 .chain(inner.iter().flatten())
490 .map(|held| held.offset + held.element.quarter_length())
491 .fold(0.0, FloatType::max);
492 let plain_measure = inner.is_empty();
493 let mut inner = inner.into_iter();
494 for (within, voice) in voices.iter_mut().enumerate() {
495 let own = inner.next();
496 let mut held = Vec::new();
497 if within == 0 {
498 held.extend(common.iter().copied());
499 if index == 0 {
502 held.extend(loose.iter().filter(|held| held.offset < EPSILON).copied());
503 }
504 } else {
505 let shared = |held: &&Held<'a>| {
508 matches!(
509 held.element,
510 StreamElement::Clef(_)
511 | StreamElement::Key(_)
512 | StreamElement::KeySignature(_)
513 | StreamElement::TimeSignature(_)
514 )
515 };
516 held.extend(common.iter().filter(shared).copied());
517 if index == 0 {
518 held.extend(
519 loose
520 .iter()
521 .filter(|held| held.offset < EPSILON)
522 .filter(shared)
523 .copied(),
524 );
525 }
526 }
527 let filler = match own {
528 Some(own) => {
529 held.extend(own);
530 None
531 }
532 None if within == 0 && plain_measure => None,
533 None => Some(length),
534 };
535 if within > 0 || !plain_measure || index == 0 {
536 sort_held(&mut held);
537 }
538 voice.bars.push(Bar {
539 measure: Some(measure),
540 held,
541 filler,
542 });
543 }
544 }
545 Ok(voices)
546}
547
548fn meter_of<'a>(bar: &Bar<'a>) -> Option<&'a TimeSignature> {
550 bar.held.iter().find_map(|held| match held.element {
551 StreamElement::TimeSignature(meter) if held.offset < EPSILON => Some(meter),
552 _ => None,
553 })
554}
555
556fn length_of(bar: &Bar<'_>) -> FloatType {
558 bar.held
559 .iter()
560 .map(|held| held.offset + held.element.quarter_length())
561 .fold(0.0, FloatType::max)
562}
563
564#[derive(Clone, Debug, Default)]
567struct Tail {
568 extra: FloatType,
570 tied: bool,
572 closes: String,
574}
575
576#[derive(Clone, Debug, Default)]
578struct Plan {
579 drop_meter: bool,
581 tail: Option<Tail>,
583 skip_head: bool,
585}
586
587fn first_sounding(bar: &Bar<'_>) -> Option<usize> {
588 bar.held.iter().position(|held| is_sounding(held.element))
589}
590
591fn last_sounding(bar: &Bar<'_>) -> Option<usize> {
592 bar.held
593 .iter()
594 .rposition(|held| is_sounding(held.element) && !is_grace(held.element))
595}
596
597fn notes_of(element: &StreamElement) -> Option<&[Note]> {
599 match element {
600 StreamElement::Note(note) => Some(std::slice::from_ref(note)),
601 StreamElement::Chord(chord) => Some(chord.notes()),
602 _ => None,
603 }
604}
605
606fn cut_note(first: &Bar<'_>, second: &Bar<'_>) -> Option<bool> {
612 let before = first.held[last_sounding(first)?];
613 let after = second.held[first_sounding(second)?];
614 if is_grace(after.element)
615 || after.offset > EPSILON
616 || (before.offset + before.element.quarter_length() - length_of(first)).abs() > EPSILON
617 {
618 return None;
619 }
620 let is_chord = matches!(before.element, StreamElement::Chord(_));
621 if is_chord != matches!(after.element, StreamElement::Chord(_)) {
622 return None;
623 }
624 let (ending, starting) = (notes_of(before.element)?, notes_of(after.element)?);
625 let said = |element: &StreamElement| {
626 element
627 .duration()
628 .and_then(Duration::said_tuplets)
629 .is_some_and(|tuplets| !tuplets.is_empty())
630 };
631 if ending.len() != starting.len() || said(before.element) || said(after.element) {
632 return None;
633 }
634 let tie = |note: &Note| note.tie().map(crate::notation::Tie::tie_type);
635 let halves = ending.iter().zip(starting).all(|(one, other)| {
636 one.pitch().name_with_octave() == other.pitch().name_with_octave()
637 && matches!(tie(one), Some(TieType::Start | TieType::Continue))
638 && matches!(tie(other), Some(TieType::Stop | TieType::Continue))
639 && other.lyrics().is_empty()
640 });
641 if !halves {
642 return None;
643 }
644 let hidden = ending.iter().zip(starting).any(|(one, other)| {
645 shows_accidental(one.pitch())
646 && other
647 .pitch()
648 .written_accidental()
649 .is_some_and(|accidental| accidental.display_status() == Some(false))
650 });
651 Some(is_chord || hidden)
652}
653
654struct Sounding<'a> {
656 duration: Duration,
657 written: FloatType,
659 tuplet: Option<(UnsignedIntegerType, UnsignedIntegerType)>,
661 bracket: Option<TupletType>,
662 grace: bool,
663 held: Held<'a>,
664}
665
666fn sounding_of<'a>(held: Held<'a>) -> Result<Option<Sounding<'a>>> {
667 let duration = match held.element {
668 StreamElement::Note(note) => note.duration().cloned().unwrap_or_else(Duration::quarter),
669 StreamElement::Chord(chord) => chord.duration().cloned().unwrap_or_else(Duration::quarter),
670 StreamElement::Rest(rest) => rest.duration().clone(),
671 StreamElement::Unpitched(_) | StreamElement::PercussionChord(_) => {
672 return Err(abc_error("ABC has no unpitched notes"));
673 }
674 _ => return Ok(None),
675 };
676 let grace = duration.is_grace();
677 let said = duration.said_tuplets().unwrap_or(&[]);
678 if said.len() > 1 {
679 return Err(abc_error("ABC cannot write a tuplet inside a tuplet"));
680 }
681 let (tuplet, bracket) = match said.first().filter(|_| !grace) {
682 Some(tuplet) => {
683 let multiplier = tuplet.multiplier();
684 let numerator = *multiplier.numer().unwrap_or(&0) as FloatType;
685 let denominator = *multiplier.denom().unwrap_or(&1) as FloatType;
686 let normal = FloatType::from(tuplet.actual()) * numerator / denominator;
687 if normal < 1.0 || (normal - normal.round()).abs() > EPSILON {
688 return Err(abc_error(format!(
689 "ABC cannot write {} notes in the time of {normal}",
690 tuplet.actual()
691 )));
692 }
693 (
694 Some((tuplet.actual(), normal.round() as UnsignedIntegerType)),
695 tuplet.tuplet_type(),
696 )
697 }
698 None => (None, None),
699 };
700 let written = if grace {
701 duration
702 .written_values()
703 .iter()
704 .filter_map(|value| {
705 Some(
706 value
707 .duration_type()?
708 .quarter_length_with_dots(value.dots()),
709 )
710 })
711 .sum()
712 } else if tuplet.is_some() {
713 duration.quarter_length_no_tuplets()
714 } else {
715 duration.quarter_length()
716 };
717 Ok(Some(Sounding {
718 duration,
719 written,
720 tuplet,
721 bracket,
722 grace,
723 held,
724 }))
725}
726
727fn tuplet_marker(actual: u32, normal: u32, count: usize) -> String {
730 let usual = match actual {
731 2 | 4 | 8 => Some(3),
732 3 | 6 => Some(2),
733 _ => None,
734 };
735 let mut marker = format!("({actual}");
736 let says_normal = usual != Some(normal);
737 if says_normal {
738 let _ = write!(marker, ":{normal}");
739 }
740 if count != actual as usize {
741 if !says_normal {
742 marker.push(':');
743 }
744 let _ = write!(marker, ":{count}");
745 }
746 marker
747}
748
749fn tuplet_markers(
752 tokens: &[Sounding<'_>],
753 opens_on: &dyn Fn(&Sounding<'_>) -> bool,
754) -> BTreeMap<usize, String> {
755 let mut markers = BTreeMap::new();
756 let mut index = 0;
757 while index < tokens.len() {
758 let Some((actual, normal)) = tokens[index].tuplet else {
759 index += 1;
760 continue;
761 };
762 let mut start = index;
767 while start > 0 && tokens[start - 1].grace {
768 start -= 1;
769 }
770 if !tokens[start..index].iter().any(opens_on) {
771 start = index;
772 }
773 let mut last = index;
774 let mut filled = tokens[index].written;
775 loop {
776 let whole = filled / FloatType::from(actual);
780 let filled_up = whole > 0.0 && (whole.log2() - whole.log2().round()).abs() < 1e-9;
781 let complete = last - start + 1 >= actual as usize || (filled_up && last > index);
782 if complete
783 || matches!(
784 tokens[last].bracket,
785 Some(TupletType::Stop | TupletType::StartStop)
786 )
787 {
788 break;
789 }
790 let mut next = last + 1;
791 while next < tokens.len() && tokens[next].grace {
792 next += 1;
793 }
794 if next >= tokens.len()
796 || tokens[next].tuplet != Some((actual, normal))
797 || matches!(
798 tokens[next].bracket,
799 Some(TupletType::Start | TupletType::StartStop)
800 )
801 || next - start + 1 > 9
802 {
803 break;
804 }
805 filled += tokens[next].written;
806 last = next;
807 }
808 markers.insert(start, tuplet_marker(actual, normal, last - start + 1));
809 index = last + 1;
810 }
811 markers
812}
813
814fn articulations_of(element: &StreamElement) -> &[Articulation] {
816 match element {
817 StreamElement::Note(note) => note.articulations(),
818 StreamElement::Chord(chord) => chord.articulations(),
819 StreamElement::Rest(rest) => rest.articulations(),
820 _ => &[],
821 }
822}
823
824fn is_lettered(articulation: &Articulation) -> bool {
827 matches!(
828 articulation.kind(),
829 ArticulationKind::Staccato
830 | ArticulationKind::UpBow
831 | ArticulationKind::DownBow
832 | ArticulationKind::Accent
833 | ArticulationKind::StrongAccent
834 | ArticulationKind::Tenuto
835 )
836}
837
838fn articulation_text(articulation: &Articulation) -> Result<String> {
840 Ok(match articulation.kind() {
841 ArticulationKind::Staccato => ".".to_string(),
842 ArticulationKind::UpBow => "u".to_string(),
843 ArticulationKind::DownBow => "v".to_string(),
844 ArticulationKind::Accent => "K".to_string(),
846 ArticulationKind::StrongAccent => "k".to_string(),
847 ArticulationKind::Tenuto => "M".to_string(),
848 ArticulationKind::Staccatissimo => "!wedge!".to_string(),
849 ArticulationKind::BreathMark => "!breath!".to_string(),
850 ArticulationKind::OpenString => "!open!".to_string(),
851 ArticulationKind::Stopped => "!+!".to_string(),
852 ArticulationKind::SnapPizzicato => "!snap!".to_string(),
853 ArticulationKind::Fingering => match articulation.finger() {
854 Some(Finger::Number(number)) if (0..=5).contains(number) => format!("!{number}!"),
855 _ => return Err(abc_error("ABC writes fingers nought to five and no others")),
856 },
857 other => {
858 return Err(abc_error(format!(
859 "ABC has no decoration for {}",
860 other.class_name()
861 )));
862 }
863 })
864}
865
866fn expression_text(expression: &Expression) -> Result<&'static str> {
867 Ok(match expression {
868 Expression::Fermata(fermata) => match fermata.fermata_type() {
869 FermataType::Inverted => "!fermata!",
870 FermataType::Upright => "!invertedfermata!",
871 },
872 Expression::Arpeggio(ArpeggioType::Normal) => "!arpeggio!",
873 Expression::Arpeggio(_) => {
874 return Err(abc_error("ABC has one arpeggio sign, the plain one"));
875 }
876 Expression::Ornament(ornament) => match ornament.kind() {
877 OrnamentKind::Trill | OrnamentKind::HalfStepTrill | OrnamentKind::WholeStepTrill => {
878 "!trill!"
879 }
880 OrnamentKind::Mordent
881 | OrnamentKind::HalfStepMordent
882 | OrnamentKind::WholeStepMordent => "!lowermordent!",
883 OrnamentKind::InvertedMordent
884 | OrnamentKind::HalfStepInvertedMordent
885 | OrnamentKind::WholeStepInvertedMordent => "!uppermordent!",
886 OrnamentKind::Turn => "!turn!",
887 OrnamentKind::InvertedTurn => "!invertedturn!",
888 other => {
889 return Err(abc_error(format!(
890 "ABC has no decoration for {}",
891 other.class_name()
892 )));
893 }
894 },
895 })
896}
897
898fn decorations(articulations: &[Articulation], expressions: &[Expression]) -> Result<String> {
899 let mut text = String::new();
900 for expression in expressions {
901 text.push_str(expression_text(expression)?);
902 }
903 let mut written = articulations
905 .iter()
906 .map(articulation_text)
907 .collect::<Result<Vec<_>>>()?;
908 written.sort_by_key(|text| std::cmp::Reverse(text.len()));
909 text.push_str(&written.concat());
910 Ok(text)
911}
912
913fn spanner_open(kind: SpannerKind) -> Result<&'static str> {
915 Ok(match kind {
916 SpannerKind::Slur => "(",
917 SpannerKind::Crescendo => "!crescendo(!",
918 SpannerKind::Diminuendo => "!diminuendo(!",
919 other => {
920 return Err(abc_error(format!("ABC has no {}", other.class_name())));
921 }
922 })
923}
924
925fn spanner_close(kind: SpannerKind) -> &'static str {
926 match kind {
927 SpannerKind::Crescendo => "!crescendo)!",
928 SpannerKind::Diminuendo => "!diminuendo)!",
929 _ => ")",
930 }
931}
932
933struct Reading {
935 alters: Alters,
937}
938
939#[derive(Default)]
942struct Written<'a> {
943 opening: Vec<String>,
945 music: String,
946 closing: Vec<String>,
948 sung: Vec<&'a [Lyric]>,
950 starts_with_note: bool,
953}
954
955struct Tune<'a> {
957 metadata: Option<&'a Metadata>,
958 voices: Vec<Voice<'a>>,
959 parts: Vec<(&'a Stream, usize)>,
961 opens: BTreeMap<usize, Vec<SpannerKind>>,
962 closes: BTreeMap<usize, Vec<SpannerKind>>,
963 number: IntegerType,
964}
965
966impl<'a> Tune<'a> {
967 fn new(stream: &'a Stream, number: IntegerType) -> Result<Self> {
968 let mut leaf = 0;
969 let mut voices = Vec::new();
970 let mut parts = Vec::new();
971 let holds_parts = stream.events().iter().any(|event| {
972 event.element().as_stream().is_some_and(|inner| {
973 matches!(inner.kind(), StreamKind::Part | StreamKind::PartStaff)
974 })
975 });
976 if holds_parts {
977 for event in stream.events() {
978 match event.element() {
979 StreamElement::Stream(part) => {
980 let made = voices_of(part, &mut leaf)?;
981 parts.push((part.as_ref(), made.len()));
982 voices.extend(made);
983 }
984 element if is_sounding(element) => {
985 return Err(abc_error(
986 "a score holds notes outside its parts as well as inside them",
987 ));
988 }
989 _ => leaf += 1,
990 }
991 }
992 } else if stream.kind() == StreamKind::Measure {
993 let mut held = Vec::new();
995 gather(stream, 0.0, &mut leaf, &mut held);
996 parts.push((stream, 1));
997 voices.push(Voice {
998 part: stream,
999 bars: vec![Bar {
1000 measure: None,
1001 held,
1002 filler: None,
1003 }],
1004 measured: false,
1005 within: 0,
1006 });
1007 } else {
1008 let made = voices_of(stream, &mut leaf)?;
1009 parts.push((stream, made.len()));
1010 voices.extend(made);
1011 }
1012
1013 let leaves = stream.leaves();
1015 let graced = |leaf: usize| {
1016 leaves
1017 .get(leaf)
1018 .is_some_and(|(_, element)| is_grace(element))
1019 };
1020 let mut opens: BTreeMap<usize, Vec<(usize, SpannerKind)>> = BTreeMap::new();
1021 let mut closes: BTreeMap<usize, Vec<(usize, SpannerKind)>> = BTreeMap::new();
1022 for spanner in stream.spanners() {
1023 spanner_open(spanner.kind())?;
1024 let spanned = spanner.spanned();
1025 let placed: Vec<usize> = spanned.iter().flatten().copied().collect();
1026 let (Some(first), Some(last)) = (placed.iter().min(), placed.iter().max()) else {
1027 continue;
1028 };
1029 let mut start = *first;
1032 for _ in spanned.iter().take_while(|place| place.is_none()) {
1033 if start > 0 && graced(start - 1) {
1034 start -= 1;
1035 }
1036 }
1037 let mut end = *last;
1038 for _ in spanned.iter().rev().take_while(|place| place.is_none()) {
1039 if graced(end + 1) {
1040 end += 1;
1041 }
1042 }
1043 opens.entry(start).or_default().push((end, spanner.kind()));
1044 closes.entry(end).or_default().push((start, spanner.kind()));
1045 }
1046 let ordered = |mut list: Vec<(usize, SpannerKind)>| {
1049 list.sort_by_key(|(other, _)| std::cmp::Reverse(*other));
1050 list.into_iter().map(|(_, kind)| kind).collect::<Vec<_>>()
1051 };
1052 Ok(Self {
1053 metadata: stream.metadata(),
1054 voices,
1055 parts,
1056 opens: opens
1057 .into_iter()
1058 .map(|(leaf, list)| (leaf, ordered(list)))
1059 .collect(),
1060 closes: closes
1061 .into_iter()
1062 .map(|(leaf, list)| (leaf, ordered(list)))
1063 .collect(),
1064 number,
1065 })
1066 }
1067
1068 fn unit(&self, options: &ExportOptions) -> Result<(FloatType, String)> {
1071 if let Some((numerator, denominator)) = options.unit_length {
1072 if numerator == 0 || denominator == 0 {
1073 return Err(abc_error("the unit note length cannot be nought"));
1074 }
1075 return Ok((
1076 4.0 * FloatType::from(numerator) / FloatType::from(denominator),
1077 format!("{numerator}/{denominator}"),
1078 ));
1079 }
1080 const UNITS: [(FloatType, &str); 6] = [
1081 (0.5, "1/8"),
1082 (1.0, "1/4"),
1083 (0.25, "1/16"),
1084 (2.0, "1/2"),
1085 (0.125, "1/32"),
1086 (4.0, "1/1"),
1087 ];
1088 let mut written = Vec::new();
1090 for held in self
1091 .voices
1092 .iter()
1093 .flat_map(|voice| &voice.bars)
1094 .flat_map(|bar| &bar.held)
1095 {
1096 if let Some(sounding) = sounding_of(*held)? {
1097 written.push(sounding.written);
1098 }
1099 }
1100 let mut place = 0;
1101 let mut least = usize::MAX;
1102 for (index, (unit, _)) in UNITS.iter().enumerate() {
1103 let cost: usize = written
1104 .iter()
1105 .map(|length| length_text(*length, *unit).map_or(8, |text| text.len()))
1106 .sum();
1107 if cost < least {
1108 least = cost;
1109 place = index;
1110 }
1111 }
1112 Ok((UNITS[place].0, UNITS[place].1.to_string()))
1113 }
1114
1115 fn write(&self, options: &ExportOptions) -> Result<String> {
1116 let (unit, unit_text) = self.unit(options)?;
1117 let mut out = String::new();
1118 let number = self
1119 .metadata
1120 .and_then(|metadata| metadata.first_text("number"))
1121 .and_then(|text| text.trim().parse::<IntegerType>().ok())
1122 .unwrap_or(self.number);
1123 let _ = writeln!(out, "X:{number}");
1124 if let Some(metadata) = self.metadata {
1125 for (name, tag) in [
1126 ("title", 'T'),
1127 ("alternativeTitle", 'T'),
1128 ("composer", 'C'),
1129 ("localeOfComposition", 'O'),
1130 ] {
1131 for value in metadata.get(name) {
1132 let _ = writeln!(out, "{tag}:{}", plain(value.text()));
1133 }
1134 }
1135 }
1136
1137 for (kind, field) in [
1140 (ArticulationKind::Accent, "U:K=!accent!"),
1141 (ArticulationKind::StrongAccent, "U:k=!marcato!"),
1142 (ArticulationKind::Tenuto, "U:M=!tenuto!"),
1143 ] {
1144 let used = self
1145 .voices
1146 .iter()
1147 .flat_map(|voice| &voice.bars)
1148 .flat_map(|bar| &bar.held)
1149 .flat_map(|held| articulations_of(held.element))
1150 .any(|articulation| articulation.kind() == kind);
1151 if used {
1152 let _ = writeln!(out, "{field}");
1153 }
1154 }
1155
1156 let first = self.voices.first();
1159 let opening = |voice: &Voice<'a>| -> Opening<'a> {
1160 Opening::of(voice.bars.first().map_or(&[], |bar| bar.held.as_slice()))
1161 };
1162 let shared = first.map(opening).unwrap_or_default();
1163 let openings: Vec<Opening<'a>> = self.voices.iter().map(opening).collect();
1164 let all = |same: &dyn Fn(&Opening<'a>) -> bool| openings.iter().all(same);
1165 let shared_meter = all(&|other| other.meter == shared.meter);
1166 let shared_tempo = all(&|other| other.tempos == shared.tempos);
1167
1168 if shared_meter {
1169 let _ = writeln!(
1170 out,
1171 "M:{}",
1172 shared.meter.map_or("none".to_string(), meter_field)
1173 );
1174 }
1175 let _ = writeln!(out, "L:{unit_text}");
1176 if shared_tempo {
1177 for mark in &shared.tempos {
1178 if let Some(field) = tempo_field(mark)? {
1179 let _ = writeln!(out, "Q:{field}");
1180 }
1181 }
1182 }
1183 let clef_of = |voice: &Voice<'a>, opening: &Opening<'a>| -> Option<&'a Clef> {
1184 let clef = opening.clef?;
1185 let pitches: Vec<Pitch> = voice
1186 .bars
1187 .iter()
1188 .flat_map(|bar| &bar.held)
1189 .flat_map(|held| match held.element {
1190 StreamElement::ChordSymbol(symbol) => symbol.pitches().unwrap_or_default(),
1191 element => element.pitches(),
1192 })
1193 .collect();
1194 let unsaid = clef.kind() == crate::clef::ClefKind::TrebleClef
1198 && *clef == Clef::best_for(&pitches, false);
1199 (!unsaid).then_some(clef)
1200 };
1201 let key_line = |opening: &Opening<'a>| -> Result<String> {
1205 match opening.key {
1206 Some(key) => key_field(key, None),
1207 None => Ok("none".to_string()),
1208 }
1209 };
1210 let voice_line =
1211 |index: usize, voice: &Voice<'a>, opening: &Opening<'a>| -> Result<String> {
1212 let mut line = format!("V:{}", index + 1);
1213 if let Some(clef) = clef_of(voice, opening) {
1214 let _ = write!(line, " clef={}", clef_name(clef)?);
1215 }
1216 if let Some(name) = voice.part.name().filter(|_| voice.within == 0) {
1217 let _ = write!(line, " name=\"{}\"", quoted(name));
1218 }
1219 Ok(line)
1220 };
1221 let mut reading = Reading { alters: [0; 7] };
1222 let several = self.voices.len() > 1;
1223 let key_heads = !several || shared.key.is_some();
1226 if !key_heads {
1227 } else if first.is_some() {
1228 let _ = writeln!(out, "K:{}", key_line(&shared)?);
1229 reading.alters = shared
1230 .key
1231 .and_then(signature_of)
1232 .map_or([0; 7], |signature| alters_of(&signature));
1233 } else {
1234 let _ = writeln!(out, "K:none");
1235 }
1236
1237 if self.parts.iter().any(|(_, voices)| *voices > 1) {
1238 let mut next = 1;
1240 let groups: Vec<String> = self
1241 .parts
1242 .iter()
1243 .map(|(_, voices)| {
1244 let ids: Vec<String> = (next..next + voices).map(|id| id.to_string()).collect();
1245 next += voices;
1246 if ids.len() > 1 {
1247 format!("({})", ids.join(" "))
1248 } else {
1249 ids.concat()
1250 }
1251 })
1252 .collect();
1253 let _ = writeln!(out, "%%score {}", groups.join(" "));
1254 }
1255 for (index, voice) in self.voices.iter().enumerate() {
1256 let opening = &openings[index];
1257 let line = voice_line(index, voice, opening)?;
1261 if several || line != "V:1" {
1262 let _ = writeln!(out, "{line}");
1263 }
1264 if several {
1265 if !shared_meter {
1266 let _ = writeln!(
1267 out,
1268 "M:{}",
1269 opening.meter.map_or("none".to_string(), meter_field)
1270 );
1271 }
1272 if !shared_tempo {
1273 for mark in &opening.tempos {
1274 if let Some(field) = tempo_field(mark)? {
1275 let _ = writeln!(out, "Q:{field}");
1276 }
1277 }
1278 }
1279 let alters = opening
1282 .key
1283 .and_then(signature_of)
1284 .map_or([0; 7], |signature| alters_of(&signature));
1285 let line = key_line(opening)?;
1286 let stated = opening.key.is_some();
1287 let differs = !key_heads || alters != reading.alters || line != key_line(&shared)?;
1288 if stated && differs && (index > 0 || !key_heads) {
1289 let _ = writeln!(out, "K:{line}");
1290 }
1291 reading.alters = alters;
1292 }
1293 if let Some(program) = program_of(voice.part) {
1296 let _ = writeln!(out, "%%MIDI program {program}");
1297 }
1298 self.write_voice(voice, unit, &unit_text, options, &mut reading, &mut out)?;
1299 }
1300 Ok(out)
1301 }
1302
1303 fn write_voice(
1304 &self,
1305 voice: &Voice<'a>,
1306 unit: FloatType,
1307 unit_text: &str,
1308 options: &ExportOptions,
1309 reading: &mut Reading,
1310 out: &mut String,
1311 ) -> Result<()> {
1312 let mut tokens = Vec::new();
1315 let mut places = Vec::new();
1316 for (bar_index, bar) in voice.bars.iter().enumerate() {
1317 for (held_index, held) in bar.held.iter().enumerate() {
1318 if let Some(sounding) = sounding_of(*held)? {
1319 places.push((bar_index, held_index));
1320 tokens.push(sounding);
1321 }
1322 }
1323 }
1324 let opens_on = |sounding: &Sounding<'_>| self.opens.contains_key(&sounding.held.leaf);
1325 let markers: BTreeMap<(usize, usize), String> = tuplet_markers(&tokens, &opens_on)
1326 .into_iter()
1327 .map(|(token, marker)| (places[token], marker))
1328 .collect();
1329
1330 let count = voice.bars.len();
1336 let mut joined = vec![false; count];
1337 let mut plans = vec![Plan::default(); count];
1338 if voice.measured {
1339 let mut meter: Option<&TimeSignature> = None;
1340 for index in 0..count {
1341 let before = meter;
1342 if let Some(own) = meter_of(&voice.bars[index]) {
1343 meter = Some(own);
1344 }
1345 if index == 0 || joined[index - 1] {
1346 continue;
1347 }
1348 let (first, second) = (&voice.bars[index - 1], &voice.bars[index]);
1349 let moved = second
1350 .measure
1351 .and_then(Stream::left_barline)
1352 .is_some_and(|barline| {
1353 barline.repeat_direction() != Some(RepeatDirection::Start)
1354 });
1355 let closed = first.measure.and_then(Stream::right_barline).is_some()
1356 || second.measure.and_then(Stream::right_barline).is_some();
1357 let bar_length = before.map(TimeSignature::bar_quarter_length);
1358 let full = bar_length.is_some_and(|bar| (length_of(first) - bar).abs() < EPSILON);
1359 let remainder = meter_of(second).is_some()
1362 || bar_length.is_some_and(|bar| (length_of(second) - bar).abs() < EPSILON);
1363 let cut = cut_note(first, second);
1364 if (moved || (cut == Some(true) && remainder))
1365 && !closed
1366 && full
1367 && first.filler.is_none()
1368 && second.filler.is_none()
1369 {
1370 joined[index] = true;
1371 plans[index].drop_meter = true;
1372 if cut.is_some()
1373 && let Some(head) = first_sounding(second)
1374 {
1375 let head = second.held[head];
1376 plans[index].skip_head = true;
1377 plans[index - 1].tail = Some(Tail {
1378 extra: head.element.quarter_length(),
1379 tied: notes_of(head.element).is_some_and(|notes| {
1380 notes.iter().any(|note| {
1381 note.tie().map(crate::notation::Tie::tie_type)
1382 == Some(TieType::Continue)
1383 })
1384 }),
1385 closes: self
1386 .closes
1387 .get(&head.leaf)
1388 .map_or(&[][..], Vec::as_slice)
1389 .iter()
1390 .map(|kind| spanner_close(*kind))
1391 .collect(),
1392 });
1393 }
1394 if let (Some(next), Some(old)) = (voice.bars.get(index + 1), before)
1396 && meter_of(next)
1397 .is_some_and(|given| given.ratio_string() == old.ratio_string())
1398 {
1399 plans[index + 1].drop_meter = true;
1400 }
1401 meter = before;
1402 }
1403 }
1404 }
1405
1406 let mut written = Vec::new();
1407 for (index, bar) in voice.bars.iter().enumerate() {
1408 written.push(self.write_bar(
1409 voice,
1410 index,
1411 bar,
1412 unit,
1413 unit_text,
1414 &markers,
1415 reading,
1416 &plans[index],
1417 )?);
1418 }
1419 let barline_after = |index: usize| -> (String, bool) {
1421 if !voice.measured {
1422 return (String::new(), false);
1423 }
1424 let bar = &voice.bars[index];
1425 let right = if joined[index] {
1426 bar.measure.and_then(Stream::left_barline)
1427 } else {
1428 bar.measure.and_then(Stream::right_barline)
1429 };
1430 let next = voice.bars.get(index + 1).and_then(|bar| bar.measure);
1431 let field_can_follow = written
1433 .get(index + 1)
1434 .is_none_or(|next| next.opening.is_empty() && next.starts_with_note);
1435 barline_text(
1436 right,
1437 next.and_then(Stream::left_barline),
1438 next.and_then(Stream::ending),
1439 false,
1440 field_can_follow,
1441 )
1442 };
1443
1444 let has_lyrics = written
1445 .iter()
1446 .any(|bar| bar.sung.iter().any(|lyrics| !lyrics.is_empty()));
1447 let per_line = options.measures_per_line.max(1);
1448 let last = written.len().saturating_sub(1);
1449 let mut line = String::new();
1450 let mut on_line = 0;
1451 let mut sung: Vec<&[Lyric]> = Vec::new();
1452 if voice.measured {
1453 let first = voice.bars.first().and_then(|bar| bar.measure);
1454 line.push_str(
1455 &barline_text(
1456 None,
1457 first.and_then(Stream::left_barline),
1458 first.and_then(Stream::ending),
1459 true,
1460 false,
1461 )
1462 .0,
1463 );
1464 }
1465 for (index, bar) in written.iter().enumerate() {
1466 if !line.is_empty() && !line.ends_with(' ') && !bar.music.is_empty() {
1467 line.push(' ');
1468 }
1469 line.push_str(&bar.music);
1470 sung.extend(bar.sung.iter().copied());
1471 if joined.get(index + 1) == Some(&true) {
1472 continue;
1473 }
1474 on_line += 1;
1475 let next = written.get(index + 1);
1476 let (barline, keep_off) = barline_after(index);
1477 let said_again = [format!("L:{unit_text}")];
1481 let opening: &[String] = match next {
1482 Some(next) if !next.opening.is_empty() => &next.opening,
1483 Some(_) if keep_off => &said_again,
1484 _ => &[],
1485 };
1486 let fields_follow = !opening.is_empty();
1487 let mid_word = sung
1489 .iter()
1490 .rev()
1491 .find_map(|lyrics| lyrics.first())
1492 .is_some_and(|lyric| lyric.raw_text().ends_with('-'));
1493 let breaks = index == last
1494 || !bar.closing.is_empty()
1495 || fields_follow
1496 || (on_line >= per_line && !(has_lyrics && mid_word));
1497 if !breaks {
1498 if !barline.is_empty() {
1499 let _ = write!(line, " {barline}");
1500 }
1501 continue;
1502 }
1503 let words = if has_lyrics {
1504 lyric_lines(&sung)
1505 } else {
1506 Vec::new()
1507 };
1508 sung.clear();
1509 on_line = 0;
1510 let before_bar = index != last && !words.is_empty() || !bar.closing.is_empty();
1513 if !before_bar && !barline.is_empty() {
1514 let _ = write!(line, " {barline}");
1515 }
1516 push_line(out, &line);
1517 line.clear();
1518 for words in words {
1519 let _ = writeln!(out, "w:{words}");
1520 }
1521 for field in &bar.closing {
1522 let _ = writeln!(out, "{field}");
1523 }
1524 if before_bar {
1525 line.push_str(&barline);
1526 }
1527 if !opening.is_empty() {
1528 push_line(out, &line);
1529 line.clear();
1530 for field in opening {
1531 let _ = writeln!(out, "{field}");
1532 }
1533 }
1534 }
1535 push_line(out, &line);
1536 Ok(())
1537 }
1538
1539 #[allow(clippy::too_many_arguments)]
1540 fn write_bar(
1541 &self,
1542 voice: &Voice<'a>,
1543 index: usize,
1544 bar: &Bar<'a>,
1545 unit: FloatType,
1546 unit_text: &str,
1547 markers: &BTreeMap<(usize, usize), String>,
1548 reading: &mut Reading,
1549 plan: &Plan,
1550 ) -> Result<Written<'a>> {
1551 let mut written = Written::default();
1552 if let Some(length) = bar.filler {
1553 if length > EPSILON {
1554 written.music = format!("x{}", length_text(length, unit)?);
1555 }
1556 return Ok(written);
1557 }
1558 let end = bar
1559 .held
1560 .iter()
1561 .map(|held| held.offset + held.element.quarter_length())
1562 .fold(0.0, FloatType::max);
1563 let first = index == 0;
1564 let skipped = first_sounding(bar).filter(|_| plan.skip_head);
1565 let tailed = last_sounding(bar).filter(|_| plan.tail.is_some());
1566
1567 let stated = bar
1571 .held
1572 .iter()
1573 .find(|held| held.offset < EPSILON && signature_of(held.element).is_some());
1574 let mut key_closes = false;
1575 let mut silent_key = None;
1576 if voice.measured && !first {
1577 let unmarked = unmarked_notes(bar, skipped);
1578 let fits = |alters: &Alters| {
1579 unmarked
1580 .iter()
1581 .all(|(letter, alter)| alters[*letter] == *alter)
1582 };
1583 match stated.and_then(|held| signature_of(held.element)) {
1584 Some(signature) => {
1585 let alters = alters_of(&signature);
1586 if !fits(&alters) && fits(&reading.alters) {
1587 key_closes = true;
1588 } else {
1589 reading.alters = alters;
1590 }
1591 }
1592 None if !fits(&reading.alters) => {
1593 let in_force = self.key_in_force(voice, index);
1597 let candidates = in_force.into_iter().chain((-7..=7).map(KeySignature::new));
1598 silent_key = candidates
1599 .map(|signature| (alters_of(&signature), signature))
1600 .find(|(alters, _)| fits(alters));
1601 }
1602 None => {}
1603 }
1604 }
1605
1606 let mut opening_clef = None;
1609 for held in &bar.held {
1610 if held.offset > EPSILON || first || !voice.measured {
1611 continue;
1612 }
1613 match held.element {
1614 StreamElement::TimeSignature(meter) if !plan.drop_meter => {
1615 written.opening.push(format!("M:{}", meter_field(meter)));
1616 }
1617 StreamElement::MetronomeMark(mark) if end > EPSILON => {
1618 if let Some(field) = tempo_field(mark)? {
1619 written.opening.push(format!("Q:{field}"));
1620 }
1621 }
1622 StreamElement::Clef(clef) => opening_clef = Some(clef),
1623 StreamElement::Key(_) | StreamElement::KeySignature(_) => {
1624 let field = format!("K:{}", key_field(held.element, None)?);
1625 if key_closes {
1626 written.closing.push(field);
1627 } else {
1628 written.opening.push(field);
1629 }
1630 }
1631 _ => {}
1632 }
1633 }
1634
1635 let mut music = Vec::new();
1636 let mut cursor = 0.0;
1637 let mut symbol: Option<String> = None;
1640 let mut before = String::new();
1641 let mut waiting_since: Option<FloatType> = None;
1642 let mut graces = String::new();
1643 let mut grace_slash = false;
1644 let mut grace_closes = String::new();
1645 let mut lead = String::new();
1646 let mut first_token = true;
1647 if let Some(clef) = opening_clef {
1648 lead.push_str(&format!("[K:clef={}]", clef_name(clef)?));
1649 }
1650
1651 for (held_index, held) in bar.held.iter().enumerate() {
1652 let at_start = held.offset < EPSILON;
1653 let header = at_start && (first || voice.measured);
1654 match held.element {
1655 StreamElement::Stream(_)
1656 | StreamElement::Instrument(_)
1657 | StreamElement::Variant(_)
1658 | StreamElement::GlobalComment(_)
1659 | StreamElement::MiscTandem(_)
1660 | StreamElement::SpineComment(_)
1661 | StreamElement::BrailleSegmentDivision(_) => {}
1662 StreamElement::Clef(clef) => {
1663 if !header {
1664 lead.push_str(&format!("[K:clef={}]", clef_name(clef)?));
1665 }
1666 }
1667 StreamElement::TimeSignature(meter) => {
1668 if !header {
1669 self.field(
1670 voice,
1671 &mut music,
1672 &mut lead,
1673 format!("M:{}", meter_field(meter)),
1674 );
1675 }
1676 }
1677 StreamElement::Key(_) | StreamElement::KeySignature(_) => {
1678 if !header {
1679 let field = format!("K:{}", key_field(held.element, None)?);
1680 self.field(voice, &mut music, &mut lead, field);
1681 if !voice.measured
1682 && let Some(signature) = signature_of(held.element)
1683 {
1684 reading.alters = alters_of(&signature);
1685 }
1686 }
1687 }
1688 StreamElement::MetronomeMark(mark) => {
1689 let Some(field) = tempo_field(mark)? else {
1690 continue;
1691 };
1692 if header && (first || end > EPSILON) {
1693 } else if voice.measured && (held.offset - end).abs() < EPSILON {
1696 written.closing.push(format!("Q:{field}"));
1697 } else {
1698 self.field(voice, &mut music, &mut lead, format!("Q:{field}"));
1699 }
1700 }
1701 StreamElement::TempoText(text) => {
1702 let field = format!("Q:\"{}\"", quoted(text.text()));
1703 self.field(voice, &mut music, &mut lead, field);
1704 }
1705 StreamElement::ChordSymbol(chord_symbol) => {
1706 if symbol.is_some() {
1707 return Err(abc_error("ABC writes one chord symbol on a note"));
1708 }
1709 symbol = Some(format!("\"{}\"", symbol_text(chord_symbol)));
1710 waiting_since.get_or_insert(held.offset);
1711 }
1712 StreamElement::Dynamic(dynamic) => {
1713 let value = dynamic.value();
1714 if value.is_empty() || !value.chars().all(|c| c.is_ascii_alphabetic()) {
1715 return Err(abc_error(format!("ABC has no dynamic {value:?}")));
1716 }
1717 let _ = write!(before, "!{value}!");
1718 waiting_since.get_or_insert(held.offset);
1719 }
1720 StreamElement::TextExpression(expression) => {
1721 let side = if expression.placement() == Some(Placement::Below) {
1722 '_'
1723 } else {
1724 '^'
1725 };
1726 let _ = write!(before, "\"{side}{}\"", quoted(expression.content()));
1727 waiting_since.get_or_insert(held.offset);
1728 }
1729 StreamElement::RepeatExpression(mark) => {
1730 let sign = match mark.kind() {
1731 RepeatExpressionKind::Coda => "!coda!".to_string(),
1732 RepeatExpressionKind::Segno => "!segno!".to_string(),
1733 RepeatExpressionKind::Fine => "!fine!".to_string(),
1734 RepeatExpressionKind::DaCapo => "!D.C.!".to_string(),
1735 RepeatExpressionKind::DalSegno => "!D.S.!".to_string(),
1736 _ => format!("\"^{}\"", quoted(mark.text())),
1737 };
1738 before.push_str(&sign);
1739 waiting_since.get_or_insert(held.offset);
1740 }
1741 StreamElement::Barline(_)
1745 | StreamElement::Layout(_)
1746 | StreamElement::TextBox(_) => {}
1747 StreamElement::PedalObject(_) => {
1748 return Err(abc_error("ABC has no pedal bounces or gaps"));
1749 }
1750 StreamElement::RehearsalMark(_) => {
1751 return Err(abc_error("ABC has no rehearsal marks"));
1752 }
1753 StreamElement::Break(_) => {
1754 return Err(abc_error(
1755 "ABC writes no manuscript's line, page or column breaks",
1756 ));
1757 }
1758 StreamElement::MetricModulation(_) => {
1759 return Err(abc_error("ABC has no metric modulations"));
1760 }
1761 StreamElement::Unpitched(_) | StreamElement::PercussionChord(_) => {
1762 return Err(abc_error("ABC has no unpitched notes"));
1763 }
1764 StreamElement::Note(_) | StreamElement::Chord(_) | StreamElement::Rest(_) => {
1765 if skipped == Some(held_index) {
1766 cursor = held.offset + held.element.quarter_length();
1767 continue;
1768 }
1769 let tail = plan.tail.as_ref().filter(|_| tailed == Some(held_index));
1770 let sounding = sounding_of(*held)?
1771 .ok_or_else(|| abc_error("a note with nothing to write it from"))?;
1772 let opens = self.opens.get(&held.leaf).map_or(&[][..], Vec::as_slice);
1773 let closes: String = self
1774 .closes
1775 .get(&held.leaf)
1776 .map_or(&[][..], Vec::as_slice)
1777 .iter()
1778 .map(|kind| spanner_close(*kind))
1779 .collect();
1780 let marker = markers.get(&(index, held_index));
1781 if marker.is_some() || !opens.is_empty() {
1782 first_token = false;
1783 }
1784 let mut opening = String::new();
1785 for kind in opens {
1786 opening.push_str(spanner_open(*kind)?);
1787 }
1788 if sounding.grace {
1789 if graces.is_empty() {
1790 lead.push_str(marker.map_or("", String::as_str));
1791 lead.push_str(&opening);
1792 grace_slash = sounding
1793 .duration
1794 .grace()
1795 .is_some_and(crate::duration::Grace::slash);
1796 } else if marker.is_some() || !opening.is_empty() {
1797 lead.push_str(&opening);
1800 }
1801 graces.push_str(&self.note_text(&sounding, unit, &reading.alters, None)?);
1802 grace_closes.push_str(&closes);
1803 first_token = false;
1804 continue;
1805 }
1806
1807 if held.offset > cursor + EPSILON {
1809 music.push(format!("x{}", length_text(held.offset - cursor, unit)?));
1810 first_token = false;
1811 } else if held.offset < cursor - EPSILON {
1812 return Err(abc_error(
1813 "notes sound together outside a chord; ABC writes one line to a voice",
1814 ));
1815 }
1816 if waiting_since.is_some_and(|since| since < held.offset - EPSILON) {
1817 return Err(abc_error(
1818 "a chord symbol, dynamic or text stands where no note starts",
1819 ));
1820 }
1821 waiting_since = None;
1822
1823 let mut token = std::mem::take(&mut lead);
1824 if !graces.is_empty() {
1825 let _ = write!(
1826 token,
1827 "{{{}{}}}{}",
1828 if grace_slash { "/" } else { "" },
1829 std::mem::take(&mut graces),
1830 std::mem::take(&mut grace_closes)
1831 );
1832 token.push_str(marker.map_or("", String::as_str));
1833 } else if let Some(marker) = marker {
1834 token.push_str(marker);
1835 }
1836 token.push_str(&opening);
1837 if first_token
1838 && token.is_empty()
1839 && !articulations_of(held.element).iter().any(is_lettered)
1840 {
1841 written.starts_with_note = true;
1842 }
1843 first_token = false;
1844 token.push_str(symbol.take().as_deref().unwrap_or(""));
1845 token.push_str(&std::mem::take(&mut before));
1846 token.push_str(&self.note_text(&sounding, unit, &reading.alters, tail)?);
1847 token.push_str(&closes);
1848 token.push_str(tail.map_or("", |tail| tail.closes.as_str()));
1849 music.push(token);
1850 cursor = held.offset + held.element.quarter_length();
1851
1852 match held.element {
1853 StreamElement::Note(note) => written.sung.push(note.lyrics()),
1854 StreamElement::Chord(chord) => written.sung.push(chord.lyrics()),
1855 _ => {}
1856 }
1857 let beams = match held.element {
1859 StreamElement::Note(note) => Some(note.beams()),
1860 StreamElement::Chord(chord) => Some(chord.beams()),
1861 _ => None,
1862 };
1863 if !beams.is_some_and(beamed_on) {
1864 music.push(" ".to_string());
1865 }
1866 }
1867 }
1868 }
1869 if symbol.is_some() || !before.is_empty() {
1870 return Err(abc_error(
1871 "a chord symbol, dynamic or text stands where no note starts",
1872 ));
1873 }
1874 if !graces.is_empty() {
1876 lead.push_str(&format!(
1877 "{{{}{graces}}}{grace_closes}",
1878 if grace_slash { "/" } else { "" }
1879 ));
1880 }
1881 if !lead.is_empty() {
1882 music.push(lead);
1883 }
1884 written.music = music.concat().trim().to_string();
1885 if written.music.is_empty() && voice.measured {
1886 written.music = "y".to_string();
1888 }
1889
1890 if let Some((alters, signature)) =
1894 silent_key.filter(|_| written.opening.is_empty() && written.starts_with_note)
1895 {
1896 {
1897 written
1898 .opening
1899 .push(format!("K:{}", key_field(&signature.into(), None)?));
1900 reading.alters = alters;
1901 let again =
1904 self.write_bar(voice, index, bar, unit, unit_text, markers, reading, plan)?;
1905 written.music = again.music;
1906 }
1907 } else if written.opening.len() == 1 {
1908 written.opening.push(format!("L:{unit_text}"));
1909 }
1910 if key_closes && let Some(signature) = stated.and_then(|held| signature_of(held.element)) {
1911 reading.alters = alters_of(&signature);
1912 }
1913 Ok(written)
1914 }
1915
1916 fn field(&self, voice: &Voice<'a>, music: &mut Vec<String>, lead: &mut String, field: String) {
1919 if voice.measured {
1920 let _ = write!(lead, "[{field}]");
1921 } else {
1922 music.push(format!("\n{field}\n"));
1923 }
1924 }
1925
1926 fn key_in_force(&self, voice: &Voice<'a>, index: usize) -> Option<KeySignature> {
1928 voice.bars[..=index]
1929 .iter()
1930 .rev()
1931 .flat_map(|bar| bar.held.iter().rev())
1932 .find_map(|held| signature_of(held.element))
1933 }
1934
1935 fn note_text(
1938 &self,
1939 sounding: &Sounding<'a>,
1940 unit: FloatType,
1941 alters: &Alters,
1942 tail: Option<&Tail>,
1943 ) -> Result<String> {
1944 let length = length_text(sounding.written + tail.map_or(0.0, |tail| tail.extra), unit)?;
1945 let tied = |note: &Note| match tail {
1946 Some(tail) => tail.tied,
1947 None => matches!(
1948 note.tie().map(crate::notation::Tie::tie_type),
1949 Some(TieType::Start | TieType::Continue)
1950 ),
1951 };
1952 Ok(match sounding.held.element {
1953 StreamElement::Note(note) => {
1954 let marks = decorations(note.articulations(), note.expressions())?;
1957 format!(
1958 "{marks}{}{length}{}",
1959 pitch_text(note.pitch(), alters)?,
1960 if tied(note) { "-" } else { "" }
1961 )
1962 }
1963 StreamElement::Chord(chord) => chord_text(chord, alters, &length, tied)?,
1964 StreamElement::Rest(rest) => rest_text(rest, &length)?,
1965 _ => String::new(),
1966 })
1967 }
1968}
1969
1970#[derive(Clone, Default)]
1972struct Opening<'a> {
1973 meter: Option<&'a TimeSignature>,
1974 key: Option<&'a StreamElement>,
1975 clef: Option<&'a Clef>,
1976 tempos: Vec<&'a MetronomeMark>,
1977}
1978
1979impl<'a> Opening<'a> {
1980 fn of(held: &[Held<'a>]) -> Self {
1981 let mut opening = Self::default();
1982 for held in held.iter().filter(|held| held.offset < EPSILON) {
1983 match held.element {
1984 StreamElement::TimeSignature(meter) => opening.meter = Some(meter),
1985 StreamElement::Key(_) | StreamElement::KeySignature(_) => {
1986 opening.key = Some(held.element);
1987 }
1988 StreamElement::Clef(clef) => opening.clef = Some(clef),
1989 StreamElement::MetronomeMark(mark) => opening.tempos.push(mark),
1990 _ => {}
1991 }
1992 }
1993 opening
1994 }
1995}
1996
1997fn beamed_on(beams: &Beams) -> bool {
1999 matches!(
2000 beams
2001 .beams()
2002 .first()
2003 .and_then(crate::notation::Beam::beam_type),
2004 Some(BeamType::Start | BeamType::Continue)
2005 )
2006}
2007
2008fn unmarked_notes(bar: &Bar<'_>, skipped: Option<usize>) -> Vec<(usize, IntegerType)> {
2011 let mut notes = Vec::new();
2012 let mut take = |pitch: &Pitch| {
2013 if !shows_accidental(pitch) {
2014 notes.push((
2015 letter_index(pitch.step().as_char()),
2016 pitch.alter().round() as IntegerType,
2017 ));
2018 }
2019 };
2020 for (index, held) in bar.held.iter().enumerate() {
2021 if skipped == Some(index) {
2022 continue;
2023 }
2024 match held.element {
2025 StreamElement::Note(note) => take(note.pitch()),
2026 StreamElement::Chord(chord) => {
2027 chord.notes().iter().map(Note::pitch).for_each(&mut take)
2028 }
2029 _ => {}
2030 }
2031 }
2032 notes
2033}
2034
2035fn shows_accidental(pitch: &Pitch) -> bool {
2037 pitch
2038 .written_accidental()
2039 .is_some_and(|accidental| accidental.display_status() != Some(false))
2040}
2041
2042fn pitch_text(pitch: &Pitch, alters: &Alters) -> Result<String> {
2045 if pitch
2046 .microtone()
2047 .is_some_and(|microtone| microtone.alter() != 0.0)
2048 {
2049 return Err(abc_error("ABC has no microtones"));
2050 }
2051 let alter = pitch.alter();
2052 if alter.fract() != 0.0 {
2053 return Err(abc_error("ABC has no quarter-tone accidentals"));
2054 }
2055 let alter = alter as IntegerType;
2056 let letter = pitch.step().as_char();
2057 let marks = if shows_accidental(pitch) || alters[letter_index(letter)] != alter {
2058 accidental_marks(alter)?
2059 } else {
2060 ""
2061 };
2062 let octave = pitch.octave().unwrap_or(4);
2063 Ok(if octave >= 5 {
2064 format!(
2065 "{marks}{}{}",
2066 letter.to_ascii_lowercase(),
2067 "'".repeat((octave - 5) as usize)
2068 )
2069 } else {
2070 format!("{marks}{letter}{}", ",".repeat((4 - octave) as usize))
2071 })
2072}
2073
2074fn chord_text(
2075 chord: &Chord,
2076 alters: &Alters,
2077 length: &str,
2078 tied: impl Fn(&Note) -> bool,
2079) -> Result<String> {
2080 let mut text = decorations(chord.articulations(), chord.expressions())?;
2081 text.push('[');
2082 for note in chord.notes() {
2083 text.push_str(&pitch_text(note.pitch(), alters)?);
2084 if tied(note) {
2086 text.push('-');
2087 }
2088 }
2089 text.push(']');
2090 text.push_str(length);
2091 Ok(text)
2092}
2093
2094fn rest_text(rest: &Rest, length: &str) -> Result<String> {
2095 let marks = decorations(rest.articulations(), rest.expressions())?;
2096 let tied = matches!(
2097 rest.tie().map(crate::notation::Tie::tie_type),
2098 Some(TieType::Start | TieType::Continue)
2099 );
2100 Ok(format!(
2101 "{marks}{}{length}{}",
2102 if rest.hidden() { 'x' } else { 'z' },
2103 if tied { "-" } else { "" }
2104 ))
2105}
2106
2107fn symbol_text(symbol: &ChordSymbol) -> String {
2109 if symbol.is_no_chord() {
2110 return quoted(symbol.kind_text().unwrap_or("N.C."));
2111 }
2112 let figure: Vec<char> = symbol.figure().chars().collect();
2113 let mut text = String::new();
2114 for (index, character) in figure.iter().enumerate() {
2115 let after_letter = index > 0 && matches!(figure[index - 1], 'A'..='G');
2116 text.push(if *character == '-' && after_letter {
2117 'b'
2118 } else {
2119 *character
2120 });
2121 }
2122 quoted(&text)
2123}
2124
2125fn barline_text(
2134 right: Option<&Barline>,
2135 left: Option<&Barline>,
2136 ending: Option<&Ending>,
2137 opens_tune: bool,
2138 field_can_follow: bool,
2139) -> (String, bool) {
2140 let token = |barline: Option<&Barline>| -> Option<&'static str> {
2141 let barline = barline?;
2142 match barline.repeat_direction() {
2143 Some(RepeatDirection::End) => Some(":|"),
2144 Some(RepeatDirection::Start) => Some("|:"),
2145 None => match barline.bar_type() {
2146 BarlineType::Double => Some("||"),
2147 BarlineType::Final => Some("|]"),
2148 BarlineType::HeavyLight => Some("[|"),
2149 BarlineType::Dotted | BarlineType::Dashed => Some(":"),
2150 _ => None,
2151 },
2152 }
2153 };
2154 let repeats = |barline: Option<&Barline>| {
2155 barline.is_some_and(|barline| barline.repeat_direction().is_some())
2156 };
2157 let ending = ending.filter(|ending| ending.starts() && !ending.numbers().is_empty());
2158 let mut keep_off = false;
2159 let mut text = match (token(right), token(left)) {
2160 (Some(":|"), Some("|:")) => "::".to_string(),
2161 (Some(closing), Some(opening)) if closing == opening => closing.to_string(),
2162 (Some(":"), Some(opening)) => format!(": {opening}"),
2164 (Some(closing), Some(opening)) => format!("{closing}{opening}"),
2165 (Some(closing), None) if repeats(right) || opens_tune => closing.to_string(),
2166 (Some(closing), None) if field_can_follow && ending.is_none() => {
2169 keep_off = true;
2170 closing.to_string()
2171 }
2172 (Some(closing), None) => format!("{closing} |"),
2173 (None, Some(opening)) if opens_tune || repeats(left) => opening.to_string(),
2174 (None, Some(opening)) => format!("| {opening}"),
2177 (None, None) if opens_tune => String::new(),
2180 (None, None) => "|".to_string(),
2181 };
2182 if let Some(ending) = ending {
2183 let numbers = ending
2184 .numbers()
2185 .iter()
2186 .map(u32::to_string)
2187 .collect::<Vec<_>>()
2188 .join(",");
2189 if !numbers.is_empty() {
2190 if matches!(text.as_str(), "|" | ":|") && matches!(numbers.as_str(), "1" | "2") {
2193 text.push_str(&numbers);
2194 } else if matches!(numbers.as_str(), "1" | "2") {
2195 let _ = write!(text, "[{numbers}");
2196 } else if text.is_empty() {
2197 let _ = write!(text, "|{numbers}");
2198 } else {
2199 text.push_str(&numbers);
2200 }
2201 }
2202 }
2203 (text, keep_off)
2204}
2205
2206fn lyric_lines(sung: &[&[Lyric]]) -> Vec<String> {
2208 let mut numbers: Vec<IntegerType> = sung
2209 .iter()
2210 .flat_map(|lyrics| lyrics.iter().map(Lyric::number))
2211 .collect();
2212 numbers.sort_unstable();
2213 numbers.dedup();
2214 if numbers.is_empty() {
2215 numbers.push(1);
2218 }
2219 numbers
2220 .into_iter()
2221 .map(|number| {
2222 let mut words: Vec<String> = Vec::new();
2223 let mut word: Option<Vec<String>> = None;
2226 for lyrics in sung {
2227 let lyric = lyrics.iter().find(|lyric| lyric.number() == number);
2228 match lyric {
2229 None => match &mut word {
2230 Some(syllables) => syllables.push(String::new()),
2231 None => words.push("*".to_string()),
2232 },
2233 Some(lyric) => {
2234 let raw = lyric.raw_text();
2235 let continues = raw.len() > 1 && raw.ends_with('-');
2236 let syllable = lyric.text().replace(' ', "~").replace('-', "\\-");
2237 let syllable = if syllable.is_empty() {
2238 "*".to_string()
2239 } else {
2240 syllable
2241 };
2242 let joins = raw.len() > 1 && raw.starts_with('-');
2243 match &mut word {
2244 Some(syllables) if joins => syllables.push(syllable),
2245 _ => {
2246 if let Some(syllables) = word.take() {
2247 words.push(syllables.join("-"));
2248 }
2249 word = Some(vec![syllable]);
2250 }
2251 }
2252 if !continues && let Some(syllables) = word.take() {
2253 words.push(syllables.join("-"));
2254 }
2255 }
2256 }
2257 }
2258 if let Some(mut syllables) = word.take() {
2259 syllables.push(String::new());
2261 while syllables.len() > 2 && syllables[syllables.len() - 2].is_empty() {
2262 syllables.pop();
2263 }
2264 words.push(syllables.join("-"));
2265 }
2266 words.join(" ")
2267 })
2268 .collect()
2269}
2270
2271fn push_line(out: &mut String, line: &str) {
2272 for piece in line.split('\n') {
2275 let piece = piece.trim();
2276 if !piece.is_empty() {
2277 out.push_str(piece);
2278 out.push('\n');
2279 }
2280 }
2281}
2282
2283#[cfg(test)]
2284mod tests {
2285 use super::*;
2286 use crate::abc::from_abc;
2287
2288 fn round_trip(tune: &str) -> String {
2291 let score = from_abc(tune).unwrap();
2292 let written = to_abc(&score, &ExportOptions::default()).unwrap();
2293 assert_eq!(
2294 format!("{:#?}", from_abc(&written).unwrap()),
2295 format!("{score:#?}"),
2296 "read back differently:\n{written}"
2297 );
2298 written
2299 }
2300
2301 #[test]
2302 fn a_tune_is_written_with_its_header() {
2303 let written = round_trip(
2304 "X:7\nT:Tune\nT:Other\nC:Anon\nO:Here\nM:6/8\nL:1/8\nQ:3/8=120\nK:Ador\nABc def|gfe dcB|A6|]\n",
2305 );
2306 assert_eq!(
2307 written,
2308 "X:7\nT:Tune\nT:Other\nC:Anon\nO:Here\nM:6/8\nL:1/8\nQ:3/8=120\nK:Ador\nABc def | gfe dcB | A6 |]\n"
2309 );
2310 }
2311
2312 #[test]
2313 fn accidentals_octaves_and_lengths_are_written() {
2314 let written = round_trip("X:1\nM:4/4\nL:1/4\nK:G\n^C,/ _e'3/2 =F f/4 z3/4|c4|C4|]\n");
2315 assert!(
2316 written.contains("L:1/8\nK:G\n^C, _e'3 =F2 f/ z3/2 | c8 | C8 |]"),
2317 "{written}"
2318 );
2319 }
2320
2321 fn part_of(names: &[&str], opening: Vec<StreamElement>) -> Stream {
2324 let mut part = Stream::with_kind(StreamKind::Part);
2325 for (index, name) in names.iter().enumerate() {
2326 let mut measure = Stream::with_kind(StreamKind::Measure);
2327 if index == 0 {
2328 for element in &opening {
2329 measure.insert(0.0, element.clone());
2330 }
2331 }
2332 measure.insert(
2333 0.0,
2334 Note::from_name(name)
2335 .unwrap()
2336 .with_duration(Duration::whole()),
2337 );
2338 part.insert(index as FloatType * 4.0, measure);
2339 }
2340 part
2341 }
2342
2343 #[test]
2344 fn a_part_says_its_name_and_its_instrument_s_program() {
2345 let mut part = part_of(
2346 &["C4", "D4"],
2347 vec![StreamElement::TimeSignature(
2348 TimeSignature::new(4, 4).unwrap(),
2349 )],
2350 );
2351 part.set_name(Some("Guitar".to_string()));
2352 let mut guitar = crate::instrument::Instrument::new();
2353 guitar.set_midi_program(Some(24));
2354 part.insert(0.0, StreamElement::Instrument(Box::new(guitar)));
2355 let written = to_abc(&part, &ExportOptions::default()).unwrap();
2356 assert!(
2357 written.contains(
2358 "V:1 name=\"Guitar\"
2359%%MIDI program 24
2360"
2361 ),
2362 "{written}"
2363 );
2364 }
2365
2366 #[test]
2367 fn a_clef_other_than_treble_is_said_even_where_the_notes_imply_it() {
2368 let low = part_of(
2369 &["C2", "G2"],
2370 vec![
2371 StreamElement::Clef(Clef::of_kind(crate::clef::ClefKind::BassClef)),
2372 StreamElement::TimeSignature(TimeSignature::new(4, 4).unwrap()),
2373 ],
2374 );
2375 let written = to_abc(&low, &ExportOptions::default()).unwrap();
2376 assert!(written.contains("clef=bass"), "{written}");
2377 let high = part_of(
2378 &["C5", "G5"],
2379 vec![
2380 StreamElement::Clef(Clef::of_kind(crate::clef::ClefKind::TrebleClef)),
2381 StreamElement::TimeSignature(TimeSignature::new(4, 4).unwrap()),
2382 ],
2383 );
2384 let written = to_abc(&high, &ExportOptions::default()).unwrap();
2385 assert!(!written.contains("clef="), "{written}");
2386 }
2387}