sim_lib_pitch_ratio/
model.rs1use crate::PitchRatioError;
4
5pub const MAX_PRIME_LIMIT: u32 = 97;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct PitchRatio {
11 pub(crate) numerator: u64,
12 pub(crate) denominator: u64,
13}
14
15impl PitchRatio {
16 pub fn new(numerator: u64, denominator: u64) -> Result<Self, PitchRatioError> {
18 if numerator == 0 || denominator == 0 {
19 return Err(PitchRatioError::NonPositiveRatio);
20 }
21 let divisor = gcd(numerator, denominator);
22 Ok(Self {
23 numerator: numerator / divisor,
24 denominator: denominator / divisor,
25 })
26 }
27
28 pub const fn numerator(self) -> u64 {
30 self.numerator
31 }
32
33 pub const fn denominator(self) -> u64 {
35 self.denominator
36 }
37
38 pub fn as_f64(self) -> f64 {
40 self.numerator as f64 / self.denominator as f64
41 }
42
43 pub const fn unison() -> Self {
45 Self {
46 numerator: 1,
47 denominator: 1,
48 }
49 }
50
51 pub fn multiply(self, other: Self) -> Result<Self, PitchRatioError> {
53 let numerator = self
54 .numerator
55 .checked_mul(other.numerator)
56 .ok_or(PitchRatioError::Overflow)?;
57 let denominator = self
58 .denominator
59 .checked_mul(other.denominator)
60 .ok_or(PitchRatioError::Overflow)?;
61 Self::new(numerator, denominator)
62 }
63
64 pub fn divide(self, other: Self) -> Result<Self, PitchRatioError> {
66 let numerator = self
67 .numerator
68 .checked_mul(other.denominator)
69 .ok_or(PitchRatioError::Overflow)?;
70 let denominator = self
71 .denominator
72 .checked_mul(other.numerator)
73 .ok_or(PitchRatioError::Overflow)?;
74 Self::new(numerator, denominator)
75 }
76
77 pub fn cents(self) -> f64 {
79 1200.0 * self.as_f64().log2()
80 }
81
82 pub fn tuning_error_cents(self, target_cents: f64) -> f64 {
84 (self.cents() - target_cents).abs()
85 }
86
87 pub fn octave_reduced(self) -> Result<Self, PitchRatioError> {
89 let mut numerator = self.numerator;
90 let mut denominator = self.denominator;
91 while numerator >= denominator.saturating_mul(2) {
92 denominator = denominator
93 .checked_mul(2)
94 .ok_or(PitchRatioError::Overflow)?;
95 }
96 while numerator < denominator {
97 numerator = numerator.checked_mul(2).ok_or(PitchRatioError::Overflow)?;
98 }
99 Self::new(numerator, denominator)
100 }
101
102 pub fn canonical(self, policy: RatioPolicy) -> Result<Self, PitchRatioError> {
104 let ratio = if policy.octave_reduce {
105 self.octave_reduced()?
106 } else {
107 self
108 };
109 if let Some(prime_limit) = policy.prime_limit {
110 ratio.factor_vector(policy.with_prime_limit(prime_limit))?;
111 }
112 Ok(ratio)
113 }
114
115 pub fn factor_vector(self, policy: RatioPolicy) -> Result<FactorVector, PitchRatioError> {
117 let Some(prime_limit) = policy.prime_limit else {
118 return Err(PitchRatioError::UnboundedFactorization);
119 };
120 let primes = primes_up_to(prime_limit)?;
121 let mut numerator = self.numerator;
122 let mut denominator = self.denominator;
123 let mut exponents = Vec::with_capacity(primes.len());
124 for prime in &primes {
125 let prime_u64 = u64::from(*prime);
126 let mut exponent = 0i16;
127 while numerator.is_multiple_of(prime_u64) {
128 numerator /= prime_u64;
129 exponent = exponent
130 .checked_add(1)
131 .ok_or(PitchRatioError::ExponentOverflow)?;
132 }
133 while denominator.is_multiple_of(prime_u64) {
134 denominator /= prime_u64;
135 exponent = exponent
136 .checked_sub(1)
137 .ok_or(PitchRatioError::ExponentOverflow)?;
138 }
139 exponents.push(exponent);
140 }
141 if numerator != 1 || denominator != 1 {
142 return Err(PitchRatioError::PrimeLimitExceeded {
143 remaining_numerator: numerator,
144 remaining_denominator: denominator,
145 prime_limit,
146 });
147 }
148 Ok(FactorVector { primes, exponents })
149 }
150}
151
152#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154pub struct RatioPolicy {
155 pub octave_reduce: bool,
157 pub prime_limit: Option<u32>,
159}
160
161impl RatioPolicy {
162 pub const fn three_limit() -> Self {
164 Self {
165 octave_reduce: true,
166 prime_limit: Some(3),
167 }
168 }
169
170 pub const fn five_limit() -> Self {
172 Self {
173 octave_reduce: true,
174 prime_limit: Some(5),
175 }
176 }
177
178 pub const fn with_prime_limit(mut self, prime_limit: u32) -> Self {
180 self.prime_limit = Some(prime_limit);
181 self
182 }
183}
184
185impl Default for RatioPolicy {
186 fn default() -> Self {
187 Self {
188 octave_reduce: true,
189 prime_limit: Some(13),
190 }
191 }
192}
193
194#[derive(Clone, Debug, PartialEq, Eq, Hash)]
196pub struct FactorVector {
197 pub primes: Vec<u32>,
199 pub exponents: Vec<i16>,
201}
202
203impl FactorVector {
204 pub fn to_ratio(&self) -> Result<PitchRatio, PitchRatioError> {
206 if self.primes.len() != self.exponents.len() {
207 return Err(PitchRatioError::InvalidFactorVector);
208 }
209 let mut numerator = 1u64;
210 let mut denominator = 1u64;
211 for (&prime, &exponent) in self.primes.iter().zip(&self.exponents) {
212 if exponent >= 0 {
213 multiply_power(&mut numerator, prime, exponent as u16)?;
214 } else {
215 multiply_power(&mut denominator, prime, exponent.unsigned_abs())?;
216 }
217 }
218 PitchRatio::new(numerator, denominator)
219 }
220}
221
222pub(crate) fn primes_up_to(limit: u32) -> Result<Vec<u32>, PitchRatioError> {
223 if !(2..=MAX_PRIME_LIMIT).contains(&limit) {
224 return Err(PitchRatioError::InvalidPrimeLimit(limit));
225 }
226 Ok((2..=limit)
227 .filter(|candidate| is_prime(*candidate))
228 .collect())
229}
230
231fn is_prime(candidate: u32) -> bool {
232 if candidate < 2 {
233 return false;
234 }
235 let mut divisor = 2;
236 while divisor * divisor <= candidate {
237 if candidate.is_multiple_of(divisor) {
238 return false;
239 }
240 divisor += 1;
241 }
242 true
243}
244
245pub(crate) fn multiply_power(
246 target: &mut u64,
247 prime: u32,
248 exponent: u16,
249) -> Result<(), PitchRatioError> {
250 for _ in 0..exponent {
251 *target = target
252 .checked_mul(u64::from(prime))
253 .ok_or(PitchRatioError::Overflow)?;
254 }
255 Ok(())
256}
257
258pub(crate) const fn gcd(mut a: u64, mut b: u64) -> u64 {
259 while b != 0 {
260 let r = a % b;
261 a = b;
262 b = r;
263 }
264 a
265}