Skip to main content

klib/core/
pitch.rs

1//! A module for the [`Pitch`] enum.
2
3// Traits.
4
5use std::sync::LazyLock;
6
7use super::helpers::mel;
8
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12/// A trait for types that have a pitch property.
13pub trait HasPitch {
14    /// Returns the pitch of the type (usually a [`Note`]).
15    fn pitch(&self) -> Pitch;
16}
17
18/// A trait for types that have a base frequency property.
19pub trait HasBaseFrequency {
20    /// Returns the base frequency of the type (usually a [`Pitch`]).
21    fn base_frequency(&self) -> f32;
22}
23
24/// A trait for types that have a frequency property.
25pub trait HasFrequency {
26    /// Returns the frequency of the type (usually a [`Note`]).
27    fn frequency(&self) -> f32;
28
29    /// Returns the frequency range of the type (usually a [`Note`]).
30    /// Essentially, mid way between the frequency and the next frequency on either side.
31    fn frequency_range(&self) -> (f32, f32) {
32        let frequency = self.frequency();
33
34        (frequency * (1.0 - 1.0 / 17.462 / 2.0), frequency * (1.0 + 1.0 / 16.8196 / 2.0))
35    }
36
37    /// Returns the tight frequency range of the type (usually a [`Note`]).
38    /// Essentially, 1/8 the way between the frequency and the next frequency on either side.
39    fn tight_frequency_range(&self) -> (f32, f32) {
40        let frequency = self.frequency();
41
42        (frequency * (1.0 - 1.0 / 17.462 / 8.0), frequency * (1.0 + 1.0 / 16.8196 / 8.0))
43    }
44}
45
46/// A trait for types that have a mel property.
47pub trait HasMel: HasFrequency {
48    /// Returns the mel of the type (usually a [`Note`]).
49    fn mel(&self) -> f32 {
50        mel(self.frequency())
51    }
52}
53
54#[cfg(feature = "audio")]
55use super::base::{Playable, PlaybackHandle, Res};
56
57#[cfg(feature = "audio")]
58impl<T: HasFrequency> Playable for T {
59    fn play(&self, delay: std::time::Duration, length: std::time::Duration, fade_in: std::time::Duration) -> Res<PlaybackHandle> {
60        use rodio::{source::SineWave, OutputStreamBuilder, Sink, Source};
61
62        let stream = OutputStreamBuilder::open_default_stream()?;
63        let sink = Sink::connect_new(stream.mixer());
64        let source = SineWave::new(self.frequency()).take_duration(length - delay).buffered().delay(delay).fade_in(fade_in).amplify(0.20);
65        sink.append(source);
66
67        Ok(PlaybackHandle::new(stream, vec![sink]))
68    }
69}
70
71// Enum.
72
73/// An enum representing the pitch of a note.
74///
75/// The frequencies of the pitches are based on the [A4 frequency](https://en.wikipedia.org/wiki/A4_(pitch_standard)).
76/// There is no enharmonic representation here, so all of the sharps are represented.
77#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Ord, PartialOrd)]
79#[repr(u8)]
80pub enum Pitch {
81    /// The pitch C.
82    C,
83    /// The pitch C♯.
84    DFlat,
85    /// The pitch D.
86    D,
87    /// The pitch D♯.
88    EFlat,
89    /// The pitch E.
90    E,
91    /// The pitch F.
92    F,
93    /// The pitch F♯.
94    GFlat,
95    /// The pitch G.
96    G,
97    /// The pitch G♯.
98    AFlat,
99    /// The pitch A.
100    A,
101    /// The pitch A♯.
102    BFlat,
103    /// The pitch B.
104    B,
105}
106
107// Pitch impls.
108
109impl HasBaseFrequency for Pitch {
110    #[coverage(off)]
111    fn base_frequency(&self) -> f32 {
112        match self {
113            Pitch::C => 16.35,
114            Pitch::DFlat => 17.32,
115            Pitch::D => 18.35,
116            Pitch::EFlat => 19.45,
117            Pitch::E => 20.60,
118            Pitch::F => 21.83,
119            Pitch::GFlat => 23.12,
120            Pitch::G => 24.50,
121            Pitch::AFlat => 25.96,
122            Pitch::A => 27.50,
123            Pitch::BFlat => 29.14,
124            Pitch::B => 30.87,
125        }
126    }
127}
128
129impl HasPitch for Pitch {
130    fn pitch(&self) -> Pitch {
131        *self
132    }
133}
134
135impl TryFrom<u8> for Pitch {
136    type Error = &'static str;
137
138    fn try_from(value: u8) -> Result<Self, Self::Error> {
139        match value {
140            0 => Ok(Pitch::C),
141            1 => Ok(Pitch::DFlat),
142            2 => Ok(Pitch::D),
143            3 => Ok(Pitch::EFlat),
144            4 => Ok(Pitch::E),
145            5 => Ok(Pitch::F),
146            6 => Ok(Pitch::GFlat),
147            7 => Ok(Pitch::G),
148            8 => Ok(Pitch::AFlat),
149            9 => Ok(Pitch::A),
150            10 => Ok(Pitch::BFlat),
151            11 => Ok(Pitch::B),
152            _ => Err("Invalid pitch"),
153        }
154    }
155}
156
157// Statics.
158
159/// An array of all the pitches.
160pub static ALL_PITCHES: LazyLock<[Pitch; 12]> = LazyLock::new(|| {
161    [
162        Pitch::C,
163        Pitch::DFlat,
164        Pitch::D,
165        Pitch::EFlat,
166        Pitch::E,
167        Pitch::F,
168        Pitch::GFlat,
169        Pitch::G,
170        Pitch::AFlat,
171        Pitch::A,
172        Pitch::BFlat,
173        Pitch::B,
174    ]
175});
176
177// Tests.
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use pretty_assertions::assert_eq;
183
184    #[test]
185    fn test_properties() {
186        assert_eq!(Pitch::G.pitch(), Pitch::G);
187        assert_eq!(Pitch::G.base_frequency(), 24.50);
188    }
189}