tune/
tuning.rs

1//! Types for working with musical tunings.
2
3#![allow(clippy::wrong_self_convention)] // Would require a breaking change. Fix when appropriate.
4
5use crate::{
6    note::{Note, NoteLetter},
7    pitch::{Pitch, Pitched, Ratio},
8};
9
10/// A [`Tuning`] maps keys or notes of type `K` to a [`Pitch`] or vice versa.
11pub trait Tuning<K> {
12    /// Returns the [`Pitch`] of the given key or note `K` in the current [`Tuning`].
13    ///
14    /// # Examples
15    ///
16    /// ```
17    /// # use assert_approx_eq::assert_approx_eq;
18    /// # use tune::note::NoteLetter;
19    /// # use tune::tuning::ConcertPitch;
20    /// use tune::tuning::Tuning;
21    ///
22    /// let standard_tuning = ConcertPitch::default();
23    /// let a5 = NoteLetter::A.in_octave(5);
24    ///
25    /// assert_approx_eq!(standard_tuning.pitch_of(a5).as_hz(), 880.0);
26    /// ```
27    fn pitch_of(&self, key: K) -> Pitch;
28
29    /// Finds a closest key or note [`Approximation`] for the given [`Pitch`] in the current [`Tuning`].
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// # use assert_approx_eq::assert_approx_eq;
35    /// # use tune::note::NoteLetter;
36    /// # use tune::pitch::Pitch;
37    /// # use tune::pitch::Ratio;
38    /// # use tune::tuning::ConcertPitch;
39    /// use tune::tuning::Tuning;
40    ///
41    /// let standard_tuning = ConcertPitch::default();
42    /// let a5 = NoteLetter::A.in_octave(5);
43    /// let detuned_a5_pitch = Pitch::of(a5) * Ratio::from_cents(10.0);
44    ///
45    /// let approximation = standard_tuning.find_by_pitch(detuned_a5_pitch);
46    /// assert_eq!(approximation.approx_value, a5);
47    /// assert_approx_eq!(approximation.deviation.as_cents(), 10.0);
48    /// ```
49    fn find_by_pitch(&self, pitch: Pitch) -> Approximation<K>;
50
51    /// Wraps `self` in a type adapter s.t. it can be used in functions that are generic over [`KeyboardMapping<K>`].
52    fn as_linear_mapping(self) -> LinearMapping<Self>
53    where
54        Self: Sized,
55    {
56        LinearMapping { inner: self }
57    }
58}
59
60/// `impl` forwarding for references.
61impl<K, T: Tuning<K> + ?Sized> Tuning<K> for &T {
62    fn pitch_of(&self, key: K) -> Pitch {
63        T::pitch_of(self, key)
64    }
65
66    fn find_by_pitch(&self, pitch: Pitch) -> Approximation<K> {
67        T::find_by_pitch(self, pitch)
68    }
69}
70
71/// A key or note `K` paired with an appropriate [`Tuning<K>`] is considered [`Pitched`].
72///
73/// # Examples
74///
75/// ```
76/// # use assert_approx_eq::assert_approx_eq;
77/// # use tune::note::NoteLetter;
78/// # use tune::pitch::Pitch;
79/// # use tune::tuning::ConcertPitch;
80/// use tune::pitch::Pitched;
81///
82/// let concert_pitch = ConcertPitch::from_a4_pitch(Pitch::from_hz(432.0));
83/// assert_approx_eq!((NoteLetter::A.in_octave(5), concert_pitch).pitch().as_hz(), 864.0);
84/// ```
85impl<K: Copy, T: Tuning<K> + ?Sized> Pitched for (K, T) {
86    fn pitch(&self) -> Pitch {
87        self.1.pitch_of(self.0)
88    }
89}
90
91/// A [`Scale`] is a tuning whose [`Pitch`]es can be accessed in a sorted manner.
92///
93/// Accessing pitches in order can be important, e.g. when handling pitches in a certain frequency window.
94pub trait Scale {
95    /// Returns the [`Pitch`] at the given scale degree in the current [`Scale`].
96    fn sorted_pitch_of(&self, degree: i32) -> Pitch;
97
98    /// Finds a closest scale degree [`Approximation`] for the given [`Pitch`] in the current [`Scale`].
99    fn find_by_pitch_sorted(&self, pitch: Pitch) -> Approximation<i32>;
100
101    /// Wraps `self` in a type adapter s.t. it can be used in functions that are generic over [`Tuning<i32>`].
102    fn as_sorted_tuning(self) -> SortedTuning<Self>
103    where
104        Self: Sized,
105    {
106        SortedTuning { inner: self }
107    }
108}
109
110/// `impl` forwarding for references.
111impl<S: Scale + ?Sized> Scale for &S {
112    fn sorted_pitch_of(&self, degree: i32) -> Pitch {
113        S::sorted_pitch_of(self, degree)
114    }
115
116    fn find_by_pitch_sorted(&self, pitch: Pitch) -> Approximation<i32> {
117        S::find_by_pitch_sorted(self, pitch)
118    }
119}
120
121/// Type adapter returned by [`Scale::as_sorted_tuning`].
122pub struct SortedTuning<S> {
123    inner: S,
124}
125
126impl<S: Scale> Tuning<i32> for SortedTuning<S> {
127    fn pitch_of(&self, key: i32) -> Pitch {
128        self.inner.sorted_pitch_of(key)
129    }
130
131    fn find_by_pitch(&self, pitch: Pitch) -> Approximation<i32> {
132        self.inner.find_by_pitch_sorted(pitch)
133    }
134}
135
136/// Similar to a [`Tuning`] but not designed to be surjective or injecive.
137///
138/// An inversion operation is not provided.
139/// In return, zero or multiple keys/notes can point to a [`Pitch`].
140pub trait KeyboardMapping<K> {
141    /// Returns the [`Pitch`] of the provided key or note.
142    fn maybe_pitch_of(&self, key: K) -> Option<Pitch>;
143}
144
145/// `impl` forwarding for references.
146impl<K, T: KeyboardMapping<K> + ?Sized> KeyboardMapping<K> for &T {
147    fn maybe_pitch_of(&self, key: K) -> Option<Pitch> {
148        T::maybe_pitch_of(self, key)
149    }
150}
151
152/// Type adapter returned by [`Tuning::as_linear_mapping`].
153pub struct LinearMapping<T> {
154    inner: T,
155}
156
157impl<K, T: Tuning<K>> KeyboardMapping<K> for LinearMapping<T> {
158    fn maybe_pitch_of(&self, key: K) -> Option<Pitch> {
159        Some(self.inner.pitch_of(key))
160    }
161}
162
163/// The result of a find operation on [`Scale`]s or [`Tuning`]s.
164#[derive(Copy, Clone, Debug)]
165pub struct Approximation<K> {
166    /// The value to find.
167    pub approx_value: K,
168
169    /// The deviation from the ideal value.
170    pub deviation: Ratio,
171}
172
173/// A [`ConcertPitch`] enables [`Note`]s to sound at a [`Pitch`] different to what would be expected in 440&nbsp;Hz standard tuning.
174///
175/// To access the full potential of [`ConcertPitch`]es have a look at the [`Tuning`] and [`PitchedNote`](crate::note::PitchedNote) traits.
176#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
177pub struct ConcertPitch {
178    a4_pitch: Pitch,
179}
180
181impl ConcertPitch {
182    /// Creates a [`ConcertPitch`] with the given `a4_pitch`.
183    pub fn from_a4_pitch(a4_pitch: impl Pitched) -> Self {
184        Self {
185            a4_pitch: a4_pitch.pitch(),
186        }
187    }
188
189    /// Creates a [`ConcertPitch`] from the given `note` and `pitched` value.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// # use assert_approx_eq::assert_approx_eq;
195    /// # use tune::note::NoteLetter;
196    /// # use tune::tuning::ConcertPitch;
197    /// # use tune::pitch::Pitch;
198    /// let c4 = NoteLetter::C.in_octave(4);
199    /// let fixed_c4_tuning = ConcertPitch::from_note_and_pitch(c4, Pitch::from_hz(260.0));
200    /// assert_approx_eq!(fixed_c4_tuning.a4_pitch().as_hz(), 437.266136);
201    /// ```
202    pub fn from_note_and_pitch(note: Note, pitched: impl Pitched) -> Self {
203        Self {
204            a4_pitch: pitched.pitch()
205                * Ratio::from_semitones(f64::from(
206                    note.num_semitones_before(NoteLetter::A.in_octave(4)),
207                )),
208        }
209    }
210
211    /// Returns the [`Pitch`] of A4.
212    pub fn a4_pitch(self) -> Pitch {
213        self.a4_pitch
214    }
215}
216
217/// The default [`ConcertPitch`] is A4 sounding at 440&nbsp;Hz.
218///
219/// # Examples
220///
221/// ```
222/// # use assert_approx_eq::assert_approx_eq;
223/// # use tune::tuning::ConcertPitch;
224/// assert_approx_eq!(ConcertPitch::default().a4_pitch().as_hz(), 440.0);
225/// ```
226impl Default for ConcertPitch {
227    fn default() -> Self {
228        Self::from_a4_pitch(Pitch::from_hz(440.0))
229    }
230}
231
232/// A [`ConcertPitch`] maps [`Note`]s to [`Pitch`]es and is considered a [`Tuning`].
233///
234/// # Examples
235///
236/// ```rust
237/// # use assert_approx_eq::assert_approx_eq;
238/// # use tune::note::NoteLetter;
239/// # use tune::tuning::ConcertPitch;
240/// # use tune::pitch::Pitch;
241/// use tune::tuning::Tuning;
242///
243/// let c4 = NoteLetter::C.in_octave(4);
244/// let a4 = NoteLetter::A.in_octave(4);
245///
246/// let standard_tuning = ConcertPitch::default();
247/// assert_approx_eq!(standard_tuning.pitch_of(c4).as_hz(), 261.625565);
248/// assert_approx_eq!(standard_tuning.pitch_of(a4).as_hz(), 440.0);
249///
250/// let healing_tuning = ConcertPitch::from_a4_pitch(Pitch::from_hz(432.0));
251/// assert_approx_eq!(healing_tuning.pitch_of(c4).as_hz(), 256.868737);
252/// assert_approx_eq!(healing_tuning.pitch_of(a4).as_hz(), 432.0);
253/// ```
254impl Tuning<Note> for ConcertPitch {
255    fn pitch_of(&self, note: Note) -> Pitch {
256        self.a4_pitch * Ratio::from_semitones(NoteLetter::A.in_octave(4).num_semitones_before(note))
257    }
258
259    fn find_by_pitch(&self, pitch: Pitch) -> Approximation<Note> {
260        let semitones_above_a4 = Ratio::between_pitches(self.a4_pitch, pitch).as_semitones();
261        let round_to_lower_step = Ratio::from_float(1.000001);
262        let approx_semitones_above_a4 =
263            (semitones_above_a4 - round_to_lower_step.as_semitones()).round();
264
265        Approximation {
266            approx_value: Note::from_midi_number(
267                approx_semitones_above_a4 as i32 + NoteLetter::A.in_octave(4).midi_number(),
268            ),
269            deviation: Ratio::from_semitones(semitones_above_a4 - approx_semitones_above_a4),
270        }
271    }
272}
273
274/// Convenience implementation enabling to write `()` instead of [`ConcertPitch::default()`].
275///
276/// # Examples
277///
278/// ```
279/// # use tune::note::Note;
280/// # use tune::pitch::Pitch;
281/// use tune::pitch::Pitched;
282///
283/// assert_eq!(Pitch::from_hz(880.0).find_in_tuning(()).approx_value, Note::from_midi_number(81));
284/// ```
285impl Tuning<Note> for () {
286    fn pitch_of(&self, note: Note) -> Pitch {
287        ConcertPitch::default().pitch_of(note)
288    }
289
290    fn find_by_pitch(&self, pitch: Pitch) -> Approximation<Note> {
291        ConcertPitch::default().find_by_pitch(pitch)
292    }
293}