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_mask().count_ones() as usize
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 {
191 self.notes.iter().fold(0_u16, |mask, note| {
192 mask | (1_u16 << root::pitch_class(¬e.pitch))
193 })
194 }
195
196 pub fn chord_tables_address_entry(&self) -> ChordTableAddress {
202 match self.chord_tables_address() {
203 Some((cardinality, forte_class, inversion, original)) => ChordTableAddress {
204 cardinality,
205 forte_class,
206 inversion,
207 pitch_class_original: original.unwrap_or(0),
208 },
209 None => ChordTableAddress {
210 cardinality: 0,
211 forte_class: 0,
212 inversion: 0,
213 pitch_class_original: 0,
214 },
215 }
216 }
217
218 pub(super) fn chord_tables_address(&self) -> Option<tables::RawAddress> {
219 tables::seek_chord_tables_address(&self.ordered_pitch_classes()).ok()
220 }
221
222 pub fn geometric_normal_form(&self) -> Vec<u8> {
227 let pitch_classes = self.ordered_pitch_classes();
228 if pitch_classes.is_empty() {
229 return Vec::new();
230 }
231 let intervals: Vec<u8> = pitch_classes
232 .iter()
233 .zip(pitch_classes.iter().cycle().skip(1))
234 .map(|(&low, &high)| (high + 12 - low) % 12)
235 .collect();
236 let best = (0..intervals.len())
237 .map(|rotation| {
238 let mut rotated = intervals[rotation + 1..].to_vec();
239 rotated.extend_from_slice(&intervals[..=rotation]);
240 rotated
241 })
242 .min()
243 .unwrap_or_default();
244 let mut sum = 0;
245 best.iter()
246 .map(|interval| {
247 let pitch_class = sum;
248 sum += interval;
249 pitch_class
250 })
251 .collect()
252 }
253
254 pub fn interval_vector_string(&self) -> String {
257 format_pitch_classes(&self.interval_class_vector().unwrap_or_else(|| vec![0; 6]))
258 }
259
260 pub fn normal_order(&self) -> Vec<u8> {
265 let Some(transposed) = self.normal_form() else {
266 return Vec::new();
267 };
268 let ordered = self.ordered_pitch_classes();
269 ordered
270 .iter()
271 .map(|&transposition| {
272 transposed
273 .iter()
274 .map(|&pc| (pc + transposition) % 12)
275 .collect::<Vec<u8>>()
276 })
277 .find(|candidate| {
278 let mut sorted = candidate.clone();
279 sorted.sort_unstable();
280 sorted == ordered
281 })
282 .unwrap_or_default()
283 }
284
285 pub fn normal_order_string(&self) -> String {
288 format_pitch_classes(&self.normal_order())
289 }
290
291 pub fn forte_class_number(&self) -> Option<u8> {
294 self.chord_tables_address().map(|address| address.1)
295 }
296
297 pub fn forte_class_tn(&self) -> Option<String> {
301 self.forte_class()
302 }
303
304 pub fn multiset_cardinality(&self) -> usize {
307 self.notes.len()
308 }
309
310 pub fn is_prime_form_inversion(&self) -> bool {
313 self.chord_tables_address()
314 .is_some_and(|address| address.2 == -1)
315 }
316
317 pub fn has_z_relation(&self) -> bool {
319 self.z_relation().is_some()
320 }
321
322 pub fn are_z_relations(&self, other: &Chord) -> bool {
326 let Some(z_relation) = self.z_relation() else {
327 return false;
328 };
329 other
330 .chord_tables_address()
331 .is_some_and(|address| format!("{}-{}", address.0, address.1) == z_relation)
332 }
333
334 pub fn is_false_diminished_seventh(&self) -> bool {
338 self.chord_tables_address()
339 .is_some_and(|address| (address.0, address.1, address.2) == (4, 28, 0))
340 }
341}
342
343pub fn format_vector_string(values: &[u8]) -> String {
347 let digits: String = values
348 .iter()
349 .map(|value| crate::pitch::convert_pitch_class_to_str(*value as IntegerType))
350 .collect();
351 format!("<{digits}>")
352}
353
354pub(super) fn format_pitch_classes(pitch_classes: &[u8]) -> String {
355 let mut out = String::with_capacity(pitch_classes.len() + 2);
356 out.push('<');
357 for pitch_class in pitch_classes {
358 out.push(crate::pitch::pitchclass::pitch_class_char(
359 IntegerType::from(*pitch_class),
360 ));
361 }
362 out.push('>');
363 out
364}
365
366#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
374#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
375#[must_use]
376pub struct ChordTableAddress {
377 pub cardinality: u8,
379 pub forte_class: u8,
381 pub inversion: i8,
383 pub pitch_class_original: u8,
385}