euphony_core/pitch/mode/
intervals.rs

1use crate::pitch::interval::Interval;
2use core::{cmp::Ordering, fmt};
3
4#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
5pub struct ModeIntervals {
6    pub tones: &'static [Interval],
7    pub steps: &'static [Interval],
8    pub intervals: &'static [Interval],
9}
10
11impl fmt::Debug for ModeIntervals {
12    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
13        write!(f, "ModeIntervals({:?})", self.steps)
14    }
15}
16
17impl fmt::Display for ModeIntervals {
18    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
19        f.debug_list()
20            .entries(self.intervals.iter().map(|step| step.as_ratio()))
21            .finish()
22    }
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
26pub enum RoundingStrategy {
27    Down,
28    Up,
29    NearestDown,
30    NearestUp,
31    TowardsZero,
32    AwayFromZero,
33    Reject,
34    Pass,
35}
36
37impl Default for RoundingStrategy {
38    fn default() -> Self {
39        RoundingStrategy::NearestDown
40    }
41}
42
43impl ModeIntervals {
44    pub const fn len(&self) -> usize {
45        self.intervals.len()
46    }
47
48    pub const fn is_empty(&self) -> bool {
49        self.intervals.is_empty()
50    }
51
52    pub fn collapse(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
53        self.checked_collapse(interval, rounding_strategy)
54            .expect("Interval could not be collapsed")
55    }
56
57    pub fn checked_collapse(
58        &self,
59        interval: Interval,
60        rounding_strategy: RoundingStrategy,
61    ) -> Option<Interval> {
62        round_interval(&self.tones, interval, rounding_strategy)
63    }
64
65    pub fn expand(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
66        self.checked_expand(interval, rounding_strategy)
67            .expect("Interval could not be expanded")
68    }
69
70    pub fn checked_expand(
71        &self,
72        interval: Interval,
73        rounding_strategy: RoundingStrategy,
74    ) -> Option<Interval> {
75        round_interval(&self.intervals, interval, rounding_strategy)
76    }
77}
78
79fn round_interval(
80    intervals: &[Interval],
81    interval: Interval,
82    rounding_strategy: RoundingStrategy,
83) -> Option<Interval> {
84    use RoundingStrategy::*;
85
86    let scaled = (interval * intervals.len()).as_ratio();
87    let scaled = match rounding_strategy {
88        _ if scaled.is_whole() => scaled.whole(),
89        Down => scaled.floor().whole(),
90        Up => scaled.ceil().whole(),
91        TowardsZero => scaled.truncate().whole(),
92        AwayFromZero => scaled.round().whole(),
93        Pass => return Some(interval),
94        Reject => return None,
95        NearestDown | NearestUp => {
96            let lower = get_scaled_interval(&intervals, scaled.floor().whole());
97            let upper = get_scaled_interval(&intervals, scaled.ceil().whole());
98            return match lower.cmp(&upper) {
99                Ordering::Equal if rounding_strategy == NearestDown => Some(lower),
100                Ordering::Equal => Some(upper),
101                Ordering::Greater => Some(upper),
102                Ordering::Less => Some(lower),
103            };
104        }
105    };
106
107    Some(get_scaled_interval(intervals, scaled))
108}
109
110fn get_scaled_interval(intervals: &[Interval], scaled: i64) -> Interval {
111    let len = intervals.len();
112
113    if scaled < 0 {
114        let index = (len - (scaled.abs() as usize % len)) % len;
115        let octave = (scaled.abs() - 1) as usize / len;
116        let value = -(Interval(1, 1) - intervals[index]);
117        value - Interval(1, 1) * octave
118    } else {
119        let index = scaled as usize % len;
120        let octave = scaled as usize / len;
121        intervals[index] + Interval(1, 1) * octave
122    }
123}
124
125impl core::ops::Mul<Interval> for ModeIntervals {
126    type Output = Interval;
127
128    fn mul(self, interval: Interval) -> Self::Output {
129        self.expand(interval, Default::default())
130    }
131}
132
133impl core::ops::Div<ModeIntervals> for Interval {
134    type Output = Interval;
135
136    fn div(self, mode: ModeIntervals) -> Self::Output {
137        mode.collapse(self, Default::default())
138    }
139}
140
141#[test]
142fn interval_mode_bounds_test() {
143    use super::heptatonic::MAJOR;
144
145    for i in -10000..10000 {
146        let _ = MAJOR.expand(i.into(), Default::default());
147    }
148}