Skip to main content

sim_lib_pitch_ratio/
model.rs

1//! Ratio identity, policy, factors, and errors.
2
3use crate::PitchRatioError;
4
5/// Largest prime allowed for a bounded factor vector.
6pub const MAX_PRIME_LIMIT: u32 = 97;
7
8/// Exact positive reduced musical ratio.
9#[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    /// Construct a positive reduced ratio.
17    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    /// Reduced numerator.
29    pub const fn numerator(self) -> u64 {
30        self.numerator
31    }
32
33    /// Reduced denominator.
34    pub const fn denominator(self) -> u64 {
35        self.denominator
36    }
37
38    /// Return this ratio as a floating frequency multiplier.
39    pub fn as_f64(self) -> f64 {
40        self.numerator as f64 / self.denominator as f64
41    }
42
43    /// Exact unison ratio.
44    pub const fn unison() -> Self {
45        Self {
46            numerator: 1,
47            denominator: 1,
48        }
49    }
50
51    /// Multiply two reduced ratios exactly.
52    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    /// Divide this ratio by another reduced ratio exactly.
65    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    /// Convert this ratio to cents.
78    pub fn cents(self) -> f64 {
79        1200.0 * self.as_f64().log2()
80    }
81
82    /// Absolute cents error from a target.
83    pub fn tuning_error_cents(self, target_cents: f64) -> f64 {
84        (self.cents() - target_cents).abs()
85    }
86
87    /// Return this ratio folded into `[1, 2)` by octave equivalence.
88    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    /// Apply a ratio policy to this interval.
103    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    /// Factor this ratio into signed exponents for primes up to the policy limit.
116    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/// Ratio canonicalization and admissibility policy.
153#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
154pub struct RatioPolicy {
155    /// Fold ratios into one octave when true.
156    pub octave_reduce: bool,
157    /// Reject factors above this prime and require bounded factorization.
158    pub prime_limit: Option<u32>,
159}
160
161impl RatioPolicy {
162    /// Policy with octave reduction and a three-limit factor vector.
163    pub const fn three_limit() -> Self {
164        Self {
165            octave_reduce: true,
166            prime_limit: Some(3),
167        }
168    }
169
170    /// Policy with octave reduction and a five-limit factor vector.
171    pub const fn five_limit() -> Self {
172        Self {
173            octave_reduce: true,
174            prime_limit: Some(5),
175        }
176    }
177
178    /// Return this policy with a prime limit.
179    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/// Signed prime-exponent vector for a ratio.
195#[derive(Clone, Debug, PartialEq, Eq, Hash)]
196pub struct FactorVector {
197    /// Prime basis in ascending order.
198    pub primes: Vec<u32>,
199    /// Signed exponents matching `primes`.
200    pub exponents: Vec<i16>,
201}
202
203impl FactorVector {
204    /// Rebuild the ratio represented by this factor vector.
205    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}