1mod generated;
18
19use crate::chord::ChordTableAddress;
20use crate::defaults::IntegerType;
21use crate::error::Error;
22
23use generated::*;
24
25pub(crate) type RawAddress = (u8, u8, i8, Option<u8>);
29
30type PitchClasses = [bool; 12];
36type IntervalClassVector = [u8; 6];
37type InvarianceVector = [u8; 8];
38type ZRelation = u8;
39
40type TNIStructure = (
41 PitchClasses,
42 IntervalClassVector,
43 InvarianceVector,
44 ZRelation,
45);
46
47type Pcivicv = (PitchClasses, InvarianceVector, IntervalClassVector);
48
49trait TNITupleExt {
50 fn pitches(&self) -> PitchClasses;
51 fn pitch_classes(&self) -> PitchClasses;
52 fn invariance_vector(&self) -> InvarianceVector;
53 fn z_relation(&self) -> ZRelation;
54}
55
56impl TNITupleExt for TNIStructure {
57 fn pitches(&self) -> PitchClasses {
58 self.0
59 }
60
61 fn pitch_classes(&self) -> PitchClasses {
62 self.pitches()
63 }
64
65 fn invariance_vector(&self) -> InvarianceVector {
66 self.2
67 }
68
69 fn z_relation(&self) -> ZRelation {
70 self.3
71 }
72}
73
74#[repr(i8)]
75#[derive(Copy, Clone, Eq, Hash, PartialEq, Debug)]
76enum Sign {
77 NegativeOne = -1,
78 Zero = 0,
79 One = 1,
80}
81
82impl Sign {
83 pub(crate) fn from_i8(i: i8) -> Option<Self> {
84 match i {
85 0 => Some(Sign::Zero),
86 1 => Some(Sign::One),
87 -1 => Some(Sign::NegativeOne),
88 _ => None,
89 }
90 }
91
92 fn as_i8(&self) -> i8 {
93 *self as i8
94 }
95}
96
97type U8SB = (u8, Sign);
98type U8U8SB = (u8, u8, Sign);
99
100const CARDINALITIES: usize = 13;
101
102type Forte = [&'static [Option<TNIStructure>]; CARDINALITIES];
103type CardinalityToChordMembers = [&'static [(U8SB, Pcivicv)]; CARDINALITIES];
104type ForteNumberWithInversionToIndex = &'static [(U8U8SB, u8)];
105type TnIndexToChordInfo = &'static [(U8U8SB, Option<&'static [&'static str]>)];
106type MaximumIndexNumberWithoutInversionEquivalence = [u8; CARDINALITIES];
107type MaximumIndexNumberWithInversionEquivalence = [u8; CARDINALITIES];
108
109#[derive(Debug, Clone)]
110pub(crate) struct KnownChordTableEntry {
111 pub(crate) cardinality: u8,
112 pub(crate) common_names: Vec<&'static str>,
113 pub(crate) forte_class: String,
114 pub(crate) normal_form: Vec<u8>,
115 pub(crate) interval_class_vector: Vec<u8>,
116}
117
118fn forte_index_is_inversion_equivalent(card: usize, index: u8) -> Result<bool, Error> {
124 if !(1..=12).contains(&card) {
125 return Err(Error::ChordTables(format!("cardinality {card} not valid")));
126 }
127 if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card] {
128 return Err(Error::ChordTables(format!("index {index} not valid")));
129 }
130 Ok(FORTE[card]
131 .get(index as usize)
132 .and_then(Option::as_ref)
133 .is_some_and(|entry| entry.invariance_vector()[1] > 0))
134}
135
136fn forte_index_to_inversions_available(card: usize, index: u8) -> Result<Vec<Sign>, Error> {
137 if !(1..=12).contains(&card) {
138 return Err(Error::ChordTables(format!("cardinality {card} not valid")));
139 }
140 if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card] {
141 return Err(Error::ChordTables(format!("index {index} not valid")));
142 }
143
144 let mut inversions = vec![];
145 if let Some(entry) = FORTE[card].get(index as usize).and_then(Option::as_ref) {
146 if entry.invariance_vector()[1] > 0 {
148 inversions.push(Sign::Zero);
149 } else {
150 inversions.push(Sign::NegativeOne);
151 inversions.push(Sign::One);
152 }
153 }
154 Ok(inversions)
155}
156
157fn validate_address(address: (u8, u8, Option<i8>)) -> Result<(u8, u8, Sign), Error> {
158 let card = address.0;
159 let index = address.1;
160 let inversion = match address.2 {
164 None => None,
165 Some(given) => Some(
166 Sign::from_i8(given)
167 .ok_or_else(|| Error::ChordTables(format!("inversion {given} not valid")))?,
168 ),
169 };
170
171 if !(1..=12).contains(&card) {
172 return Err(Error::ChordTables(format!("cardinality {card} not valid")));
173 }
174
175 if index < 1 || index > MAXIMUM_INDEX_NUMBER_WITHOUT_INVERSION_EQUIVALENCE[card as usize] {
176 return Err(Error::ChordTables(format!("index {index} not valid")));
177 }
178
179 let inversions_available = forte_index_to_inversions_available(card as usize, index)?;
180
181 let resolved_inversion = if let Some(inv) = inversion {
182 if inversions_available.contains(&inv) {
183 inv
184 } else {
185 return Err(Error::ChordTables(format!(
186 "inversion {} not valid",
187 inv.as_i8()
188 )));
189 }
190 } else if inversions_available.contains(&Sign::Zero) {
191 Sign::Zero
192 } else {
193 Sign::One
194 };
195
196 Ok((card, index, resolved_inversion))
197}
198
199fn bool_vec_to_pitch_classes(v: &[bool]) -> Vec<u8> {
200 v.iter()
201 .enumerate()
202 .filter_map(
203 |(idx, present)| {
204 if *present { Some(idx as u8) } else { None }
205 },
206 )
207 .collect()
208}
209
210fn pitch_classes_to_bools(pcs: &[u8]) -> [bool; 12] {
211 let mut out = [false; 12];
212 for pc in pcs {
213 out[*pc as usize % 12] = true;
214 }
215 out
216}
217
218pub(crate) fn seek_chord_tables_address(ordered_pitch_classes: &[u8]) -> Result<RawAddress, Error> {
219 if ordered_pitch_classes.is_empty() {
220 return Err(Error::ChordTables(
221 "cannot access chord tables address for Chord with 0 pitches".to_string(),
222 ));
223 }
224
225 let card = ordered_pitch_classes.len() as u8;
226 if card == 1 {
227 return Ok((1, 1, 0, Some(ordered_pitch_classes[0] % 12)));
228 }
229 if card == 12 {
230 return Ok((12, 1, 0, Some(0)));
231 }
232
233 let count = ordered_pitch_classes.len();
238 let mut candidates = [([false; 12], [false; 12], 0_u8); 12];
239 let mut transposed = [0_u8; 12];
240 let mut inverted = [0_u8; 12];
241 for rot in 0..count {
242 let original_pc = ordered_pitch_classes[rot] % 12;
243 for (offset, slot) in transposed[..count].iter_mut().enumerate() {
244 let pitch_class = ordered_pitch_classes[(rot + offset) % count];
245 *slot = ((IntegerType::from(pitch_class) - IntegerType::from(original_pc))
246 .rem_euclid(12)) as u8;
247 }
248 for offset in 0..count {
250 inverted[offset] = (12 - transposed[count - 1 - offset]) % 12;
251 }
252 let shift = (12 - inverted[0]) % 12;
253 for slot in inverted[..count].iter_mut() {
254 *slot = (*slot + shift) % 12;
255 }
256 candidates[rot] = (
257 pitch_classes_to_bools(&transposed[..count]),
258 pitch_classes_to_bools(&inverted[..count]),
259 original_pc,
260 );
261 }
262 let candidates = &candidates[..count];
263
264 for (index_candidate, data_line) in FORTE[card as usize].iter().enumerate().skip(1) {
265 let Some(data_line) = data_line else {
266 continue;
267 };
268 let data_line_pcs = data_line.pitch_classes();
269 let is_own_inversion =
270 || forte_index_is_inversion_equivalent(card as usize, index_candidate as u8);
271
272 for (candidate, candidate_inversion, candidate_original_pc) in candidates {
273 if data_line_pcs == *candidate {
274 let inversion = if is_own_inversion()? { 0 } else { 1 };
275 return Ok((
276 card,
277 index_candidate as u8,
278 inversion,
279 Some(*candidate_original_pc),
280 ));
281 }
282 if data_line_pcs == *candidate_inversion {
283 let inversion = if is_own_inversion()? { 0 } else { -1 };
284 return Ok((
285 card,
286 index_candidate as u8,
287 inversion,
288 Some(*candidate_original_pc),
289 ));
290 }
291 }
292 }
293
294 Err(Error::ChordTables(format!(
295 "cannot find a chord table address for {ordered_pitch_classes:?}"
296 )))
297}
298
299pub(crate) fn address_to_common_names(
300 address: RawAddress,
301) -> Result<Option<Vec<&'static str>>, Error> {
302 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
303 Ok(find_tn_index_to_chord_info(card, index, inversion).map(<[_]>::to_vec))
304}
305
306pub(crate) fn address_to_forte_name(
307 address: RawAddress,
308 classification: &str,
309) -> Result<String, Error> {
310 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
311 let inversion_suffix = match classification.to_ascii_lowercase().as_str() {
312 "tn" => match inversion {
313 Sign::NegativeOne => "B",
314 Sign::One => "A",
315 Sign::Zero => "",
316 },
317 _ => "",
318 };
319 Ok(format!("{card}-{index}{inversion_suffix}"))
320}
321
322pub(crate) fn prime_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
325 let (card, index, inversion) = validate_address((address.0, address.1, None))?;
326 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
327 Error::ChordTables(format!(
328 "cannot resolve prime form for address ({card}, {index})"
329 ))
330 })?;
331 Ok(bool_vec_to_pitch_classes(&entry.0))
332}
333
334pub(crate) fn transposed_normal_form(
337 cardinality: u8,
338 index: u8,
339 inversion: Option<i8>,
340) -> Result<Vec<u8>, Error> {
341 let (card, index, inversion) = validate_address((cardinality, index, inversion))?;
342 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
343 Error::ChordTables(format!(
344 "cannot resolve normal form for address ({card}, {index}, {})",
345 inversion.as_i8()
346 ))
347 })?;
348 Ok(bool_vec_to_pitch_classes(&entry.0))
349}
350
351pub(crate) fn transposed_normal_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
352 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
353 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
354 Error::ChordTables(format!(
355 "cannot resolve normal form for address ({card}, {index}, {})",
356 inversion.as_i8()
357 ))
358 })?;
359 Ok(bool_vec_to_pitch_classes(&entry.0))
360}
361
362pub(crate) fn interval_class_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
363 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
364 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
365 Error::ChordTables(format!(
366 "cannot resolve interval class vector for address ({card}, {index}, {})",
367 inversion.as_i8()
368 ))
369 })?;
370 Ok(entry.2.to_vec())
371}
372
373pub(crate) fn invariance_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
374 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
375 let entry = FORTE[card as usize]
376 .get(index as usize)
377 .and_then(Option::as_ref)
378 .ok_or_else(|| {
379 Error::ChordTables(format!(
380 "cannot resolve invariance vector for address ({card}, {index}, {})",
381 inversion.as_i8()
382 ))
383 })?;
384 Ok(entry.invariance_vector().to_vec())
385}
386
387pub(crate) fn z_relation_from_address(address: RawAddress) -> Result<Option<String>, Error> {
388 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
389 let entry = FORTE[card as usize]
390 .get(index as usize)
391 .and_then(Option::as_ref)
392 .ok_or_else(|| {
393 Error::ChordTables(format!(
394 "cannot resolve z-relation for address ({card}, {index}, {})",
395 inversion.as_i8()
396 ))
397 })?;
398 let z_relation = entry.z_relation();
399 if z_relation == 0 {
400 Ok(None)
401 } else {
402 Ok(Some(format!("{card}-{}", z_relation)))
403 }
404}
405
406pub(crate) fn known_chord_table_entries() -> Vec<KnownChordTableEntry> {
407 let mut entries = TN_INDEX_TO_CHORD_INFO
408 .iter()
409 .filter_map(|&((cardinality, index, inversion), common_names)| {
410 let common_names = common_names.unwrap_or_default().to_vec();
411 let address = (cardinality, index, inversion.as_i8(), None);
412 Some((
413 (cardinality, index, inversion.as_i8()),
414 KnownChordTableEntry {
415 cardinality,
416 common_names,
417 forte_class: address_to_forte_name(address, "tn").ok()?,
418 normal_form: transposed_normal_form_from_address(address).ok()?,
419 interval_class_vector: interval_class_vector_from_address(address).ok()?,
420 },
421 ))
422 })
423 .collect::<Vec<_>>();
424
425 entries.sort_by_key(|(sort_key, _)| *sort_key);
426 entries.into_iter().map(|(_, entry)| entry).collect()
427}
428
429fn find_cardinality_member(card: u8, index: u8, inversion: Sign) -> Option<&'static Pcivicv> {
430 CARDINALITY_TO_CHORD_MEMBERS
431 .get(card as usize)?
432 .iter()
433 .find_map(|(key, value)| (*key == (index, inversion)).then_some(value))
434}
435
436fn find_tn_index_to_chord_info(
437 card: u8,
438 index: u8,
439 inversion: Sign,
440) -> Option<&'static [&'static str]> {
441 TN_INDEX_TO_CHORD_INFO
442 .iter()
443 .find_map(|(key, names)| (*key == (card, index, inversion)).then_some(*names))
444 .flatten()
445}
446
447pub fn inversions_available(cardinality: u8, forte_class: u8) -> Result<Vec<i8>, Error> {
464 Ok(
465 forte_index_to_inversions_available(cardinality as usize, forte_class)?
466 .into_iter()
467 .map(|sign| sign.as_i8())
468 .collect(),
469 )
470}
471
472pub fn read_address(
483 cardinality: u8,
484 forte_class: u8,
485 inversion: Option<i8>,
486) -> Result<(u8, u8, i8), Error> {
487 let (cardinality, forte_class, inversion) =
488 validate_address((cardinality, forte_class, inversion))?;
489 Ok((cardinality, forte_class, inversion.as_i8()))
490}
491
492pub fn normal_form(
499 cardinality: u8,
500 forte_class: u8,
501 inversion: Option<i8>,
502) -> Result<Vec<u8>, Error> {
503 transposed_normal_form(cardinality, forte_class, inversion)
504}
505
506pub fn prime_form(cardinality: u8, forte_class: u8) -> Result<Vec<u8>, Error> {
513 transposed_normal_form(cardinality, forte_class, None)
514}
515
516pub fn interval_class_vector(
523 cardinality: u8,
524 forte_class: u8,
525 inversion: Option<i8>,
526) -> Result<Vec<u8>, Error> {
527 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
528 interval_class_vector_from_address((cardinality, forte_class, inversion, None))
529}
530
531pub fn set_classes_with_interval_vector(vector: &[u8]) -> Result<Vec<(u8, u8)>, Error> {
543 if vector.len() != 6 {
544 return Err(Error::ChordTables(
545 "Vector must have exactly six entries".to_string(),
546 ));
547 }
548 let mut found = Vec::new();
549 for (cardinality, classes) in FORTE.iter().enumerate().skip(1) {
550 for (forte_class, entry) in classes.iter().enumerate() {
551 let Some(entry) = entry else {
552 continue;
553 };
554 if entry.1 == vector {
555 found.push((cardinality as u8, forte_class as u8));
556 }
557 }
558 }
559 Ok(found)
560}
561
562pub fn z_related_class(cardinality: u8, forte_class: u8) -> Result<Option<(u8, u8, i8)>, Error> {
571 let (cardinality, forte_class, _) = read_address(cardinality, forte_class, None)?;
572 let related = FORTE[cardinality as usize]
573 .get(forte_class as usize)
574 .and_then(Option::as_ref)
575 .map_or(0, TNITupleExt::z_relation);
576 if related == 0 {
577 return Ok(None);
578 }
579 Some(read_address(cardinality, related, None)).transpose()
580}
581
582pub fn common_names(
596 cardinality: u8,
597 forte_class: u8,
598 inversion: Option<i8>,
599) -> Result<Option<Vec<&'static str>>, Error> {
600 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
601 address_to_common_names((cardinality, forte_class, inversion, None))
602}
603
604pub fn forte_name(
614 cardinality: u8,
615 forte_class: u8,
616 inversion: Option<i8>,
617 inversional_equivalence: bool,
618) -> Result<String, Error> {
619 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
620 address_to_forte_name(
621 (cardinality, forte_class, inversion, None),
622 if inversional_equivalence { "tni" } else { "tn" },
623 )
624}
625
626pub fn address_of_pitch_classes(pitch_classes: &[u8]) -> Result<ChordTableAddress, Error> {
638 let (cardinality, forte_class, inversion, original) = seek_chord_tables_address(pitch_classes)?;
639 Ok(ChordTableAddress {
640 cardinality,
641 forte_class,
642 inversion,
643 pitch_class_original: original.unwrap_or(0),
644 })
645}
646
647#[cfg(test)]
648mod tests {
649
650 #[test]
651 fn the_address_helpers_read_the_tables_for_a_major_triad() {
652 use super::{
653 interval_class_vector_from_address, invariance_vector_from_address,
654 prime_form_from_address, transposed_normal_form_from_address, z_relation_from_address,
655 };
656
657 let major = (3, 11, -1, Some(0));
658 assert_eq!(prime_form_from_address(major).unwrap(), [0, 3, 7]);
659 assert_eq!(
660 transposed_normal_form_from_address(major).unwrap(),
661 [0, 4, 7]
662 );
663 assert_eq!(
664 interval_class_vector_from_address(major).unwrap(),
665 [0, 0, 1, 1, 1, 0]
666 );
667 assert_eq!(invariance_vector_from_address(major).unwrap().len(), 8);
668 assert_eq!(z_relation_from_address(major).unwrap(), None);
669 assert_eq!(
670 z_relation_from_address((4, 15, 1, Some(0)))
671 .unwrap()
672 .as_deref(),
673 Some("4-29")
674 );
675 assert!(prime_form_from_address((3, 99, 0, None)).is_err());
676 }
677
678 use super::{Sign, find_cardinality_member};
679
680 #[test]
681 fn cardinality_to_chord_members_include_major_triad() {
682 let member = find_cardinality_member(3, 11, Sign::NegativeOne).unwrap();
683 assert_eq!(
684 member.0,
685 [
686 true, false, false, false, true, false, false, true, false, false, false, false
687 ]
688 );
689 assert_eq!(member.2, [0, 0, 1, 1, 1, 0]);
690 }
691
692 #[test]
694 fn the_tables_answer_what_music21_answers() {
695 use super::*;
696
697 assert_eq!(inversions_available(3, 1).unwrap(), [0]);
698 assert_eq!(inversions_available(3, 2).unwrap(), [-1, 1]);
699 assert_eq!(inversions_available(3, 12).unwrap(), [0]);
700 assert!(inversions_available(20, 1).is_err());
701 assert!(inversions_available(8, 200).is_err());
702
703 assert_eq!(read_address(3, 1, Some(0)).unwrap(), (3, 1, 0));
706 assert_eq!(read_address(2, 3, None).unwrap(), (2, 3, 0));
707 assert_eq!(read_address(3, 12, None).unwrap(), (3, 12, 0));
708 assert!(read_address(8, 3, Some(-30)).is_err());
709
710 assert_eq!(normal_form(3, 1, Some(0)).unwrap(), [0, 1, 2]);
711 assert_eq!(normal_form(3, 11, Some(-1)).unwrap(), [0, 4, 7]);
712 assert_eq!(normal_form(3, 11, Some(1)).unwrap(), [0, 3, 7]);
713 assert_eq!(normal_form(3, 11, None).unwrap(), [0, 3, 7]);
714
715 assert_eq!(prime_form(3, 11).unwrap(), [0, 3, 7]);
717 assert_eq!(prime_form(3, 1).unwrap(), [0, 1, 2]);
718
719 assert_eq!(
720 interval_class_vector(3, 1, Some(0)).unwrap(),
721 [2, 1, 0, 0, 0, 0]
722 );
723 assert_eq!(
724 interval_class_vector(3, 11, Some(-1)).unwrap(),
725 [0, 0, 1, 1, 1, 0]
726 );
727 assert_eq!(
728 interval_class_vector(4, 29, None).unwrap(),
729 [1, 1, 1, 1, 1, 1]
730 );
731
732 assert_eq!(
733 set_classes_with_interval_vector(&[7, 6, 5, 4, 4, 2]).unwrap(),
734 [(8, 1)]
735 );
736 assert_eq!(
737 set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 1]).unwrap(),
738 [(5, 10)]
739 );
740 assert!(
741 set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 99])
742 .unwrap()
743 .is_empty()
744 );
745 assert_eq!(
747 set_classes_with_interval_vector(&[1, 1, 1, 1, 1, 1]).unwrap(),
748 [(4, 15), (4, 29)]
749 );
750 assert!(set_classes_with_interval_vector(&[0, 2, 4]).is_err());
751
752 assert_eq!(z_related_class(5, 12).unwrap(), Some((5, 36, 1)));
753 assert_eq!(z_related_class(5, 36).unwrap(), Some((5, 12, 0)));
754 assert_eq!(z_related_class(3, 11).unwrap(), None);
755 assert_eq!(z_related_class(8, 29).unwrap(), Some((8, 15, 1)));
756
757 assert_eq!(
758 common_names(3, 1, Some(0)).unwrap(),
759 Some(vec!["chromatic trimirror"])
760 );
761 assert_eq!(
762 common_names(3, 11, Some(-1)).unwrap(),
763 Some(vec!["major triad"])
764 );
765 assert_eq!(
766 common_names(7, 33, None).unwrap(),
767 Some(vec!["Neapolitan-major mode", "leading-whole-tone mode"])
768 );
769
770 assert_eq!(forte_name(8, 15, Some(-1), false).unwrap(), "8-15B");
771 assert_eq!(forte_name(8, 15, None, false).unwrap(), "8-15A");
772 assert_eq!(forte_name(3, 12, None, false).unwrap(), "3-12");
773 assert_eq!(forte_name(8, 15, None, true).unwrap(), "8-15");
774 assert_eq!(forte_name(5, 37, None, false).unwrap(), "5-37");
775
776 let address = address_of_pitch_classes(&[0]).unwrap();
777 assert_eq!(address.cardinality, 1);
778 assert_eq!(address.forte_class, 1);
779 assert_eq!(address.inversion, 0);
780 assert_eq!(address.pitch_class_original, 0);
781 assert!(address_of_pitch_classes(&[]).is_err());
782 }
783}