Skip to main content

ym2149_core/
audio.rs

1use crate::errors::Error;
2
3/// Reference pitch of A=440.0Hz.
4const REFERENCE_PITCH: f32 = 440.0;
5
6/// One of the YM2149's 3 audio channels.
7#[derive(Debug, Clone, Copy)]
8pub enum AudioChannel {
9    A,
10    B,
11    C
12}
13
14impl AudioChannel {
15    pub fn index(&self) -> usize {
16        self.clone() as usize
17    }
18}
19
20/// This helper struct tracks the current state of the YM2149's channels.
21#[derive(Debug, Clone, Copy)]
22pub struct AudioChannelData {
23    pub address: u8,
24    pub enabled: bool,
25    pub noise_enabled: bool,
26    pub level: u8,
27    pub pitch_bend: f32,
28    pub last_note: Option<Note>,
29}
30
31impl AudioChannelData {
32    /// Creates a new `AudioChannelData` for the channel at the given register address.
33    pub fn new(address: u8) -> Self {
34        Self {
35            address: address,
36            enabled: false,
37            noise_enabled: false,
38            level: 0,
39            pitch_bend: 0.0,
40            last_note: None,
41        }
42    }
43
44    /// Set the channel's pitch bend.
45    #[allow(unused)]
46    pub fn set_pitch_bend(&mut self, byte1: u8, byte2: u8) {
47        let new: f32 = (((byte2 as u16) << 7) + byte1 as u16).into();
48        let as_semitones: f32 = (new - 8192.0) / 1024.0;
49        self.pitch_bend = as_semitones;
50    }
51}
52
53/// An accidental, represented by an i8 value that corresponds to the offset in quarter tones.
54#[repr(i8)]
55#[derive(Debug, Clone, Copy)]
56pub enum Accidental {
57    Natural = 0,
58    Sharp = 2,
59    Flat = -2,
60    MicroSharp = 1,
61    MicroFlat = -1,
62}
63
64impl From<Accidental> for f32 {
65    fn from(acc: Accidental) -> f32 {
66        (acc as i8) as f32 / 2.0
67    }
68}
69
70/// Offsets of the 7 white keys in the C Major scale (from A), in semitones.
71#[repr(i8)]
72#[derive(Debug, Clone, Copy)]
73pub enum BaseNote {
74    C = -9,
75    D = -7,
76    E = -5,
77    F = -4,
78    G = -2,
79    A = 0,
80    B = 2,
81}
82
83impl From<BaseNote> for f32 {
84    fn from(bn: BaseNote) -> f32 {
85        bn as i8 as f32
86    }
87}
88
89/// A musical note.
90///
91/// Example code:
92/// ```no_run
93/// use ym2149_core::audio::{Note, BaseNote};
94///
95/// let a_4 = Note::new(
96///     BaseNote::A,
97///     4,
98///     None
99/// );
100/// ```
101#[derive(Debug, Clone, Copy)]
102pub struct Note {
103    base_note: BaseNote,
104    octave: u8,
105    accidental: Option<Accidental>,
106    offset: f32,
107}
108
109impl Note {
110    /// Creates a new [Note](#Note) from a [BaseNote](#BaseNote), octave, and optionally an [Accidental](#Accidental)
111    pub fn new(base_note: BaseNote, octave: u8, accidental: Option<Accidental>) -> Result<Self, Error> {
112        if octave <= 14 {
113            Ok(Self {
114                base_note: base_note,
115                octave: octave.clamp(0, 14),
116                accidental: accidental,
117                offset: 0.0,
118            })
119        } else {
120            Err(Error::OctaveOutOfRange(octave))
121        }
122
123    }
124
125    /// Transposes a note +`semitones` semitones. The type of semitones is f32 because I wanted to
126    /// allow precise control of the pitch to cover all use cases.
127    pub fn transpose(self, semitones: f32) -> Self {
128        Self {
129            offset: self.offset + semitones,
130            ..self
131        }
132    }
133
134    /// Returns the frequency of this note in Hertz.
135    pub fn as_hz(&self) -> u32 {
136        // f = f0 * 2 ^ (n / 12) | f0 - reference pitch, n - semitones away from ref.
137        use libm::{powf, roundf};
138
139        let distance_a4: f32 = f32::from(self.base_note)
140            + f32::from(self.accidental.unwrap_or(Accidental::Natural))
141            + (self.octave.clamp(0, 14) as f32 - 4.0) * 12.0
142            + self.offset;
143
144        roundf(REFERENCE_PITCH * powf(2.0, distance_a4 / 12.0)) as u32
145    }
146}