1use crate::bar::Ending;
24use crate::chordsymbol::ChordSymbol;
25use crate::defaults::{FloatType, IntegerType};
26use crate::duration::{Duration, limited_fraction};
27use crate::error::{Error, FormatErrorKind, Position, Result};
28use crate::expressions::python_float;
29use crate::key::Key;
30use crate::makenotation::{MAX_MEASURES, op_frac};
31use crate::meter::TimeSignature;
32use crate::roman::{Minor67Default, RomanNumeral};
33use crate::stream::{Stream, StreamElement, StreamEvent, StreamKind};
34
35use super::music21_key_name;
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum Version {
41 #[default]
43 One,
44 Two,
47}
48
49impl Version {
50 pub fn from_number(number: u8) -> Result<Self> {
56 match number {
57 1 => Ok(Self::One),
58 2 => Ok(Self::Two),
59 other => Err(Error::roman_text(format!(
60 "dcml_version {other} is not in (1, 2)"
61 ))),
62 }
63 }
64
65 pub fn number(self) -> u8 {
67 match self {
68 Self::One => 1,
69 Self::Two => 2,
70 }
71 }
72
73 pub fn read_columns(self) -> &'static [&'static str] {
77 match self {
78 Self::One => &[
79 "chord",
80 "altchord",
81 "measure",
82 "beat",
83 "totbeat",
84 "timesig",
85 "length",
86 "global_key",
87 "local_key",
88 "pedal",
89 "numeral",
90 "form",
91 "figbass",
92 "changes",
93 "relativeroot",
94 "phraseend",
95 ],
96 Self::Two => &[
97 "chord",
98 "mn",
99 "mn_onset",
100 "timesig",
101 "volta",
102 "globalkey",
103 "localkey",
104 "pedal",
105 "numeral",
106 "form",
107 "figbass",
108 "changes",
109 "relativeroot",
110 "phraseend",
111 "label",
112 ],
113 }
114 }
115
116 pub fn columns(self) -> &'static [&'static str] {
120 match self {
121 Self::One => &[
122 "chord",
123 "altchord",
124 "measure",
125 "beat",
126 "totbeat",
127 "timesig",
128 "op",
129 "no",
130 "mov",
131 "length",
132 "global_key",
133 "local_key",
134 "pedal",
135 "numeral",
136 "form",
137 "figbass",
138 "changes",
139 "relativeroot",
140 "phraseend",
141 ],
142 Self::Two => &[
143 "mc",
144 "mn",
145 "mc_onset",
146 "mn_onset",
147 "timesig",
148 "staff",
149 "voice",
150 "volta",
151 "label",
152 "globalkey",
153 "localkey",
154 "pedal",
155 "chord",
156 "special",
157 "numeral",
158 "form",
159 "figbass",
160 "changes",
161 "relativeroot",
162 "cadence",
163 "phraseend",
164 "chord_type",
165 "globalkey_is_minor",
166 "localkey_is_minor",
167 "chord_tones",
168 "added_tones",
169 "root",
170 "bass_note",
171 ],
172 }
173 }
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub enum Direction {
179 IntoMusic21,
181 IntoDcml,
183}
184
185pub fn is_minor(key: &str) -> bool {
197 key == key.to_lowercase()
198}
199
200pub fn character_swaps(written: &str, direction: Direction) -> String {
213 match direction {
214 Direction::IntoDcml => written.replace("/o", "%").replace('ø', "%"),
215 Direction::IntoMusic21 => written.replace('%', "ø").replace("M7", "7"),
216 }
217}
218
219pub fn local_key(degree: &str, global_key: &str, from_dcml: bool) -> Result<String> {
230 let degree = if from_dcml {
231 character_swaps(degree, Direction::IntoMusic21)
232 } else {
233 degree.to_string()
234 };
235 let numeral = RomanNumeral::with_minor_defaults(
236 °ree,
237 Key::from_tonic(global_key)?,
238 Minor67Default::Flat,
239 Minor67Default::Flat,
240 )?;
241 let chord = numeral.to_chord()?;
242 let root = chord
243 .root()
244 .ok_or_else(|| Error::roman_text("a local key with no root".to_string()))?
245 .name();
246 let cased = |upper: bool| -> String {
250 let mut letters = root.chars();
251 let step = letters.next().unwrap_or_default();
252 let step = if upper {
253 step.to_ascii_uppercase()
254 } else {
255 step.to_ascii_lowercase()
256 };
257 format!("{step}{}", letters.as_str())
258 };
259 if chord.is_major_triad() {
260 return Ok(cased(true));
261 }
262 if chord.is_minor_triad() {
263 return Ok(cased(false));
264 }
265 Err(Error::roman_text(
266 "local key must be major or minor".to_string(),
267 ))
268}
269
270pub fn local_key_as_roman(local: &Key, global: &Key) -> Result<String> {
278 let letter = local.tonic_pitch_name_with_case();
279 let figure = if letter.chars().next().is_some_and(char::is_lowercase) {
280 "i"
281 } else {
282 "I"
283 };
284 let tonic = RomanNumeral::new(figure, local.clone())?.to_chord()?;
285 let found = crate::roman::roman_numeral_from_chord(&tonic, Some(global))?.ok_or_else(|| {
286 Error::roman_text(format!(
287 "{letter} names no degree of {}",
288 global.tonic_pitch_name_with_case()
289 ))
290 })?;
291 Ok(found.roman_numeral().to_string())
292}
293
294pub fn secondary_key(numeral: &str, local: &str) -> Result<String> {
301 match numeral.split_once('/') {
302 Some((_, secondary)) => local_key(secondary, local, false),
303 None => Ok(local.to_string()),
304 }
305}
306
307pub fn form_of(figure: &str) -> &'static str {
312 if figure.contains('ø') {
313 return "%";
314 }
315 if figure.contains('o') {
316 return "o";
317 }
318 if figure.contains("+M") {
319 return "+M";
320 }
321 if figure.contains('+') {
322 return "+";
323 }
324 if figure.contains('M') {
325 return "M";
326 }
327 ""
328}
329
330pub fn figured_bass_of(figures_written: &str) -> &str {
334 match figures_written.find(|mark: char| mark.is_ascii_digit()) {
335 Some(at) => &figures_written[at..],
336 None => "",
337 }
338}
339
340#[derive(Clone, Copy, Debug, PartialEq, Eq)]
342struct AddedTone<'a> {
343 added_or_removed: Option<char>,
345 above_or_below: Option<char>,
347 alteration: &'a str,
349 factor: &'a str,
351}
352
353fn added_tones(written: &str) -> Vec<AddedTone<'_>> {
357 let bytes = written.as_bytes();
358 let mut found = Vec::new();
359 let mut at = 0;
360 while at < bytes.len() {
361 let start = at;
362 let mut cursor = at;
363 let added_or_removed = match bytes.get(cursor) {
364 Some(mark @ (b'+' | b'-')) => {
365 cursor += 1;
366 Some(*mark as char)
367 }
368 _ => None,
369 };
370 let above_or_below = match bytes.get(cursor) {
371 Some(mark @ (b'^' | b'v')) => {
372 cursor += 1;
373 Some(*mark as char)
374 }
375 _ => None,
376 };
377 let alteration_start = cursor;
378 if let Some(mark @ (b'#' | b'b')) = bytes.get(cursor) {
379 while bytes.get(cursor) == Some(mark) {
380 cursor += 1;
381 }
382 }
383 let alteration = &written[alteration_start..cursor];
384 let digits = match (bytes.get(cursor), bytes.get(cursor + 1)) {
386 (Some(b'1'), Some(second)) if second.is_ascii_digit() => 2,
387 (Some(first), _) if first.is_ascii_digit() => 1,
388 _ => 0,
389 };
390 if digits == 0 {
391 at = start + written[start..].chars().next().map_or(1, char::len_utf8);
394 continue;
395 }
396 found.push(AddedTone {
397 added_or_removed,
398 above_or_below,
399 alteration,
400 factor: &written[cursor..cursor + digits],
401 });
402 at = cursor + digits;
403 }
404 found
405}
406
407pub fn handle_added_tones(label: &str) -> String {
425 let Some(open) = label.find('(') else {
428 return label.to_string();
429 };
430 let primary = &label[..open];
431 let rest = &label[open + 1..];
432 let Some(close) = rest.rfind(')').filter(|close| {
435 let after = &rest[close + 1..];
436 after.is_empty() || after.starts_with('/')
437 }) else {
438 return label.to_string();
439 };
440 let inside = &rest[..close];
441 let secondary = &rest[close + 1..];
442 if primary == "V" && inside == "64" {
443 return format!("Cad64{secondary}");
444 }
445 let figure_at = primary
448 .rfind(|mark: char| !mark.is_ascii_digit() && mark != '/')
449 .map_or(0, |at| {
450 at + primary[at..].chars().next().map_or(1, char::len_utf8)
451 });
452 let figure = &primary[figure_at..];
453 let threshold = match figure {
454 "" | "5" | "53" | "5/3" | "3" | "7" => 7,
455 _ => 8,
456 };
457 let mut omissions = String::new();
458 let mut additions = String::new();
459 for tone in added_tones(inside) {
460 if tone.added_or_removed == Some('-') {
461 omissions.push_str(&format!("[no{}]", tone.factor));
462 continue;
463 }
464 let factor: i32 = tone.factor.parse().unwrap_or(0);
465 let replace_above = if tone.added_or_removed == Some('+')
466 || factor >= threshold
467 || matches!(factor, 1 | 3 | 5)
468 {
469 None
473 } else if matches!(factor, 2 | 4 | 6) {
474 Some(tone.alteration == "#" || tone.above_or_below == Some('^'))
477 } else {
478 Some(tone.alteration != "b" && tone.above_or_below != Some('v'))
479 };
480 if let Some(above) = replace_above {
481 let replaced = if above { factor + 1 } else { factor - 1 };
482 omissions.push_str(&format!("[no{replaced}]"));
483 }
484 let factor = if factor == 0 { 7 } else { factor };
486 additions.push_str(&format!("[add{}{factor}]", tone.alteration));
487 }
488 format!("{primary}{omissions}{additions}{secondary}")
489}
490
491fn is_degree(local_key: &str) -> bool {
494 local_key
495 .chars()
496 .any(|mark| matches!(mark.to_ascii_lowercase(), 'i' | 'v'))
497}
498
499fn prefix_sevenths(label: &str) -> String {
503 let bytes = label.as_bytes();
504 let mut out = String::with_capacity(label.len() + 2);
505 let mut at = 0;
506 while at < bytes.len() {
507 if !bytes[at].is_ascii_digit() {
508 out.push(bytes[at] as char);
509 at += 1;
510 continue;
511 }
512 let start = at;
513 while bytes.get(at).is_some_and(u8::is_ascii_digit) {
514 at += 1;
515 }
516 if bytes.get(at) == Some(&b']') {
519 out.push_str(&label[start..at]);
520 } else {
521 out.push('d');
522 out.push_str(&label[start..at]);
523 }
524 }
525 out
526}
527
528pub(crate) fn no_chord() -> ChordSymbol {
531 ChordSymbol::no_chord(None)
532}
533
534pub(crate) fn offset_in_measure(beat: FloatType) -> FloatType {
537 beat - 1.0
538}
539
540#[derive(Clone, Debug, PartialEq)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550pub struct Spelling {
551 pub chord: String,
553 pub numeral: String,
555 pub form: Option<String>,
557 pub relativeroot: Option<String>,
559 pub local_key: String,
561}
562
563#[derive(Clone, Debug, Default, PartialEq)]
566#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
567pub struct TabChord {
568 pub version: Version,
570 pub chord: String,
572 pub numeral: String,
574 pub relativeroot: Option<String>,
576 pub form: Option<String>,
578 pub figbass: Option<String>,
580 pub changes: Option<String>,
582 pub phraseend: Option<String>,
584 pub pedal: Option<String>,
586 pub local_key: String,
588 pub global_key: String,
590 pub beat: FloatType,
592 pub measure: i32,
594 pub timesig: String,
596 pub volta: Option<String>,
599 pub length: Option<FloatType>,
601 pub extra: Vec<(String, String)>,
603}
604
605impl TabChord {
606 pub fn from_row(header: &[String], row: &[String], version: Version) -> Result<Self> {
610 let mut read = Self {
611 version,
612 ..Self::default()
613 };
614 let read_columns = version.read_columns();
615 for (index, name) in header.iter().enumerate() {
616 let Some(cell) = row.get(index) else {
617 continue;
618 };
619 if !read_columns.contains(&name.as_str()) {
620 if !cell.is_empty() {
621 read.extra.push((name.clone(), cell.clone()));
622 }
623 continue;
624 }
625 let text = cell.clone();
626 match name.as_str() {
627 "chord" => read.chord = text,
628 "numeral" => read.numeral = text,
629 "relativeroot" => read.relativeroot = Some(text),
630 "form" => read.form = Some(text),
631 "figbass" => read.figbass = Some(text),
632 "changes" => read.changes = Some(text),
633 "phraseend" => read.phraseend = Some(text),
634 "pedal" => read.pedal = Some(text),
635 "timesig" => read.timesig = text,
636 "volta" => read.volta = Some(text),
637 "local_key" | "localkey" => read.local_key = text,
638 "global_key" | "globalkey" => read.global_key = text,
639 "measure" | "mn" => {
640 read.measure = text.parse().map_err(|_| {
641 Error::roman_text(format!("{name} is not a measure number: {text}"))
642 })?;
643 }
644 "beat" => {
645 read.beat = text
646 .parse()
647 .map_err(|_| Error::roman_text(format!("beat is not a number: {text}")))?;
648 }
649 "mn_onset" => read.beat = float_or_fraction(&text)? * 4.0 + 1.0,
652 "length" => {
655 read.length = Some(text.parse().map_err(|_| {
656 Error::roman_text(format!("length is not a number: {text}"))
657 })?);
658 }
659 _ => {}
665 }
666 }
667 Ok(read)
668 }
669
670 pub fn spelled(&self, direction: Direction) -> Spelling {
677 let numeral = character_swaps(&self.numeral, direction);
682 let relativeroot = self
683 .relativeroot
684 .as_deref()
685 .filter(|root| !root.is_empty())
686 .map(|root| character_swaps(root, direction));
687 let local_key = character_swaps(&self.local_key, direction);
688 if direction == Direction::IntoDcml {
689 return Spelling {
690 chord: self.chord.clone(),
691 numeral,
692 form: self.form.clone(),
693 relativeroot,
694 local_key,
695 };
696 }
697 let form = self.form.as_deref().map(|form| form.replace('%', "ø"));
698 let mut chord = self.chord.clone();
699 if self.version == Version::Two && !chord.is_empty() {
700 chord = handle_added_tones(&chord.replace('%', "ø"));
701 if self
705 .extra
706 .iter()
707 .any(|(name, value)| name == "chord_type" && value == "Mm7")
708 && self.numeral != "V"
709 {
710 chord = prefix_sevenths(&chord);
711 }
712 }
713 Spelling {
714 chord,
715 numeral,
716 form,
717 relativeroot,
718 local_key,
719 }
720 }
721
722 fn spelled_row(&self, direction: Direction) -> Self {
724 let spelling = self.spelled(direction);
725 Self {
726 chord: spelling.chord,
727 numeral: spelling.numeral,
728 form: spelling.form,
729 relativeroot: spelling.relativeroot,
730 local_key: spelling.local_key,
731 ..self.clone()
732 }
733 }
734
735 pub fn label(&self) -> String {
744 if self.version == Version::Two && !self.chord.is_empty() {
745 return self.chord.clone();
746 }
747 let mut label = format!(
748 "{}{}{}",
749 self.numeral,
750 self.form
751 .as_deref()
752 .filter(|form| !form.is_empty())
753 .unwrap_or_default(),
754 self.figbass.as_deref().unwrap_or_default()
755 );
756 if let Some(root) = self.relativeroot.as_deref().filter(|root| !root.is_empty()) {
757 label.push('/');
758 label.push_str(root);
759 }
760 label
761 }
762
763 pub fn key(&self) -> Result<Key> {
770 let local = character_swaps(&self.local_key, Direction::IntoMusic21);
771 let named = if is_degree(&local) {
772 local_key(&local, &self.global_key, false)?
773 } else {
774 local
775 };
776 Key::from_tonic(&named)
777 }
778
779 pub fn element(&self) -> Result<StreamElement> {
789 self.spelled_row(Direction::IntoMusic21)
790 .element_as_spelled()
791 }
792
793 pub fn element_as_spelled(&self) -> Result<StreamElement> {
800 if self.numeral.is_empty() || self.numeral == "@none" {
801 return Ok(no_chord().into());
802 }
803 let numeral = RomanNumeral::with_minor_defaults(
804 self.label(),
805 self.key()?,
806 Minor67Default::Flat,
807 Minor67Default::Flat,
808 )?;
809 let chord = numeral
810 .to_chord()?
811 .with_duration(Duration::new(0.0)?)
812 .with_numeral(numeral);
813 Ok(chord.into())
814 }
815}
816
817fn float_or_fraction(text: &str) -> Result<FloatType> {
820 if let Ok(number) = text.parse::<FloatType>() {
821 return Ok(number);
822 }
823 let (numerator, denominator) = text
824 .split_once('/')
825 .ok_or_else(|| Error::roman_text(format!("not a number: {text}")))?;
826 let numerator: FloatType = numerator
827 .parse()
828 .map_err(|_| Error::roman_text(format!("not a number: {text}")))?;
829 let denominator: FloatType = denominator
830 .parse()
831 .map_err(|_| Error::roman_text(format!("not a number: {text}")))?;
832 if denominator == 0.0 {
833 return Err(Error::roman_text(format!("not a number: {text}")));
834 }
835 Ok(numerator / denominator)
836}
837
838pub fn tsv_to_chords(text: &str, version: Version) -> Result<Vec<TabChord>> {
844 let mut lines = text.lines().filter(|line| !line.trim().is_empty());
845 let header: Vec<String> = match lines.next() {
846 Some(line) => cells(line),
847 None => return Err(Error::roman_text("a table with no header".to_string())),
848 };
849 lines
850 .map(|line| TabChord::from_row(&header, &cells(line), version))
851 .collect()
852}
853
854fn cells(line: &str) -> Vec<String> {
857 line.split('\t')
858 .map(|cell| {
859 let cell = cell.trim_end_matches('\r');
860 match cell
861 .strip_prefix('"')
862 .and_then(|rest| rest.strip_suffix('"'))
863 {
864 Some(inside) => inside.replace("\"\"", "\""),
865 None => cell.to_string(),
866 }
867 })
868 .collect()
869}
870
871pub fn from_tsv(text: &str, version: Version) -> Result<Stream> {
897 let rows = tsv_to_chords(text, version)?;
898 let Some(first) = rows.first() else {
899 return Err(Error::roman_text("a table with no rows".to_string()));
900 };
901 let mut part = Stream::with_kind(StreamKind::Part);
902 let key = Key::from_tonic(&character_swaps(&first.global_key, Direction::IntoMusic21))?;
903 let mut meter = TimeSignature::from_ratio_string(&first.timesig)?;
904 let mut said_timesig = first.timesig.clone();
905 let mut measure_length = meter.bar_duration().quarter_length();
906 let mut offset = 0.0;
907 let mut previous_measure = i64::from(first.measure) - 1;
910 let mut previous_volta = String::new();
911 let mut measures: Vec<(String, FloatType, Stream)> = Vec::new();
914 let new_measure = |number: i32, volta: Option<&str>, offset: FloatType| {
915 let mut measure = Stream::with_kind(StreamKind::Measure);
916 measure.set_number(number);
917 let suffix = volta.and_then(volta_suffix);
918 if let Some(suffix) = &suffix {
919 measure.set_number_suffix(Some(suffix.clone()));
920 }
921 let name = format!("{number}{}", suffix.unwrap_or_default());
922 (name, offset, measure)
923 };
924 for row in &rows {
925 let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
926 if version == Version::Two && row.volta.as_deref().unwrap_or("") != previous_volta {
927 previous_volta = row.volta.clone().unwrap_or_default();
928 } else if i64::from(row.measure) == previous_measure {
929 continue;
932 }
933 if i64::from(row.measure) > previous_measure + 1 {
934 let made = measures.len() as i64 + i64::from(row.measure) - previous_measure;
935 if made > MAX_MEASURES as i64 {
936 return Err(Error::roman_text(format!(
937 "measure {} would make more than {MAX_MEASURES} measures",
938 row.measure
939 ))
940 .of_kind(FormatErrorKind::TooLarge)
941 .placed(|| Position::measure(row.measure.to_string())));
942 }
943 for number in (previous_measure + 1)..=i64::from(row.measure) {
945 offset += measure_length;
946 let number = IntegerType::try_from(number).unwrap_or(row.measure);
947 measures.push(new_measure(number, None, offset));
948 }
949 previous_measure = i64::from(row.measure);
950 continue;
951 }
952 offset += measure_length;
953 let (name, at, mut measure) = new_measure(row.measure, volta, offset);
954 if let Some(volta) = volta.and_then(|volta| volta.parse::<u32>().ok()) {
960 measure.set_ending(Some(Ending::new(vec![volta], true, true)));
961 }
962 if row.timesig != said_timesig {
963 let said = TimeSignature::from_ratio_string(&row.timesig)?;
964 measure.insert(offset_in_measure(row.beat), said.clone());
965 said_timesig = row.timesig.clone();
966 measure_length = said.bar_duration().quarter_length();
967 meter = said;
968 }
969 measures.push((name, at, measure));
970 previous_measure = i64::from(row.measure);
971 }
972 let _ = &meter;
973 if let Some((_, _, measure)) = measures.first_mut() {
975 measure.insert(0.0, key);
976 measure.insert(0.0, TimeSignature::from_ratio_string(&first.timesig)?);
977 }
978 for row in &rows {
979 let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
980 let name = format!(
981 "{}{}",
982 row.measure,
983 volta.and_then(volta_suffix).unwrap_or_default()
984 );
985 let Some((_, _, measure)) = measures.iter_mut().find(|(known, _, _)| *known == name) else {
986 return Err(Error::roman_text(format!("no measure {name} to analyse")));
987 };
988 measure.insert(offset_in_measure(row.beat), row.element()?);
989 }
990 part.insert_sorted(
992 measures
993 .into_iter()
994 .map(|(_, at, measure)| StreamEvent::new(at, measure))
995 .collect(),
996 );
997 lengthen_harmonies(&mut part)?;
998 let mut score = Stream::with_kind(StreamKind::Score);
999 score.insert(0.0, part);
1000 Ok(score)
1001}
1002
1003fn volta_suffix(volta: &str) -> Option<String> {
1006 let number: usize = volta.parse().ok()?;
1007 let letter = char::from(b'a'.checked_add(u8::try_from(number.checked_sub(1)?).ok()?)?);
1008 letter.is_ascii_lowercase().then(|| letter.to_string())
1009}
1010
1011fn lengthen_harmonies(part: &mut Stream) -> Result<()> {
1021 let places = harmony_places(part);
1023 let total = part.end_offset();
1024 for (index, (offset, measure, event)) in places.iter().enumerate() {
1025 let until = places.get(index + 1).map_or(total, |(next, _, _)| *next);
1026 set_length(part, *measure, *event, op_frac(until - offset))?;
1027 }
1028 if let Some((offset, measure, event)) = places.last() {
1031 let total = part.end_offset();
1032 set_length(part, *measure, *event, op_frac(total - offset))?;
1033 }
1034 Ok(())
1035}
1036
1037fn harmony_places(part: &Stream) -> Vec<(FloatType, usize, usize)> {
1040 let mut places = Vec::new();
1041 for (measure, event) in part.events().iter().enumerate() {
1042 let Some(inner) = event.element().as_stream() else {
1043 continue;
1044 };
1045 for (index, held) in inner.events().iter().enumerate() {
1046 if held.element().is_instance("Harmony") {
1047 places.push((op_frac(event.offset() + held.offset()), measure, index));
1048 }
1049 }
1050 }
1051 places.sort_by(|left, right| left.0.total_cmp(&right.0));
1052 places
1053}
1054
1055fn set_length(
1057 part: &mut Stream,
1058 measure: usize,
1059 event: usize,
1060 quarter_length: FloatType,
1061) -> Result<()> {
1062 let Some(StreamElement::Stream(inner)) = part
1063 .events_mut()
1064 .get_mut(measure)
1065 .map(StreamEvent::element_mut)
1066 else {
1067 return Ok(());
1068 };
1069 let Some(held) = inner.events_mut().get_mut(event) else {
1070 return Ok(());
1071 };
1072 let duration = Duration::new(quarter_length.max(0.0))?;
1073 match held.element_mut() {
1074 StreamElement::Chord(chord) => chord.set_duration(duration),
1075 StreamElement::ChordSymbol(symbol) => symbol.set_duration(duration),
1076 _ => {}
1077 }
1078 Ok(())
1079}
1080
1081pub fn to_tsv(stream: &Stream, version: Version) -> Result<String> {
1110 to_tsv_with_extra(stream, version, &[])
1111}
1112
1113pub fn to_tsv_with_extra(
1129 stream: &Stream,
1130 version: Version,
1131 extra: &[Vec<(String, String)>],
1132) -> Result<String> {
1133 let written = written_chords(stream, version);
1134 let mut text = String::new();
1135 write_row(
1136 &mut text,
1137 version.columns().iter().map(|&name| name.to_string()),
1138 );
1139 let Some(global) = written.iter().find_map(|written| written.numeral) else {
1141 if version == Version::One {
1142 return Err(Error::roman_text(
1145 "a score with no roman numeral to write".to_string(),
1146 ));
1147 }
1148 return Ok(text);
1149 };
1150 let global_key = global.key().clone();
1151 let global_name = music21_key_name(&global_key);
1152 for (index, written) in written.iter().enumerate() {
1153 let attributes = match version {
1154 Version::One => v1_attributes(written, &global_name)?,
1155 Version::Two => v2_attributes(written, &global_name, &global_key)?,
1156 };
1157 let kept = extra.get(index).map(Vec::as_slice).unwrap_or_default();
1158 let cells = version.columns().iter().map(|name| {
1159 cell_of(&attributes, name)
1163 .or_else(|| cell_of(kept, name))
1164 .unwrap_or_default()
1165 });
1166 write_row(&mut text, cells);
1167 }
1168 Ok(text)
1169}
1170
1171fn cell_of<Name: AsRef<str>>(cells: &[(Name, String)], name: &str) -> Option<String> {
1173 cells
1174 .iter()
1175 .find(|(known, _)| known.as_ref() == name)
1176 .map(|(_, value)| value.clone())
1177}
1178
1179fn v1_attributes(written: &Written<'_>, global_key: &str) -> Result<Vec<(&'static str, String)>> {
1182 let numeral = written
1183 .numeral
1184 .ok_or_else(|| Error::roman_text("a chord of no harmony has no row here".to_string()))?;
1185 let relativeroot = numeral.secondary().unwrap_or_default().to_string();
1189 Ok(vec![
1190 ("chord", numeral.figure().to_string()),
1191 ("altchord", relativeroot.clone()),
1192 ("measure", written.measure.unwrap_or(1).to_string()),
1193 ("beat", python_float(written.beat()?)),
1194 ("totbeat", String::new()),
1195 ("timesig", written.timesig()),
1196 ("length", python_number(written.length)),
1197 ("global_key", global_key.to_string()),
1198 ("local_key", music21_key_name(numeral.key())),
1199 ("pedal", String::new()),
1200 ("numeral", numeral.roman_numeral()),
1201 ("form", form_of(numeral.figure()).to_string()),
1202 (
1203 "figbass",
1204 figured_bass_of(numeral.figures_written()).to_string(),
1205 ),
1206 ("changes", String::new()),
1207 ("relativeroot", relativeroot),
1208 ("phraseend", String::new()),
1209 ])
1210}
1211
1212fn v2_attributes(
1216 written: &Written<'_>,
1217 global_key: &str,
1218 global: &Key,
1219) -> Result<Vec<(&'static str, String)>> {
1220 let mut local_key = String::new();
1221 let mut form = String::new();
1222 let mut figbass = String::new();
1223 let mut relativeroot = String::new();
1224 let (chord, numeral_text) = match written.numeral {
1225 None => ("@none".to_string(), "@none".to_string()),
1226 Some(numeral) => {
1227 local_key = local_key_as_roman(numeral.key(), global)?;
1228 if let Some(root) = numeral.secondary() {
1229 relativeroot = character_swaps(root, Direction::IntoDcml);
1230 }
1231 form = form_of(numeral.figure()).to_string();
1232 figbass = figured_bass_of(numeral.figures_written()).to_string();
1233 (
1234 numeral.figure().replacen('d', "", 1),
1238 numeral.roman_numeral(),
1239 )
1240 }
1241 };
1242 Ok(vec![
1243 (
1244 "mn",
1245 written
1246 .measure
1247 .map(|number| number.to_string())
1248 .unwrap_or_default(),
1249 ),
1250 ("mn_onset", python_number(written.offset / 4.0)),
1254 ("timesig", written.timesig()),
1255 ("volta", String::new()),
1256 ("globalkey", global_key.to_string()),
1257 ("localkey", local_key),
1258 ("pedal", String::new()),
1259 ("chord", chord),
1260 ("numeral", numeral_text),
1261 ("form", form),
1262 ("figbass", figbass),
1263 ("changes", String::new()),
1264 ("relativeroot", relativeroot),
1265 ("phraseend", String::new()),
1266 ])
1267}
1268
1269#[derive(Debug)]
1272struct Written<'a> {
1273 numeral: Option<&'a RomanNumeral>,
1274 measure: Option<IntegerType>,
1275 offset: FloatType,
1276 padding: FloatType,
1277 length: FloatType,
1278 meter: Option<TimeSignature>,
1279}
1280
1281impl Written<'_> {
1282 fn timesig(&self) -> String {
1284 self.meter
1285 .as_ref()
1286 .map(TimeSignature::ratio_string)
1287 .unwrap_or_default()
1288 }
1289
1290 fn beat(&self) -> Result<FloatType> {
1298 let meter = self.meter.as_ref().ok_or_else(|| {
1299 Error::roman_text("a chord with no meter in force does not have a beat".to_string())
1300 })?;
1301 meter.beat_proportion(op_frac(self.offset + self.padding))
1302 }
1303}
1304
1305fn written_chords(stream: &Stream, version: Version) -> Vec<Written<'_>> {
1310 let mut found = Vec::new();
1311 let mut meter = None;
1312 read_chords(stream, version, None, &mut meter, &mut found);
1313 found
1314}
1315
1316fn read_chords<'a>(
1319 stream: &'a Stream,
1320 version: Version,
1321 measure: Option<(IntegerType, FloatType)>,
1322 meter: &mut Option<TimeSignature>,
1323 found: &mut Vec<Written<'a>>,
1324) {
1325 for event in stream.events() {
1326 match event.element() {
1327 StreamElement::TimeSignature(said) => *meter = Some(said.clone()),
1330 StreamElement::Stream(inner) => {
1331 let inside = if inner.kind() == StreamKind::Measure {
1332 Some((inner.number(), inner.padding_left()))
1333 } else {
1334 measure
1335 };
1336 read_chords(inner, version, inside, meter, found);
1337 }
1338 element => {
1339 let numeral = match element {
1340 StreamElement::Chord(chord) => chord.numeral(),
1341 _ => None,
1342 };
1343 let no_chord = matches!(
1344 element,
1345 StreamElement::ChordSymbol(symbol) if symbol.is_no_chord()
1346 );
1347 if numeral.is_none() && !(version == Version::Two && no_chord) {
1348 continue;
1349 }
1350 found.push(Written {
1351 numeral,
1352 measure: measure.map(|(number, _)| number),
1353 offset: event.offset(),
1354 padding: measure.map_or(0.0, |(_, padding)| padding),
1355 length: element.quarter_length(),
1356 meter: meter.clone(),
1357 });
1358 }
1359 }
1360 }
1361}
1362
1363fn write_row(text: &mut String, cells: impl Iterator<Item = String>) {
1367 let mut first = true;
1368 for cell in cells {
1369 if !first {
1370 text.push('\t');
1371 }
1372 first = false;
1373 if cell.contains(['\t', '"', '\r', '\n']) {
1374 text.push('"');
1375 text.push_str(&cell.replace('"', "\"\""));
1376 text.push('"');
1377 } else {
1378 text.push_str(&cell);
1379 }
1380 }
1381 text.push_str("\r\n");
1382}
1383
1384fn python_number(value: FloatType) -> String {
1388 if is_fraction(value)
1389 && let Some((numerator, denominator)) = limited_fraction(value, DENOMINATOR_LIMIT)
1390 {
1391 return format!("{numerator}/{denominator}");
1392 }
1393 python_float(value)
1394}
1395
1396fn is_fraction(value: FloatType) -> bool {
1400 (value * (DENOMINATOR_LIMIT as FloatType + 1.0)).fract() != 0.0
1401}
1402
1403const DENOMINATOR_LIMIT: i128 = 65535;
1405
1406#[cfg(test)]
1407mod tests {
1408 use super::*;
1409
1410 #[test]
1414 fn a_number_a_table_leaves_out_is_refused() {
1415 let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\tlength\n";
1416 let whole = format!("{header}I\t1\t1.0\t4/4\tC\tC\tI\t4.0\n");
1417 assert!(tsv_to_chords(&whole, Version::One).is_ok());
1418 for missing in [
1419 "I\t\t1.0\t4/4\tC\tC\tI\t4.0\n",
1420 "I\t1\t\t4/4\tC\tC\tI\t4.0\n",
1421 "I\t1\t1.0\t4/4\tC\tC\tI\t\n",
1422 ] {
1423 let table = format!("{header}{missing}");
1424 assert!(
1425 tsv_to_chords(&table, Version::One).is_err(),
1426 "a table with a number left out reads: {missing:?}"
1427 );
1428 }
1429 }
1430
1431 #[test]
1433 fn the_notations_swap_as_music21_swaps_them() {
1434 assert_eq!(character_swaps("ii%", Direction::IntoMusic21), "iiø");
1435 assert_eq!(character_swaps("iiø", Direction::IntoDcml), "ii%");
1436 assert_eq!(character_swaps("ii/o", Direction::IntoDcml), "ii%");
1437 assert_eq!(character_swaps("VM7", Direction::IntoMusic21), "V7");
1438 assert_eq!(character_swaps("viio7", Direction::IntoMusic21), "viio7");
1439 assert!(!is_minor("F"));
1440 assert!(is_minor("f"));
1441 assert!(!is_minor("F#"));
1442 assert!(is_minor("bb"));
1443 }
1444
1445 #[test]
1447 fn the_form_column_reads_off_the_figure() {
1448 for (figure, form) in [
1449 ("V", ""),
1450 ("viio7", "o"),
1451 ("IVM7", "M"),
1452 ("III+", "+"),
1453 ("IV+M7", "+M"),
1454 ("viiø7", "%"),
1455 ("I", ""),
1456 ("V7", ""),
1457 ("viio", "o"),
1458 ] {
1459 assert_eq!(form_of(figure), form, "the form of {figure}");
1460 }
1461 assert_eq!(figured_bass_of("M43"), "43");
1462 assert_eq!(figured_bass_of("43"), "43");
1463 assert_eq!(figured_bass_of("M"), "");
1464 assert_eq!(figured_bass_of(""), "");
1465 }
1466
1467 #[test]
1469 fn added_tones_become_music21s_brackets() {
1470 for (label, written) in [
1471 ("V(64)", "Cad64"),
1472 ("i(4+2)", "i[no3][add4][add2]"),
1473 ("Viio7(b4-5)/V", "Viio7[no3][no5][addb4]/V"),
1474 ("vi(#74)", "vi[no3][add#7][add4]"),
1475 ("ii6(11#7b6)", "ii6[no8][no5][add11][add#7][addb6]"),
1476 ("i(#0)", "i[no1][add#7]"),
1477 ("V", "V"),
1478 ("V7(9)", "V7[add9]"),
1479 ("I(+6)", "I[add6]"),
1480 ("V(6)", "V[no5][add6]"),
1481 ("viio(b9)", "viio[addb9]"),
1482 ("I6(4)", "I6[no3][add4]"),
1483 ("V43(2)", "V43[no1][add2]"),
1484 ("i(v4)", "i[no3][add4]"),
1485 ("i(^4)", "i[no5][add4]"),
1486 ("V(13)", "V[add13]"),
1487 ("V(\u{f8}13)", "V[add13]"),
1489 ] {
1490 assert_eq!(handle_added_tones(label), written, "the label {label}");
1491 }
1492 }
1493
1494 #[test]
1496 fn a_local_key_is_read_in_its_own_terms() {
1497 for (degree, global, from_dcml, named) in [
1498 ("V", "G", false, "D"),
1499 ("ii", "C", false, "d"),
1500 ("i", "C", false, "c"),
1501 ("#vii", "a", false, "g#"),
1502 ("vii", "a", true, "g"),
1503 ("bIII", "c", false, "Ebb"),
1506 ("V", "f", false, "C"),
1507 ("iv", "Eb", false, "ab"),
1508 ] {
1509 assert_eq!(
1510 local_key(degree, global, from_dcml).unwrap(),
1511 named,
1512 "{degree} of {global}"
1513 );
1514 }
1515 }
1516
1517 #[test]
1519 fn a_key_names_a_degree_of_another() {
1520 for (local, global, degree) in [
1521 ("C", "e", "VI"),
1522 ("C#", "e", "#VI"),
1523 ("e", "C", "iii"),
1524 ("f", "A-", "vi"),
1525 ("g#", "a", "#vii"),
1526 ] {
1527 let local = Key::from_tonic(local).unwrap();
1528 let global = Key::from_tonic(global).unwrap();
1529 assert_eq!(
1530 local_key_as_roman(&local, &global).unwrap(),
1531 degree,
1532 "the degree of the pair"
1533 );
1534 }
1535 assert_eq!(secondary_key("V/vi", "C").unwrap(), "a");
1536 assert_eq!(secondary_key("V", "C").unwrap(), "C");
1537 assert_eq!(secondary_key("viio7/ii", "G").unwrap(), "a");
1538 }
1539
1540 #[test]
1543 fn a_seventh_away_from_the_dominant_says_so() {
1544 assert_eq!(prefix_sevenths("II7"), "IId7");
1545 assert_eq!(prefix_sevenths("II65"), "IId65");
1546 assert_eq!(prefix_sevenths("I[add13]"), "I[add13]");
1547 assert_eq!(prefix_sevenths("II7[add9]"), "IId7[add9]");
1548 assert_eq!(prefix_sevenths("V"), "V");
1549 }
1550
1551 #[test]
1552 fn a_version_is_one_or_two() {
1553 assert_eq!(Version::from_number(1).unwrap(), Version::One);
1554 assert_eq!(Version::from_number(2).unwrap().number(), 2);
1555 assert!(Version::from_number(3).is_err());
1556 assert!(Version::One.columns().contains(&"global_key"));
1557 assert!(Version::Two.columns().contains(&"globalkey"));
1558 }
1559
1560 #[test]
1563 fn a_table_skipping_many_measures_is_read_quickly() {
1564 let table = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n\
1565 .C.I\t1\t1.0\t4/4\tC\tI\tI\n\
1566 V\t20000\t1.0\t4/4\tC\tI\tV\n";
1567 let started = std::time::Instant::now();
1568 let read = from_tsv(table, Version::One).unwrap();
1569 assert_eq!(read.parts()[0].measures().len(), 20000);
1570 assert!(started.elapsed() < std::time::Duration::from_secs(10));
1571 }
1572
1573 #[test]
1574 fn a_table_of_more_measures_than_a_stream_takes_is_refused() {
1575 let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\n";
1576 let gap = crate::makenotation::MAX_MEASURES + 2;
1577 for (first, last) in [(1, gap as i64), (-2_147_483_648, 1), (1, 2_147_483_647)] {
1578 let table =
1579 format!("{header}.C.I\t{first}\t1.0\t4/4\tC\tI\tI\nV\t{last}\t1.0\t4/4\tC\tI\tV\n");
1580 assert!(from_tsv(&table, Version::One).is_err(), "{first} {last}");
1581 }
1582 }
1583
1584 #[test]
1585 fn a_volta_past_z_has_no_letter() {
1586 assert_eq!(volta_suffix("2").as_deref(), Some("b"));
1587 assert_eq!(volta_suffix("26").as_deref(), Some("z"));
1588 assert_eq!(volta_suffix("27"), None);
1589 assert_eq!(volta_suffix("200"), None);
1590 }
1591}