euphony_units/pitch/mode/
intervals.rs1use 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.unsigned_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}