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))]
64pub struct WeightedHexatonicBlues {
65    tonic: Pitch,
66}
67
68impl WeightedHexatonicBlues {
69    /// Builds the scale on a tonic.
70    pub fn new(tonic: Pitch) -> Self {
71        Self { tonic }
72    }
73
74    /// Returns the tonic pitch.
75    pub fn tonic(&self) -> &Pitch {
76        &self.tonic
77    }
78
79    /// Returns the pitches of one form, from the tonic through its octave.
80    pub fn pitches(&self, form: BluesForm) -> Result<Vec<Pitch>> {
81        self.scale(form)?.pitches()
82    }
83
84    /// Returns one form as a [`StepScale`], for degree and interval access.
85    pub fn scale(&self, form: BluesForm) -> Result<StepScale> {
86        StepScale::cyclical(self.tonic.clone(), form.steps())
87    }
88
89    /// Picks a form from `seed`, the way music21 picks one at random.
90    ///
91    /// Deterministic for a given seed, so a caller who wants music21's
92    /// behaviour can supply entropy and a caller who wants a reproducible
93    /// result can supply a constant. The two forms are equally likely.
94    pub fn form_for_seed(seed: u64) -> BluesForm {
95        if split_mix_64(seed) & 1 == 0 {
96            BluesForm::Pentatonic
97        } else {
98            BluesForm::Hexatonic
99        }
100    }
101
102    /// Returns the pitches of the form [`form_for_seed`] picks.
103    ///
104    /// [`form_for_seed`]: WeightedHexatonicBlues::form_for_seed
105    pub fn sample(&self, seed: u64) -> Result<Vec<Pitch>> {
106        self.pitches(Self::form_for_seed(seed))
107    }
108}
109
110/// SplitMix64, so a seed maps to a well-mixed bit pattern.
111///
112/// A whole PRNG would be overkill: one branch is drawn per realization, and the
113/// crate has no random-number dependency to reach for.
114fn split_mix_64(seed: u64) -> u64 {
115    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
116    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
117    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
118    z ^ (z >> 31)
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn names(tonic: &str, form: BluesForm) -> Vec<String> {
126        WeightedHexatonicBlues::new(Pitch::from_name(tonic).expect("valid tonic"))
127            .pitches(form)
128            .expect("scale realizes")
129            .iter()
130            .map(|pitch| pitch.name())
131            .collect()
132    }
133
134    #[test]
135    fn both_forms_match_music21() {
136        // Captured from music21, which returns one or the other at random.
137        assert_eq!(
138            names("C4", BluesForm::Hexatonic),
139            ["C", "E-", "F", "F#", "G", "B-", "C"]
140        );
141        assert_eq!(
142            names("C4", BluesForm::Pentatonic),
143            ["C", "E-", "F", "G", "B-", "C"]
144        );
145        assert_eq!(
146            names("G4", BluesForm::Hexatonic),
147            ["G", "B-", "C", "C#", "D", "F", "G"]
148        );
149        assert_eq!(
150            names("E-4", BluesForm::Hexatonic),
151            ["E-", "G-", "A-", "A", "B-", "D-", "E-"]
152        );
153        assert_eq!(
154            names("F#4", BluesForm::Hexatonic),
155            ["F#", "A", "B", "B#", "C#", "E", "F#"]
156        );
157    }
158
159    #[test]
160    fn the_blue_note_is_the_only_difference() {
161        let hexatonic = names("C4", BluesForm::Hexatonic);
162        let pentatonic = names("C4", BluesForm::Pentatonic);
163        assert_eq!(hexatonic.len(), pentatonic.len() + 1);
164
165        let without_blue_note: Vec<&String> =
166            hexatonic.iter().filter(|name| *name != "F#").collect();
167        assert_eq!(without_blue_note, pentatonic.iter().collect::<Vec<_>>());
168    }
169
170    #[test]
171    fn sampling_is_reproducible_and_reaches_both_forms() {
172        let blues = WeightedHexatonicBlues::new(Pitch::from_name("C4").unwrap());
173        for seed in [0, 1, 7, 42, u64::MAX] {
174            assert_eq!(
175                blues.sample(seed).unwrap(),
176                blues.sample(seed).unwrap(),
177                "seed {seed} should be reproducible"
178            );
179        }
180
181        let forms: Vec<BluesForm> = (0..64).map(WeightedHexatonicBlues::form_for_seed).collect();
182        assert!(forms.contains(&BluesForm::Pentatonic));
183        assert!(forms.contains(&BluesForm::Hexatonic));
184    }
185
186    #[test]
187    fn every_form_realizes_on_every_common_tonic() {
188        for form in BluesForm::ALL {
189            for tonic in [
190                "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4",
191            ] {
192                let pitches = WeightedHexatonicBlues::new(Pitch::from_name(tonic).unwrap())
193                    .pitches(form)
194                    .expect("scale realizes");
195                assert_eq!(pitches.len(), form.steps().len() + 1, "{form:?} on {tonic}");
196            }
197        }
198    }
199}