music21_rs/chord/
setclass.rs1use super::*;
6
7impl Chord {
8 pub fn from_forte_class(notation: &str) -> Result<Self> {
13 let Some((cardinality, rest)) = notation.split_once('-') else {
14 return Err(Error::Chord(format!(
15 "cannot extract set-class representation from string: {notation}"
16 )));
17 };
18 let rest = rest.trim_start_matches(['z', 'Z']);
21 let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
22 let letters = &rest[digits.len()..];
23 let inversion = match letters.to_ascii_lowercase().as_str() {
24 "a" => Some(1),
25 "b" => Some(-1),
26 _ => None,
27 };
28 let cardinality = cardinality
29 .parse::<u8>()
30 .map_err(|_| Error::Chord(format!("cannot read a cardinality out of {notation}")))?;
31 let index = digits
32 .parse::<u8>()
33 .map_err(|_| Error::Chord(format!("cannot read a Forte number out of {notation}")))?;
34 Self::from_forte_address(cardinality, index, inversion)
35 }
36
37 pub fn from_forte_address(cardinality: u8, index: u8, inversion: Option<i8>) -> Result<Self> {
41 let pitch_classes = tables::transposed_normal_form(cardinality, index, inversion)?;
42 Self::from_pitch_class_list(&pitch_classes)
43 }
44
45 pub fn from_interval_vector(vector: &[u8; 6], z_relation: bool) -> Option<Self> {
50 let mut seen: Vec<(u8, String)> = Vec::new();
51 for entry in tables::known_chord_table_entries() {
52 if entry.interval_class_vector != vector {
53 continue;
54 }
55 let number = entry.forte_class.trim_end_matches(['A', 'B']).to_string();
56 if seen
57 .iter()
58 .any(|(card, seen_number)| *card == entry.cardinality && *seen_number == number)
59 {
60 continue;
61 }
62 seen.push((entry.cardinality, number));
63 }
64 let (cardinality, number) = match (seen.len(), z_relation) {
65 (1, _) | (2, false) => seen.first()?.clone(),
66 (2, true) => seen.get(1)?.clone(),
67 _ => return None,
68 };
69 let index = number.split_once('-')?.1.parse().ok()?;
70 Self::from_forte_address(cardinality, index, None).ok()
71 }
72
73 pub(super) fn from_pitch_class_list(pitch_classes: &[u8]) -> Result<Self> {
77 let pitches = pitch_classes
78 .iter()
79 .map(|&pc| Pitch::from_pitch_class(IntegerType::from(pc)))
80 .collect::<Result<Vec<_>>>()?;
81 Self::new(pitches.as_slice())?.simplify_enharmonics(None)
82 }
83
84 pub fn forte_class(&self) -> Option<String> {
89 let ordered_pcs = self.ordered_pitch_classes();
90 let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
91 tables::address_to_forte_name(address, "tn").ok()
92 }
93
94 pub fn normal_form(&self) -> Option<Vec<u8>> {
102 let ordered_pcs = self.ordered_pitch_classes();
103 let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
104 tables::transposed_normal_form_from_address(address).ok()
105 }
106
107 pub fn interval_class_vector(&self) -> Option<Vec<u8>> {
112 let ordered_pcs = self.ordered_pitch_classes();
113 let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
114 tables::interval_class_vector_from_address(address).ok()
115 }
116
117 pub fn invariance_vector(&self) -> Option<Vec<u8>> {
122 let ordered_pcs = self.ordered_pitch_classes();
123 let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
124 tables::invariance_vector_from_address(address).ok()
125 }
126
127 pub fn z_relation(&self) -> Option<String> {
129 let ordered_pcs = self.ordered_pitch_classes();
130 let address = tables::seek_chord_tables_address(&ordered_pcs).ok()?;
131 tables::z_relation_from_address(address).ok().flatten()
132 }
133
134 pub(super) fn forte_address(&self) -> Option<(u8, u8, i8)> {
135 tables::seek_chord_tables_address(&self.ordered_pitch_classes())
136 .ok()
137 .map(|(card, index, inversion, _)| (card, index, inversion))
138 }
139
140 pub fn prime_form(&self) -> Vec<u8> {
144 tables::seek_chord_tables_address(&self.ordered_pitch_classes())
145 .and_then(tables::prime_form_from_address)
146 .unwrap_or_default()
147 }
148
149 pub fn prime_form_string(&self) -> String {
152 format_pitch_classes(&self.prime_form())
153 }
154
155 pub fn forte_class_tni(&self) -> Option<String> {
158 let address = tables::seek_chord_tables_address(&self.ordered_pitch_classes()).ok()?;
159 tables::address_to_forte_name(address, "tni").ok()
160 }
161
162 pub fn pitch_class_cardinality(&self) -> usize {
164 self.pitch_class_set().len()
165 }
166
167 pub fn ordered_pitch_classes_string(&self) -> String {
170 format_pitch_classes(&self.ordered_pitch_classes())
171 }
172
173 pub(super) fn ordered_pitch_classes(&self) -> Vec<u8> {
174 let mut pcs = self
175 .notes
176 .iter()
177 .map(|note| root::pitch_class(¬e.pitch))
178 .collect::<Vec<_>>();
179 pcs.sort_unstable();
180 pcs.dedup();
181 pcs
182 }
183
184 pub(super) fn pitch_class_set(&self) -> std::collections::BTreeSet<u8> {
185 self.ordered_pitch_classes().into_iter().collect()
186 }
187
188 pub(super) fn pitch_class_mask(&self) -> u16 {
189 self.ordered_pitch_classes()
190 .into_iter()
191 .fold(0_u16, |mask, pc| mask | (1_u16 << pc))
192 }
193
194 pub fn chord_tables_address_entry(&self) -> ChordTableAddress {
200 match self.chord_tables_address() {
201 Some((cardinality, forte_class, inversion, original)) => ChordTableAddress {
202 cardinality,
203 forte_class,
204 inversion,
205 pitch_class_original: original.unwrap_or(0),
206 },
207 None => ChordTableAddress {
208 cardinality: 0,
209 forte_class: 0,
210 inversion: 0,
211 pitch_class_original: 0,
212 },
213 }
214 }
215
216 pub(super) fn chord_tables_address(&self) -> Option<tables::RawAddress> {
217 tables::seek_chord_tables_address(&self.ordered_pitch_classes()).ok()
218 }
219
220 pub fn geometric_normal_form(&self) -> Vec<u8> {
225 let pitch_classes = self.ordered_pitch_classes();
226 if pitch_classes.is_empty() {
227 return Vec::new();
228 }
229 let intervals: Vec<u8> = pitch_classes
230 .iter()
231 .zip(pitch_classes.iter().cycle().skip(1))
232 .map(|(&low, &high)| (high + 12 - low) % 12)
233 .collect();
234 let best = (0..intervals.len())
235 .map(|rotation| {
236 let mut rotated = intervals[rotation + 1..].to_vec();
237 rotated.extend_from_slice(&intervals[..=rotation]);
238 rotated
239 })
240 .min()
241 .unwrap_or_default();
242 let mut sum = 0;
243 best.iter()
244 .map(|interval| {
245 let pitch_class = sum;
246 sum += interval;
247 pitch_class
248 })
249 .collect()
250 }
251
252 pub fn interval_vector_string(&self) -> String {
255 format_pitch_classes(&self.interval_class_vector().unwrap_or_else(|| vec![0; 6]))
256 }
257
258 pub fn normal_order(&self) -> Vec<u8> {
263 let Some(transposed) = self.normal_form() else {
264 return Vec::new();
265 };
266 let ordered = self.ordered_pitch_classes();
267 ordered
268 .iter()
269 .map(|&transposition| {
270 transposed
271 .iter()
272 .map(|&pc| (pc + transposition) % 12)
273 .collect::<Vec<u8>>()
274 })
275 .find(|candidate| {
276 let mut sorted = candidate.clone();
277 sorted.sort_unstable();
278 sorted == ordered
279 })
280 .unwrap_or_default()
281 }
282
283 pub fn normal_order_string(&self) -> String {
286 format_pitch_classes(&self.normal_order())
287 }
288
289 pub fn forte_class_number(&self) -> Option<u8> {
292 self.chord_tables_address().map(|address| address.1)
293 }
294
295 pub fn forte_class_tn(&self) -> Option<String> {
299 self.forte_class()
300 }
301
302 pub fn multiset_cardinality(&self) -> usize {
305 self.notes.len()
306 }
307
308 pub fn is_prime_form_inversion(&self) -> bool {
311 self.chord_tables_address()
312 .is_some_and(|address| address.2 == -1)
313 }
314
315 pub fn has_z_relation(&self) -> bool {
317 self.z_relation().is_some()
318 }
319
320 pub fn are_z_relations(&self, other: &Chord) -> bool {
324 let Some(z_relation) = self.z_relation() else {
325 return false;
326 };
327 other
328 .chord_tables_address()
329 .is_some_and(|address| format!("{}-{}", address.0, address.1) == z_relation)
330 }
331
332 pub fn is_false_diminished_seventh(&self) -> bool {
336 self.chord_tables_address()
337 .is_some_and(|address| (address.0, address.1, address.2) == (4, 28, 0))
338 }
339}
340
341pub fn format_vector_string(values: &[u8]) -> String {
345 let digits: String = values
346 .iter()
347 .map(|value| crate::pitch::convert_pitch_class_to_str(*value as IntegerType))
348 .collect();
349 format!("<{digits}>")
350}
351
352pub(super) fn format_pitch_classes(pitch_classes: &[u8]) -> String {
353 let mut out = String::with_capacity(pitch_classes.len() + 2);
354 out.push('<');
355 for pitch_class in pitch_classes {
356 out.push_str(&crate::pitch::pitchclass::convert_pitch_class_to_str(
357 IntegerType::from(*pitch_class),
358 ));
359 }
360 out.push('>');
361 out
362}
363
364#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
372#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
373#[must_use]
374pub struct ChordTableAddress {
375 pub cardinality: u8,
377 pub forte_class: u8,
379 pub inversion: i8,
381 pub pitch_class_original: u8,
383}