Skip to main content

music21_rs/scale/
hexatonicblues.rs

1//! music21's `WeightedHexatonicBlues`, made reproducible.
2//!
3//! Upstream this is the one `ConcreteScale` subclass that is *not*
4//! deterministic: its `IntervalNetwork` sets `deterministic=False` and branches
5//! at the fourth degree, so two calls on the same tonic can return six or seven
6//! pitches. It is a sampler, not a table, which is why it cannot be a
7//! [`ScaleType`](super::ScaleType) variant — a generated parity fixture for it
8//! would go red at random.
9//!
10//! The branch is the only source of that randomness, and it has exactly two
11//! outcomes: take the blue note or skip it. Both are exposed here as named
12//! [`BluesForm`] variants that are perfectly deterministic, plus a
13//! [`WeightedHexatonicBlues::sample`] that picks between them from a seed you
14//! supply. Nothing in this crate reaches for a global random number generator.
15
16use crate::error::Result;
17use crate::pitch::Pitch;
18
19use super::stepscale::StepScale;
20
21/// Which side of the network's branch a realization takes.
22#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub enum BluesForm {
25    /// Skip the blue note: the minor pentatonic, six pitches with the octave.
26    Pentatonic,
27    /// Take the blue note: the hexatonic blues scale, seven pitches.
28    Hexatonic,
29}
30
31impl BluesForm {
32    /// Both forms, in declaration order.
33    pub const ALL: [BluesForm; 2] = [Self::Pentatonic, Self::Hexatonic];
34
35    /// Returns the step intervals from the tonic.
36    fn steps(self) -> &'static [&'static str] {
37        match self {
38            // c -> e- -> f -> g -> b- -> c
39            Self::Pentatonic => &["m3", "M2", "M2", "m3", "M2"],
40            // c -> e- -> f -> f# -> g -> b- -> c, the blue note between f and g
41            Self::Hexatonic => &["m3", "M2", "a1", "m2", "m3", "M2"],
42        }
43    }
44}
45
46/// music21's `WeightedHexatonicBlues`, with the randomness made explicit.
47///
48/// ```
49/// use music21_rs::{BluesForm, Pitch, WeightedHexatonicBlues};
50///
51/// let blues = WeightedHexatonicBlues::new(Pitch::from_name("C4")?);
52///
53/// let names: Vec<String> = blues.pitches(BluesForm::Hexatonic)?
54///     .iter().map(|p| p.name()).collect();
55/// assert_eq!(names, ["C", "E-", "F", "F#", "G", "B-", "C"]);
56///
57/// let names: Vec<String> = blues.pitches(BluesForm::Pentatonic)?
58///     .iter().map(|p| p.name()).collect();
59/// assert_eq!(names, ["C", "E-", "F", "G", "B-", "C"]);
60/// # Ok::<(), music21_rs::Error>(())
61/// ```
62#[derive(Clone, Debug, PartialEq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[must_use]
65pub struct WeightedHexatonicBlues {
66    tonic: Pitch,
67}
68
69impl WeightedHexatonicBlues {
70    /// Builds the scale on a tonic.
71    pub fn new(tonic: Pitch) -> Self {
72        Self { tonic }
73    }
74
75    /// Returns the tonic pitch.
76    pub fn tonic(&self) -> &Pitch {
77        &self.tonic
78    }
79
80    /// Returns the pitches of one form, from the tonic through its octave.
81    pub fn pitches(&self, form: BluesForm) -> Result<Vec<Pitch>> {
82        self.scale(form)?.pitches()
83    }
84
85    /// Returns one form as a [`StepScale`], for degree and interval access.
86    pub fn scale(&self, form: BluesForm) -> Result<StepScale> {
87        StepScale::cyclical(self.tonic.clone(), form.steps())
88    }
89
90    /// Picks a form from `seed`, the way music21 picks one at random.
91    ///
92    /// Deterministic for a given seed, so a caller who wants music21's
93    /// behaviour can supply entropy and a caller who wants a reproducible
94    /// result can supply a constant. The two forms are equally likely.
95    pub fn form_for_seed(seed: u64) -> BluesForm {
96        if split_mix_64(seed) & 1 == 0 {
97            BluesForm::Pentatonic
98        } else {
99            BluesForm::Hexatonic
100        }
101    }
102
103    /// Returns the pitches of the form [`form_for_seed`] picks.
104    ///
105    /// [`form_for_seed`]: WeightedHexatonicBlues::form_for_seed
106    pub fn sample(&self, seed: u64) -> Result<Vec<Pitch>> {
107        self.pitches(Self::form_for_seed(seed))
108    }
109}
110
111/// SplitMix64, so a seed maps to a well-mixed bit pattern.
112///
113/// A whole PRNG would be overkill: one branch is drawn per realization, and the
114/// crate has no random-number dependency to reach for.
115fn split_mix_64(seed: u64) -> u64 {
116    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
117    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
118    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
119    z ^ (z >> 31)
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    fn names(tonic: &str, form: BluesForm) -> Vec<String> {
127        WeightedHexatonicBlues::new(Pitch::from_name(tonic).expect("valid tonic"))
128            .pitches(form)
129            .expect("scale realizes")
130            .iter()
131            .map(|pitch| pitch.name())
132            .collect()
133    }
134
135    #[test]
136    fn both_forms_match_music21() {
137        // Captured from music21, which returns one or the other at random.
138        assert_eq!(
139            names("C4", BluesForm::Hexatonic),
140            ["C", "E-", "F", "F#", "G", "B-", "C"]
141        );
142        assert_eq!(
143            names("C4", BluesForm::Pentatonic),
144            ["C", "E-", "F", "G", "B-", "C"]
145        );
146        assert_eq!(
147            names("G4", BluesForm::Hexatonic),
148            ["G", "B-", "C", "C#", "D", "F", "G"]
149        );
150        assert_eq!(
151            names("E-4", BluesForm::Hexatonic),
152            ["E-", "G-", "A-", "A", "B-", "D-", "E-"]
153        );
154        assert_eq!(
155            names("F#4", BluesForm::Hexatonic),
156            ["F#", "A", "B", "B#", "C#", "E", "F#"]
157        );
158    }
159
160    #[test]
161    fn the_blue_note_is_the_only_difference() {
162        let hexatonic = names("C4", BluesForm::Hexatonic);
163        let pentatonic = names("C4", BluesForm::Pentatonic);
164        assert_eq!(hexatonic.len(), pentatonic.len() + 1);
165
166        let without_blue_note: Vec<&String> =
167            hexatonic.iter().filter(|name| *name != "F#").collect();
168        assert_eq!(without_blue_note, pentatonic.iter().collect::<Vec<_>>());
169    }
170
171    #[test]
172    fn sampling_is_reproducible_and_reaches_both_forms() {
173        let blues = WeightedHexatonicBlues::new(Pitch::from_name("C4").unwrap());
174        for seed in [0, 1, 7, 42, u64::MAX] {
175            assert_eq!(
176                blues.sample(seed).unwrap(),
177                blues.sample(seed).unwrap(),
178                "seed {seed} should be reproducible"
179            );
180        }
181
182        let forms: Vec<BluesForm> = (0..64).map(WeightedHexatonicBlues::form_for_seed).collect();
183        assert!(forms.contains(&BluesForm::Pentatonic));
184        assert!(forms.contains(&BluesForm::Hexatonic));
185    }
186
187    #[test]
188    fn every_form_realizes_on_every_common_tonic() {
189        for form in BluesForm::ALL {
190            for tonic in [
191                "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4",
192            ] {
193                let pitches = WeightedHexatonicBlues::new(Pitch::from_name(tonic).unwrap())
194                    .pitches(form)
195                    .expect("scale realizes");
196                assert_eq!(pitches.len(), form.steps().len() + 1, "{form:?} on {tonic}");
197            }
198        }
199    }
200}