Skip to main content

tono_core/instrument/
note.rs

1//! Note names and the instrument error type.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::dsl::note_to_hz;
9
10/// Why an [`Instrument`](super::Instrument) could not be built.
11#[derive(Debug, Clone, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum InstrumentError {
14    /// The patch's graph is outside the streaming subset, so it can't play in
15    /// real time (e.g. a `tracks` root, a `normalize` stage, or a sampler seq).
16    NotStreamable,
17    /// The patch failed to instantiate at its defaults (a bad param path/value).
18    BadPatch(String),
19}
20
21impl fmt::Display for InstrumentError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            InstrumentError::NotStreamable => {
25                write!(
26                    f,
27                    "instrument patch is not streamable (can't play in real time)"
28                )
29            }
30            InstrumentError::BadPatch(e) => write!(f, "instrument patch is invalid: {e}"),
31        }
32    }
33}
34
35impl std::error::Error for InstrumentError {}
36
37/// A musical pitch as a MIDI note number (0–127). `A4` = 69 = 440 Hz.
38#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord, Serialize, Deserialize)]
39pub struct Note(pub u8);
40
41impl Note {
42    /// Middle C.
43    pub const C4: Note = Note(60);
44    /// Concert A (440 Hz).
45    pub const A4: Note = Note(69);
46
47    /// The note's frequency in Hz (equal temperament, A4 = 440).
48    pub fn freq(self) -> f32 {
49        440.0 * 2f32.powf((self.0 as f32 - 69.0) / 12.0)
50    }
51
52    /// The MIDI note number.
53    pub fn midi(self) -> u8 {
54        self.0
55    }
56
57    /// Parse a note name (`"C4"`, `"F#3"`, `"Bb5"`) or `"midi:60"` into the
58    /// nearest MIDI note.
59    pub fn parse(s: &str) -> Option<Note> {
60        let hz = note_to_hz(s)?;
61        let midi = crate::dsp::hz_to_midi(hz).round();
62        if (0.0..=127.0).contains(&midi) {
63            Some(Note(midi as u8))
64        } else {
65            None
66        }
67    }
68
69    /// Shift by `semitones` (clamped to the MIDI range).
70    pub fn transpose(self, semitones: i32) -> Note {
71        Note((self.0 as i32 + semitones).clamp(0, 127) as u8)
72    }
73}
74
75impl fmt::Display for Note {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        const NAMES: [&str; 12] = [
78            "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
79        ];
80        write!(
81            f,
82            "{}{}",
83            NAMES[(self.0 % 12) as usize],
84            self.0 as i32 / 12 - 1
85        )
86    }
87}
88
89impl FromStr for Note {
90    type Err = ();
91    fn from_str(s: &str) -> Result<Note, ()> {
92        Note::parse(s).ok_or(())
93    }
94}
95
96impl From<u8> for Note {
97    fn from(midi: u8) -> Note {
98        Note(midi)
99    }
100}