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 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 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}