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 = if classification.eq_ignore_ascii_case("tn") {
314 match inversion {
315 Sign::NegativeOne => "B",
316 Sign::One => "A",
317 Sign::Zero => "",
318 }
319 } else {
320 ""
321 };
322 Ok(format!("{card}-{index}{inversion_suffix}"))
323}
324
325pub(crate) fn prime_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
328 let (card, index, inversion) = validate_address((address.0, address.1, None))?;
329 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
330 Error::ChordTables(format!(
331 "cannot resolve prime form for address ({card}, {index})"
332 ))
333 })?;
334 Ok(bool_vec_to_pitch_classes(&entry.0))
335}
336
337pub(crate) fn transposed_normal_form(
340 cardinality: u8,
341 index: u8,
342 inversion: Option<i8>,
343) -> Result<Vec<u8>, Error> {
344 let (card, index, inversion) = validate_address((cardinality, index, inversion))?;
345 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
346 Error::ChordTables(format!(
347 "cannot resolve normal form for address ({card}, {index}, {})",
348 inversion.as_i8()
349 ))
350 })?;
351 Ok(bool_vec_to_pitch_classes(&entry.0))
352}
353
354pub(crate) fn transposed_normal_form_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
355 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
356 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
357 Error::ChordTables(format!(
358 "cannot resolve normal form for address ({card}, {index}, {})",
359 inversion.as_i8()
360 ))
361 })?;
362 Ok(bool_vec_to_pitch_classes(&entry.0))
363}
364
365pub(crate) fn interval_class_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
366 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
367 let entry = find_cardinality_member(card, index, inversion).ok_or_else(|| {
368 Error::ChordTables(format!(
369 "cannot resolve interval class vector for address ({card}, {index}, {})",
370 inversion.as_i8()
371 ))
372 })?;
373 Ok(entry.2.to_vec())
374}
375
376pub(crate) fn invariance_vector_from_address(address: RawAddress) -> Result<Vec<u8>, Error> {
377 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
378 let entry = FORTE[card as usize]
379 .get(index as usize)
380 .and_then(Option::as_ref)
381 .ok_or_else(|| {
382 Error::ChordTables(format!(
383 "cannot resolve invariance vector for address ({card}, {index}, {})",
384 inversion.as_i8()
385 ))
386 })?;
387 Ok(entry.invariance_vector().to_vec())
388}
389
390pub(crate) fn z_relation_from_address(address: RawAddress) -> Result<Option<String>, Error> {
391 let (card, index, inversion) = validate_address((address.0, address.1, Some(address.2)))?;
392 let entry = FORTE[card as usize]
393 .get(index as usize)
394 .and_then(Option::as_ref)
395 .ok_or_else(|| {
396 Error::ChordTables(format!(
397 "cannot resolve z-relation for address ({card}, {index}, {})",
398 inversion.as_i8()
399 ))
400 })?;
401 let z_relation = entry.z_relation();
402 if z_relation == 0 {
403 Ok(None)
404 } else {
405 Ok(Some(format!("{card}-{}", z_relation)))
406 }
407}
408
409pub(crate) fn known_chord_table_entries() -> Vec<KnownChordTableEntry> {
410 let mut entries = TN_INDEX_TO_CHORD_INFO
411 .iter()
412 .filter_map(|&((cardinality, index, inversion), common_names)| {
413 let common_names = common_names.unwrap_or_default().to_vec();
414 let address = (cardinality, index, inversion.as_i8(), None);
415 Some((
416 (cardinality, index, inversion.as_i8()),
417 KnownChordTableEntry {
418 cardinality,
419 common_names,
420 forte_class: address_to_forte_name(address, "tn").ok()?,
421 normal_form: transposed_normal_form_from_address(address).ok()?,
422 interval_class_vector: interval_class_vector_from_address(address).ok()?,
423 },
424 ))
425 })
426 .collect::<Vec<_>>();
427
428 entries.sort_by_key(|(sort_key, _)| *sort_key);
429 entries.into_iter().map(|(_, entry)| entry).collect()
430}
431
432fn find_cardinality_member(card: u8, index: u8, inversion: Sign) -> Option<&'static Pcivicv> {
433 CARDINALITY_TO_CHORD_MEMBERS
434 .get(card as usize)?
435 .iter()
436 .find_map(|(key, value)| (*key == (index, inversion)).then_some(value))
437}
438
439fn find_tn_index_to_chord_info(
440 card: u8,
441 index: u8,
442 inversion: Sign,
443) -> Option<&'static [&'static str]> {
444 TN_INDEX_TO_CHORD_INFO
445 .iter()
446 .find_map(|(key, names)| (*key == (card, index, inversion)).then_some(*names))
447 .flatten()
448}
449
450pub fn inversions_available(cardinality: u8, forte_class: u8) -> Result<Vec<i8>, Error> {
467 Ok(
468 forte_index_to_inversions_available(cardinality as usize, forte_class)?
469 .into_iter()
470 .map(|sign| sign.as_i8())
471 .collect(),
472 )
473}
474
475pub fn read_address(
486 cardinality: u8,
487 forte_class: u8,
488 inversion: Option<i8>,
489) -> Result<(u8, u8, i8), Error> {
490 let (cardinality, forte_class, inversion) =
491 validate_address((cardinality, forte_class, inversion))?;
492 Ok((cardinality, forte_class, inversion.as_i8()))
493}
494
495pub fn normal_form(
502 cardinality: u8,
503 forte_class: u8,
504 inversion: Option<i8>,
505) -> Result<Vec<u8>, Error> {
506 transposed_normal_form(cardinality, forte_class, inversion)
507}
508
509pub fn prime_form(cardinality: u8, forte_class: u8) -> Result<Vec<u8>, Error> {
516 transposed_normal_form(cardinality, forte_class, None)
517}
518
519pub fn interval_class_vector(
526 cardinality: u8,
527 forte_class: u8,
528 inversion: Option<i8>,
529) -> Result<Vec<u8>, Error> {
530 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
531 interval_class_vector_from_address((cardinality, forte_class, inversion, None))
532}
533
534pub fn set_classes_with_interval_vector(vector: &[u8]) -> Result<Vec<(u8, u8)>, Error> {
546 if vector.len() != 6 {
547 return Err(Error::ChordTables(
548 "Vector must have exactly six entries".to_string(),
549 ));
550 }
551 let mut found = Vec::new();
552 for (cardinality, classes) in FORTE.iter().enumerate().skip(1) {
553 for (forte_class, entry) in classes.iter().enumerate() {
554 let Some(entry) = entry else {
555 continue;
556 };
557 if entry.1 == vector {
558 found.push((cardinality as u8, forte_class as u8));
559 }
560 }
561 }
562 Ok(found)
563}
564
565pub fn z_related_class(cardinality: u8, forte_class: u8) -> Result<Option<(u8, u8, i8)>, Error> {
574 let (cardinality, forte_class, _) = read_address(cardinality, forte_class, None)?;
575 let related = FORTE[cardinality as usize]
576 .get(forte_class as usize)
577 .and_then(Option::as_ref)
578 .map_or(0, TNITupleExt::z_relation);
579 if related == 0 {
580 return Ok(None);
581 }
582 Some(read_address(cardinality, related, None)).transpose()
583}
584
585pub fn common_names(
599 cardinality: u8,
600 forte_class: u8,
601 inversion: Option<i8>,
602) -> Result<Option<Vec<&'static str>>, Error> {
603 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
604 address_to_common_names((cardinality, forte_class, inversion, None))
605}
606
607pub fn forte_name(
617 cardinality: u8,
618 forte_class: u8,
619 inversion: Option<i8>,
620 inversional_equivalence: bool,
621) -> Result<String, Error> {
622 let (cardinality, forte_class, inversion) = read_address(cardinality, forte_class, inversion)?;
623 address_to_forte_name(
624 (cardinality, forte_class, inversion, None),
625 if inversional_equivalence { "tni" } else { "tn" },
626 )
627}
628
629pub fn address_of_pitch_classes(pitch_classes: &[u8]) -> Result<ChordTableAddress, Error> {
641 let (cardinality, forte_class, inversion, original) = seek_chord_tables_address(pitch_classes)?;
642 Ok(ChordTableAddress {
643 cardinality,
644 forte_class,
645 inversion,
646 pitch_class_original: original.unwrap_or(0),
647 })
648}
649
650#[cfg(test)]
651mod tests {
652
653 #[test]
654 fn the_address_helpers_read_the_tables_for_a_major_triad() {
655 use super::{
656 interval_class_vector_from_address, invariance_vector_from_address,
657 prime_form_from_address, transposed_normal_form_from_address, z_relation_from_address,
658 };
659
660 let major = (3, 11, -1, Some(0));
661 assert_eq!(prime_form_from_address(major).unwrap(), [0, 3, 7]);
662 assert_eq!(
663 transposed_normal_form_from_address(major).unwrap(),
664 [0, 4, 7]
665 );
666 assert_eq!(
667 interval_class_vector_from_address(major).unwrap(),
668 [0, 0, 1, 1, 1, 0]
669 );
670 assert_eq!(invariance_vector_from_address(major).unwrap().len(), 8);
671 assert_eq!(z_relation_from_address(major).unwrap(), None);
672 assert_eq!(
673 z_relation_from_address((4, 15, 1, Some(0)))
674 .unwrap()
675 .as_deref(),
676 Some("4-29")
677 );
678 assert!(prime_form_from_address((3, 99, 0, None)).is_err());
679 }
680
681 use super::{Sign, find_cardinality_member};
682
683 #[test]
684 fn cardinality_to_chord_members_include_major_triad() {
685 let member = find_cardinality_member(3, 11, Sign::NegativeOne).unwrap();
686 assert_eq!(
687 member.0,
688 [
689 true, false, false, false, true, false, false, true, false, false, false, false
690 ]
691 );
692 assert_eq!(member.2, [0, 0, 1, 1, 1, 0]);
693 }
694
695 #[test]
697 fn the_tables_answer_what_music21_answers() {
698 use super::*;
699
700 assert_eq!(inversions_available(3, 1).unwrap(), [0]);
701 assert_eq!(inversions_available(3, 2).unwrap(), [-1, 1]);
702 assert_eq!(inversions_available(3, 12).unwrap(), [0]);
703 assert!(inversions_available(20, 1).is_err());
704 assert!(inversions_available(8, 200).is_err());
705
706 assert_eq!(read_address(3, 1, Some(0)).unwrap(), (3, 1, 0));
709 assert_eq!(read_address(2, 3, None).unwrap(), (2, 3, 0));
710 assert_eq!(read_address(3, 12, None).unwrap(), (3, 12, 0));
711 assert!(read_address(8, 3, Some(-30)).is_err());
712
713 assert_eq!(normal_form(3, 1, Some(0)).unwrap(), [0, 1, 2]);
714 assert_eq!(normal_form(3, 11, Some(-1)).unwrap(), [0, 4, 7]);
715 assert_eq!(normal_form(3, 11, Some(1)).unwrap(), [0, 3, 7]);
716 assert_eq!(normal_form(3, 11, None).unwrap(), [0, 3, 7]);
717
718 assert_eq!(prime_form(3, 11).unwrap(), [0, 3, 7]);
720 assert_eq!(prime_form(3, 1).unwrap(), [0, 1, 2]);
721
722 assert_eq!(
723 interval_class_vector(3, 1, Some(0)).unwrap(),
724 [2, 1, 0, 0, 0, 0]
725 );
726 assert_eq!(
727 interval_class_vector(3, 11, Some(-1)).unwrap(),
728 [0, 0, 1, 1, 1, 0]
729 );
730 assert_eq!(
731 interval_class_vector(4, 29, None).unwrap(),
732 [1, 1, 1, 1, 1, 1]
733 );
734
735 assert_eq!(
736 set_classes_with_interval_vector(&[7, 6, 5, 4, 4, 2]).unwrap(),
737 [(8, 1)]
738 );
739 assert_eq!(
740 set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 1]).unwrap(),
741 [(5, 10)]
742 );
743 assert!(
744 set_classes_with_interval_vector(&[2, 2, 3, 1, 1, 99])
745 .unwrap()
746 .is_empty()
747 );
748 assert_eq!(
750 set_classes_with_interval_vector(&[1, 1, 1, 1, 1, 1]).unwrap(),
751 [(4, 15), (4, 29)]
752 );
753 assert!(set_classes_with_interval_vector(&[0, 2, 4]).is_err());
754
755 assert_eq!(z_related_class(5, 12).unwrap(), Some((5, 36, 1)));
756 assert_eq!(z_related_class(5, 36).unwrap(), Some((5, 12, 0)));
757 assert_eq!(z_related_class(3, 11).unwrap(), None);
758 assert_eq!(z_related_class(8, 29).unwrap(), Some((8, 15, 1)));
759
760 assert_eq!(
761 common_names(3, 1, Some(0)).unwrap(),
762 Some(vec!["chromatic trimirror"])
763 );
764 assert_eq!(
765 common_names(3, 11, Some(-1)).unwrap(),
766 Some(vec!["major triad"])
767 );
768 assert_eq!(
769 common_names(7, 33, None).unwrap(),
770 Some(vec!["Neapolitan-major mode", "leading-whole-tone mode"])
771 );
772
773 assert_eq!(forte_name(8, 15, Some(-1), false).unwrap(), "8-15B");
774 assert_eq!(forte_name(8, 15, None, false).unwrap(), "8-15A");
775 assert_eq!(forte_name(3, 12, None, false).unwrap(), "3-12");
776 assert_eq!(forte_name(8, 15, None, true).unwrap(), "8-15");
777 assert_eq!(forte_name(5, 37, None, false).unwrap(), "5-37");
778
779 let address = address_of_pitch_classes(&[0]).unwrap();
780 assert_eq!(address.cardinality, 1);
781 assert_eq!(address.forte_class, 1);
782 assert_eq!(address.inversion, 0);
783 assert_eq!(address.pitch_class_original, 0);
784 assert!(address_of_pitch_classes(&[]).is_err());
785 }
786}