1use crate::bar::Ending;
24use crate::chordsymbol::ChordSymbol;
25use crate::defaults::{FloatType, IntegerType};
26use crate::duration::{Duration, limited_fraction};
27use crate::error::{Error, Result};
28use crate::expressions::python_float;
29use crate::key::Key;
30use crate::makenotation::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::RomanText(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::RomanText("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::RomanText(
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::RomanText(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 + 1;
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, Default, PartialEq)]
549#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
550pub struct TabChord {
551 pub version: Version,
553 pub chord: String,
555 pub numeral: String,
557 pub relativeroot: Option<String>,
559 pub form: Option<String>,
561 pub figbass: Option<String>,
563 pub changes: Option<String>,
565 pub phraseend: Option<String>,
567 pub pedal: Option<String>,
569 pub local_key: String,
571 pub global_key: String,
573 pub beat: FloatType,
575 pub measure: i32,
577 pub timesig: String,
579 pub volta: Option<String>,
582 pub length: Option<FloatType>,
584 pub extra: Vec<(String, String)>,
586}
587
588impl TabChord {
589 pub fn from_row(header: &[String], row: &[String], version: Version) -> Result<Self> {
593 let mut read = Self {
594 version,
595 ..Self::default()
596 };
597 let read_columns = version.read_columns();
598 for (index, name) in header.iter().enumerate() {
599 let Some(cell) = row.get(index) else {
600 continue;
601 };
602 if !read_columns.contains(&name.as_str()) {
603 if !cell.is_empty() {
604 read.extra.push((name.clone(), cell.clone()));
605 }
606 continue;
607 }
608 let text = cell.clone();
609 match name.as_str() {
610 "chord" => read.chord = text,
611 "numeral" => read.numeral = text,
612 "relativeroot" => read.relativeroot = Some(text),
613 "form" => read.form = Some(text),
614 "figbass" => read.figbass = Some(text),
615 "changes" => read.changes = Some(text),
616 "phraseend" => read.phraseend = Some(text),
617 "pedal" => read.pedal = Some(text),
618 "timesig" => read.timesig = text,
619 "volta" => read.volta = Some(text),
620 "local_key" | "localkey" => read.local_key = text,
621 "global_key" | "globalkey" => read.global_key = text,
622 "measure" | "mn" => {
623 read.measure = text.parse().map_err(|_| {
624 Error::RomanText(format!("{name} is not a measure number: {text}"))
625 })?;
626 }
627 "beat" => {
628 read.beat = text
629 .parse()
630 .map_err(|_| Error::RomanText(format!("beat is not a number: {text}")))?;
631 }
632 "mn_onset" => read.beat = float_or_fraction(&text)? * 4.0 + 1.0,
635 "length" => {
638 read.length = Some(text.parse().map_err(|_| {
639 Error::RomanText(format!("length is not a number: {text}"))
640 })?);
641 }
642 _ => {}
648 }
649 }
650 Ok(read)
651 }
652
653 fn music21_label(&self) -> String {
657 let numeral = character_swaps(&self.numeral, Direction::IntoMusic21);
662 let relativeroot = self
663 .relativeroot
664 .as_deref()
665 .filter(|root| !root.is_empty())
666 .map(|root| character_swaps(root, Direction::IntoMusic21));
667 let form = self
668 .form
669 .as_deref()
670 .map(|form| form.replace('%', "ø"))
671 .filter(|form| !form.is_empty());
672 if self.version == Version::Two && !self.chord.is_empty() {
673 let mut label = handle_added_tones(&self.chord.replace('%', "ø"));
674 if self
677 .extra
678 .iter()
679 .any(|(name, value)| name == "chord_type" && value == "Mm7")
680 && self.numeral != "V"
681 {
682 label = prefix_sevenths(&label);
683 }
684 return label;
685 }
686 let mut label = format!(
687 "{numeral}{}{}",
688 form.unwrap_or_default(),
689 self.figbass.clone().unwrap_or_default()
690 );
691 if let Some(root) = relativeroot.filter(|root| !root.is_empty()) {
692 label.push('/');
693 label.push_str(&root);
694 }
695 label
696 }
697
698 pub fn key(&self) -> Result<Key> {
705 let local = character_swaps(&self.local_key, Direction::IntoMusic21);
706 let named = if is_degree(&local) {
707 local_key(&local, &self.global_key, false)?
708 } else {
709 local
710 };
711 Key::from_tonic(&named)
712 }
713
714 pub fn element(&self) -> Result<StreamElement> {
724 if self.numeral.is_empty() || self.numeral == "@none" {
725 return Ok(no_chord().into());
726 }
727 let numeral = RomanNumeral::with_minor_defaults(
728 self.music21_label(),
729 self.key()?,
730 Minor67Default::Flat,
731 Minor67Default::Flat,
732 )?;
733 let chord = numeral
734 .to_chord()?
735 .with_duration(Duration::new(0.0)?)
736 .with_numeral(numeral);
737 Ok(chord.into())
738 }
739}
740
741fn float_or_fraction(text: &str) -> Result<FloatType> {
744 if let Ok(number) = text.parse::<FloatType>() {
745 return Ok(number);
746 }
747 let (numerator, denominator) = text
748 .split_once('/')
749 .ok_or_else(|| Error::RomanText(format!("not a number: {text}")))?;
750 let numerator: FloatType = numerator
751 .parse()
752 .map_err(|_| Error::RomanText(format!("not a number: {text}")))?;
753 let denominator: FloatType = denominator
754 .parse()
755 .map_err(|_| Error::RomanText(format!("not a number: {text}")))?;
756 if denominator == 0.0 {
757 return Err(Error::RomanText(format!("not a number: {text}")));
758 }
759 Ok(numerator / denominator)
760}
761
762pub fn tsv_to_chords(text: &str, version: Version) -> Result<Vec<TabChord>> {
768 let mut lines = text.lines().filter(|line| !line.trim().is_empty());
769 let header: Vec<String> = match lines.next() {
770 Some(line) => cells(line),
771 None => return Err(Error::RomanText("a table with no header".to_string())),
772 };
773 lines
774 .map(|line| TabChord::from_row(&header, &cells(line), version))
775 .collect()
776}
777
778fn cells(line: &str) -> Vec<String> {
781 line.split('\t')
782 .map(|cell| {
783 let cell = cell.trim_end_matches('\r');
784 match cell
785 .strip_prefix('"')
786 .and_then(|rest| rest.strip_suffix('"'))
787 {
788 Some(inside) => inside.replace("\"\"", "\""),
789 None => cell.to_string(),
790 }
791 })
792 .collect()
793}
794
795pub fn from_tsv(text: &str, version: Version) -> Result<Stream> {
821 let rows = tsv_to_chords(text, version)?;
822 let Some(first) = rows.first() else {
823 return Err(Error::RomanText("a table with no rows".to_string()));
824 };
825 let mut part = Stream::with_kind(StreamKind::Part);
826 let key = Key::from_tonic(&character_swaps(&first.global_key, Direction::IntoMusic21))?;
827 let mut meter = TimeSignature::from_ratio_string(&first.timesig)?;
828 let mut said_timesig = first.timesig.clone();
829 let mut measure_length = meter.bar_duration().quarter_length();
830 let mut offset = 0.0;
831 let mut previous_measure = first.measure - 1;
832 let mut previous_volta = String::new();
833 let mut measures: Vec<(String, FloatType, Stream)> = Vec::new();
836 let new_measure = |number: i32, volta: Option<&str>, offset: FloatType| {
837 let mut measure = Stream::with_kind(StreamKind::Measure);
838 measure.set_number(number);
839 let suffix = volta.and_then(volta_suffix);
840 if let Some(suffix) = &suffix {
841 measure.set_number_suffix(Some(suffix.clone()));
842 }
843 let name = format!("{number}{}", suffix.unwrap_or_default());
844 (name, offset, measure)
845 };
846 for row in &rows {
847 let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
848 if version == Version::Two && row.volta.as_deref().unwrap_or("") != previous_volta {
849 previous_volta = row.volta.clone().unwrap_or_default();
850 } else if row.measure == previous_measure {
851 continue;
854 }
855 if row.measure > previous_measure + 1 {
856 for number in (previous_measure + 1)..=row.measure {
858 offset += measure_length;
859 measures.push(new_measure(number, None, offset));
860 previous_measure = number;
861 }
862 continue;
863 }
864 offset += measure_length;
865 let (name, at, mut measure) = new_measure(row.measure, volta, offset);
866 if let Some(volta) = volta.and_then(|volta| volta.parse::<u32>().ok()) {
872 measure.set_ending(Some(Ending::new(vec![volta], true, true)));
873 }
874 if row.timesig != said_timesig {
875 let said = TimeSignature::from_ratio_string(&row.timesig)?;
876 measure.insert(offset_in_measure(row.beat), said.clone());
877 said_timesig = row.timesig.clone();
878 measure_length = said.bar_duration().quarter_length();
879 meter = said;
880 }
881 measures.push((name, at, measure));
882 previous_measure = row.measure;
883 }
884 let _ = &meter;
885 if let Some((_, _, measure)) = measures.first_mut() {
887 measure.insert(0.0, key);
888 measure.insert(0.0, TimeSignature::from_ratio_string(&first.timesig)?);
889 }
890 for row in &rows {
891 let volta = row.volta.as_deref().filter(|volta| !volta.is_empty());
892 let name = format!(
893 "{}{}",
894 row.measure,
895 volta.and_then(volta_suffix).unwrap_or_default()
896 );
897 let Some((_, _, measure)) = measures.iter_mut().find(|(known, _, _)| *known == name) else {
898 return Err(Error::RomanText(format!("no measure {name} to analyse")));
899 };
900 measure.insert(offset_in_measure(row.beat), row.element()?);
901 }
902 for (_, at, measure) in measures {
903 part.insert(at, measure);
904 }
905 lengthen_harmonies(&mut part)?;
906 let mut score = Stream::with_kind(StreamKind::Score);
907 score.insert(0.0, part);
908 Ok(score)
909}
910
911fn volta_suffix(volta: &str) -> Option<String> {
914 let number: usize = volta.parse().ok()?;
915 let letter = char::from(b'a' + u8::try_from(number.checked_sub(1)?).ok()?);
916 letter.is_ascii_lowercase().then(|| letter.to_string())
917}
918
919fn lengthen_harmonies(part: &mut Stream) -> Result<()> {
929 let places = harmony_places(part);
931 let total = part.end_offset();
932 for (index, (offset, measure, event)) in places.iter().enumerate() {
933 let until = places.get(index + 1).map_or(total, |(next, _, _)| *next);
934 set_length(part, *measure, *event, op_frac(until - offset))?;
935 }
936 if let Some((offset, measure, event)) = places.last() {
939 let total = part.end_offset();
940 set_length(part, *measure, *event, op_frac(total - offset))?;
941 }
942 Ok(())
943}
944
945fn harmony_places(part: &Stream) -> Vec<(FloatType, usize, usize)> {
948 let mut places = Vec::new();
949 for (measure, event) in part.events().iter().enumerate() {
950 let Some(inner) = event.element().as_stream() else {
951 continue;
952 };
953 for (index, held) in inner.events().iter().enumerate() {
954 if held.element().is_instance("Harmony") {
955 places.push((op_frac(event.offset() + held.offset()), measure, index));
956 }
957 }
958 }
959 places.sort_by(|left, right| left.0.total_cmp(&right.0));
960 places
961}
962
963fn set_length(
965 part: &mut Stream,
966 measure: usize,
967 event: usize,
968 quarter_length: FloatType,
969) -> Result<()> {
970 let Some(StreamElement::Stream(inner)) = part
971 .events_mut()
972 .get_mut(measure)
973 .map(StreamEvent::element_mut)
974 else {
975 return Ok(());
976 };
977 let Some(held) = inner.events_mut().get_mut(event) else {
978 return Ok(());
979 };
980 let duration = Duration::new(quarter_length.max(0.0))?;
981 match held.element_mut() {
982 StreamElement::Chord(chord) => chord.set_duration(duration),
983 StreamElement::ChordSymbol(symbol) => symbol.set_duration(duration),
984 _ => {}
985 }
986 Ok(())
987}
988
989pub fn to_tsv(stream: &Stream, version: Version) -> Result<String> {
1018 to_tsv_with_extra(stream, version, &[])
1019}
1020
1021pub fn to_tsv_with_extra(
1037 stream: &Stream,
1038 version: Version,
1039 extra: &[Vec<(String, String)>],
1040) -> Result<String> {
1041 let written = written_chords(stream, version);
1042 let mut text = String::new();
1043 write_row(
1044 &mut text,
1045 version.columns().iter().map(|&name| name.to_string()),
1046 );
1047 let Some(global) = written.iter().find_map(|written| written.numeral) else {
1049 if version == Version::One {
1050 return Err(Error::RomanText(
1053 "a score with no roman numeral to write".to_string(),
1054 ));
1055 }
1056 return Ok(text);
1057 };
1058 let global_key = global.key().clone();
1059 let global_name = music21_key_name(&global_key);
1060 for (index, written) in written.iter().enumerate() {
1061 let attributes = match version {
1062 Version::One => v1_attributes(written, &global_name)?,
1063 Version::Two => v2_attributes(written, &global_name, &global_key)?,
1064 };
1065 let kept = extra.get(index).map(Vec::as_slice).unwrap_or_default();
1066 let cells = version.columns().iter().map(|name| {
1067 cell_of(&attributes, name)
1071 .or_else(|| cell_of(kept, name))
1072 .unwrap_or_default()
1073 });
1074 write_row(&mut text, cells);
1075 }
1076 Ok(text)
1077}
1078
1079fn cell_of<Name: AsRef<str>>(cells: &[(Name, String)], name: &str) -> Option<String> {
1081 cells
1082 .iter()
1083 .find(|(known, _)| known.as_ref() == name)
1084 .map(|(_, value)| value.clone())
1085}
1086
1087fn v1_attributes(written: &Written<'_>, global_key: &str) -> Result<Vec<(&'static str, String)>> {
1090 let numeral = written
1091 .numeral
1092 .ok_or_else(|| Error::RomanText("a chord of no harmony has no row here".to_string()))?;
1093 let relativeroot = numeral.secondary().unwrap_or_default().to_string();
1097 Ok(vec![
1098 ("chord", numeral.figure().to_string()),
1099 ("altchord", relativeroot.clone()),
1100 ("measure", written.measure.unwrap_or(1).to_string()),
1101 ("beat", python_float(written.beat()?)),
1102 ("totbeat", String::new()),
1103 ("timesig", written.timesig()),
1104 ("length", python_number(written.length)),
1105 ("global_key", global_key.to_string()),
1106 ("local_key", music21_key_name(numeral.key())),
1107 ("pedal", String::new()),
1108 ("numeral", numeral.roman_numeral()),
1109 ("form", form_of(numeral.figure()).to_string()),
1110 (
1111 "figbass",
1112 figured_bass_of(numeral.figures_written()).to_string(),
1113 ),
1114 ("changes", String::new()),
1115 ("relativeroot", relativeroot),
1116 ("phraseend", String::new()),
1117 ])
1118}
1119
1120fn v2_attributes(
1124 written: &Written<'_>,
1125 global_key: &str,
1126 global: &Key,
1127) -> Result<Vec<(&'static str, String)>> {
1128 let mut local_key = String::new();
1129 let mut form = String::new();
1130 let mut figbass = String::new();
1131 let mut relativeroot = String::new();
1132 let (chord, numeral_text) = match written.numeral {
1133 None => ("@none".to_string(), "@none".to_string()),
1134 Some(numeral) => {
1135 local_key = local_key_as_roman(numeral.key(), global)?;
1136 if let Some(root) = numeral.secondary() {
1137 relativeroot = character_swaps(root, Direction::IntoDcml);
1138 }
1139 form = form_of(numeral.figure()).to_string();
1140 figbass = figured_bass_of(numeral.figures_written()).to_string();
1141 (
1142 numeral.figure().replacen('d', "", 1),
1146 numeral.roman_numeral(),
1147 )
1148 }
1149 };
1150 Ok(vec![
1151 (
1152 "mn",
1153 written
1154 .measure
1155 .map(|number| number.to_string())
1156 .unwrap_or_default(),
1157 ),
1158 ("mn_onset", python_number(written.offset / 4.0)),
1162 ("timesig", written.timesig()),
1163 ("volta", String::new()),
1164 ("globalkey", global_key.to_string()),
1165 ("localkey", local_key),
1166 ("pedal", String::new()),
1167 ("chord", chord),
1168 ("numeral", numeral_text),
1169 ("form", form),
1170 ("figbass", figbass),
1171 ("changes", String::new()),
1172 ("relativeroot", relativeroot),
1173 ("phraseend", String::new()),
1174 ])
1175}
1176
1177#[derive(Debug)]
1180struct Written<'a> {
1181 numeral: Option<&'a RomanNumeral>,
1182 measure: Option<IntegerType>,
1183 offset: FloatType,
1184 padding: FloatType,
1185 length: FloatType,
1186 meter: Option<TimeSignature>,
1187}
1188
1189impl Written<'_> {
1190 fn timesig(&self) -> String {
1192 self.meter
1193 .as_ref()
1194 .map(TimeSignature::ratio_string)
1195 .unwrap_or_default()
1196 }
1197
1198 fn beat(&self) -> Result<FloatType> {
1206 let meter = self.meter.as_ref().ok_or_else(|| {
1207 Error::RomanText("a chord with no meter in force does not have a beat".to_string())
1208 })?;
1209 meter.beat_proportion(op_frac(self.offset + self.padding))
1210 }
1211}
1212
1213fn written_chords(stream: &Stream, version: Version) -> Vec<Written<'_>> {
1218 let mut found = Vec::new();
1219 let mut meter = None;
1220 read_chords(stream, version, None, &mut meter, &mut found);
1221 found
1222}
1223
1224fn read_chords<'a>(
1227 stream: &'a Stream,
1228 version: Version,
1229 measure: Option<(IntegerType, FloatType)>,
1230 meter: &mut Option<TimeSignature>,
1231 found: &mut Vec<Written<'a>>,
1232) {
1233 for event in stream.events() {
1234 match event.element() {
1235 StreamElement::TimeSignature(said) => *meter = Some(said.clone()),
1238 StreamElement::Stream(inner) => {
1239 let inside = if inner.kind() == StreamKind::Measure {
1240 Some((inner.number(), inner.padding_left()))
1241 } else {
1242 measure
1243 };
1244 read_chords(inner, version, inside, meter, found);
1245 }
1246 element => {
1247 let numeral = match element {
1248 StreamElement::Chord(chord) => chord.numeral(),
1249 _ => None,
1250 };
1251 let no_chord = matches!(
1252 element,
1253 StreamElement::ChordSymbol(symbol) if symbol.is_no_chord()
1254 );
1255 if numeral.is_none() && !(version == Version::Two && no_chord) {
1256 continue;
1257 }
1258 found.push(Written {
1259 numeral,
1260 measure: measure.map(|(number, _)| number),
1261 offset: event.offset(),
1262 padding: measure.map_or(0.0, |(_, padding)| padding),
1263 length: element.quarter_length(),
1264 meter: meter.clone(),
1265 });
1266 }
1267 }
1268 }
1269}
1270
1271fn write_row(text: &mut String, cells: impl Iterator<Item = String>) {
1275 let mut first = true;
1276 for cell in cells {
1277 if !first {
1278 text.push('\t');
1279 }
1280 first = false;
1281 if cell.contains(['\t', '"', '\r', '\n']) {
1282 text.push('"');
1283 text.push_str(&cell.replace('"', "\"\""));
1284 text.push('"');
1285 } else {
1286 text.push_str(&cell);
1287 }
1288 }
1289 text.push_str("\r\n");
1290}
1291
1292fn python_number(value: FloatType) -> String {
1296 if is_fraction(value)
1297 && let Some((numerator, denominator)) = limited_fraction(value, DENOMINATOR_LIMIT)
1298 {
1299 return format!("{numerator}/{denominator}");
1300 }
1301 python_float(value)
1302}
1303
1304fn is_fraction(value: FloatType) -> bool {
1308 (value * (DENOMINATOR_LIMIT as FloatType + 1.0)).fract() != 0.0
1309}
1310
1311const DENOMINATOR_LIMIT: i128 = 65535;
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317
1318 #[test]
1322 fn a_number_a_table_leaves_out_is_refused() {
1323 let header = "chord\tmeasure\tbeat\ttimesig\tglobal_key\tlocal_key\tnumeral\tlength\n";
1324 let whole = format!("{header}I\t1\t1.0\t4/4\tC\tC\tI\t4.0\n");
1325 assert!(tsv_to_chords(&whole, Version::One).is_ok());
1326 for missing in [
1327 "I\t\t1.0\t4/4\tC\tC\tI\t4.0\n",
1328 "I\t1\t\t4/4\tC\tC\tI\t4.0\n",
1329 "I\t1\t1.0\t4/4\tC\tC\tI\t\n",
1330 ] {
1331 let table = format!("{header}{missing}");
1332 assert!(
1333 tsv_to_chords(&table, Version::One).is_err(),
1334 "a table with a number left out reads: {missing:?}"
1335 );
1336 }
1337 }
1338
1339 #[test]
1341 fn the_notations_swap_as_music21_swaps_them() {
1342 assert_eq!(character_swaps("ii%", Direction::IntoMusic21), "iiø");
1343 assert_eq!(character_swaps("iiø", Direction::IntoDcml), "ii%");
1344 assert_eq!(character_swaps("ii/o", Direction::IntoDcml), "ii%");
1345 assert_eq!(character_swaps("VM7", Direction::IntoMusic21), "V7");
1346 assert_eq!(character_swaps("viio7", Direction::IntoMusic21), "viio7");
1347 assert!(!is_minor("F"));
1348 assert!(is_minor("f"));
1349 assert!(!is_minor("F#"));
1350 assert!(is_minor("bb"));
1351 }
1352
1353 #[test]
1355 fn the_form_column_reads_off_the_figure() {
1356 for (figure, form) in [
1357 ("V", ""),
1358 ("viio7", "o"),
1359 ("IVM7", "M"),
1360 ("III+", "+"),
1361 ("IV+M7", "+M"),
1362 ("viiø7", "%"),
1363 ("I", ""),
1364 ("V7", ""),
1365 ("viio", "o"),
1366 ] {
1367 assert_eq!(form_of(figure), form, "the form of {figure}");
1368 }
1369 assert_eq!(figured_bass_of("M43"), "43");
1370 assert_eq!(figured_bass_of("43"), "43");
1371 assert_eq!(figured_bass_of("M"), "");
1372 assert_eq!(figured_bass_of(""), "");
1373 }
1374
1375 #[test]
1377 fn added_tones_become_music21s_brackets() {
1378 for (label, written) in [
1379 ("V(64)", "Cad64"),
1380 ("i(4+2)", "i[no3][add4][add2]"),
1381 ("Viio7(b4-5)/V", "Viio7[no3][no5][addb4]/V"),
1382 ("vi(#74)", "vi[no3][add#7][add4]"),
1383 ("ii6(11#7b6)", "ii6[no8][no5][add11][add#7][addb6]"),
1384 ("i(#0)", "i[no1][add#7]"),
1385 ("V", "V"),
1386 ("V7(9)", "V7[add9]"),
1387 ("I(+6)", "I[add6]"),
1388 ("V(6)", "V[no5][add6]"),
1389 ("viio(b9)", "viio[addb9]"),
1390 ("I6(4)", "I6[no3][add4]"),
1391 ("V43(2)", "V43[no1][add2]"),
1392 ("i(v4)", "i[no3][add4]"),
1393 ("i(^4)", "i[no5][add4]"),
1394 ("V(13)", "V[add13]"),
1395 ] {
1396 assert_eq!(handle_added_tones(label), written, "the label {label}");
1397 }
1398 }
1399
1400 #[test]
1402 fn a_local_key_is_read_in_its_own_terms() {
1403 for (degree, global, from_dcml, named) in [
1404 ("V", "G", false, "D"),
1405 ("ii", "C", false, "d"),
1406 ("i", "C", false, "c"),
1407 ("#vii", "a", false, "g#"),
1408 ("vii", "a", true, "g"),
1409 ("bIII", "c", false, "Ebb"),
1412 ("V", "f", false, "C"),
1413 ("iv", "Eb", false, "ab"),
1414 ] {
1415 assert_eq!(
1416 local_key(degree, global, from_dcml).unwrap(),
1417 named,
1418 "{degree} of {global}"
1419 );
1420 }
1421 }
1422
1423 #[test]
1425 fn a_key_names_a_degree_of_another() {
1426 for (local, global, degree) in [
1427 ("C", "e", "VI"),
1428 ("C#", "e", "#VI"),
1429 ("e", "C", "iii"),
1430 ("f", "A-", "vi"),
1431 ("g#", "a", "#vii"),
1432 ] {
1433 let local = Key::from_tonic(local).unwrap();
1434 let global = Key::from_tonic(global).unwrap();
1435 assert_eq!(
1436 local_key_as_roman(&local, &global).unwrap(),
1437 degree,
1438 "the degree of the pair"
1439 );
1440 }
1441 assert_eq!(secondary_key("V/vi", "C").unwrap(), "a");
1442 assert_eq!(secondary_key("V", "C").unwrap(), "C");
1443 assert_eq!(secondary_key("viio7/ii", "G").unwrap(), "a");
1444 }
1445
1446 #[test]
1449 fn a_seventh_away_from_the_dominant_says_so() {
1450 assert_eq!(prefix_sevenths("II7"), "IId7");
1451 assert_eq!(prefix_sevenths("II65"), "IId65");
1452 assert_eq!(prefix_sevenths("I[add13]"), "I[add13]");
1453 assert_eq!(prefix_sevenths("II7[add9]"), "IId7[add9]");
1454 assert_eq!(prefix_sevenths("V"), "V");
1455 }
1456
1457 #[test]
1458 fn a_version_is_one_or_two() {
1459 assert_eq!(Version::from_number(1).unwrap(), Version::One);
1460 assert_eq!(Version::from_number(2).unwrap().number(), 2);
1461 assert!(Version::from_number(3).is_err());
1462 assert!(Version::One.columns().contains(&"global_key"));
1463 assert!(Version::Two.columns().contains(&"globalkey"));
1464 }
1465}