1- /// 12-tone equal temperament following the A440Hz convention
1+ //! 12-tone equal temperament following the A440Hz convention
22use caw_core:: { Buf , ConstBuf , Sig , SigCtx , SigT } ;
3+ use std:: { fmt:: Display , str:: FromStr } ;
34
45#[ derive( Debug , Clone , Copy , PartialEq , PartialOrd ) ]
56pub struct Octave ( u8 ) ;
67
78impl Octave {
89 pub const MAX_OCTAVE : u8 = 8 ;
910
10- pub fn new ( i : u8 ) -> Self {
11+ pub const fn new ( i : u8 ) -> Self {
1112 assert ! ( i <= Self :: MAX_OCTAVE ) ;
1213 Self ( i)
1314 }
1415
16+ pub const fn index ( self ) -> u8 {
17+ self . 0
18+ }
19+
1520 pub const _0: Self = Self ( 0 ) ;
1621 pub const _1: Self = Self ( 1 ) ;
1722 pub const _2: Self = Self ( 2 ) ;
@@ -92,6 +97,48 @@ impl NoteName {
9297 self . relative_midi_index
9398 }
9499
100+ /// Returns a str representation of the note name where all accidentals are sharp, formatted
101+ /// like "C" or "C_sharp"
102+ pub const fn to_str_sharp ( self ) -> & ' static str {
103+ match self . relative_midi_index {
104+ 0 => "C" ,
105+ 1 => "C_sharp" ,
106+ 2 => "D" ,
107+ 3 => "D_sharp" ,
108+ 4 => "E" ,
109+ 5 => "F" ,
110+ 6 => "F_sharp" ,
111+ 7 => "G" ,
112+ 8 => "G_sharp" ,
113+ 9 => "A" ,
114+ 10 => "A_sharp" ,
115+ 11 => "B" ,
116+ _ => unreachable ! ( ) ,
117+ }
118+ }
119+
120+ /// Parses a str like "C" or "C_sharp"
121+ pub fn from_str_sharp ( s : & str ) -> Option < Self > {
122+ let relative_midi_index = match s {
123+ "C" => 0 ,
124+ "C_sharp" => 1 ,
125+ "D" => 2 ,
126+ "D_sharp" => 3 ,
127+ "E" => 4 ,
128+ "F" => 5 ,
129+ "F_sharp" => 6 ,
130+ "G" => 7 ,
131+ "G_sharp" => 8 ,
132+ "A" => 9 ,
133+ "A_sharp" => 10 ,
134+ "B" => 11 ,
135+ _ => return None ,
136+ } ;
137+ Some ( Self {
138+ relative_midi_index,
139+ } )
140+ }
141+
95142 const fn wrapping_add_semitones ( self , num_semitones : i8 ) -> Self {
96143 Self :: from_index (
97144 ( self . to_index ( ) as i8 + num_semitones)
@@ -160,7 +207,7 @@ impl Note {
160207 }
161208 }
162209
163- pub fn to_midi_index ( self ) -> u8 {
210+ pub const fn to_midi_index ( self ) -> u8 {
164211 self . midi_index
165212 }
166213
@@ -174,11 +221,20 @@ impl Note {
174221 }
175222 }
176223
177- pub fn octave ( self ) -> Octave {
178- Octave :: new ( self . midi_index / NOTES_PER_OCTAVE )
224+ pub const fn octave ( self ) -> Octave {
225+ Octave :: new ( ( self . midi_index - C0_MIDI_INDEX ) / NOTES_PER_OCTAVE )
179226 }
180227
181- pub fn add_semitones_checked ( self , num_semitones : i16 ) -> Option < Self > {
228+ pub const fn note_name ( self ) -> NoteName {
229+ NoteName :: from_index (
230+ ( self . midi_index - C0_MIDI_INDEX ) % NOTES_PER_OCTAVE ,
231+ )
232+ }
233+
234+ pub const fn add_semitones_checked (
235+ self ,
236+ num_semitones : i16 ,
237+ ) -> Option < Self > {
182238 let midi_index = self . midi_index as i16 + num_semitones;
183239 if midi_index < 0 || midi_index > MAX_MIDI_INDEX as i16 {
184240 None
@@ -189,21 +245,79 @@ impl Note {
189245 }
190246 }
191247
192- pub fn add_octaves_checked ( self , num_octaves : i8 ) -> Option < Self > {
248+ pub const fn add_octaves_checked ( self , num_octaves : i8 ) -> Option < Self > {
193249 self . add_semitones_checked ( num_octaves as i16 * NOTES_PER_OCTAVE as i16 )
194250 }
195251
196- pub fn add_semitones ( self , num_semitones : i16 ) -> Self {
252+ pub const fn add_semitones ( self , num_semitones : i16 ) -> Self {
197253 Self {
198254 midi_index : ( self . midi_index as i16 + num_semitones) as u8 ,
199255 }
200256 }
201257
202- pub fn add_octaves ( self , num_octaves : i8 ) -> Self {
258+ pub const fn add_octaves ( self , num_octaves : i8 ) -> Self {
203259 self . add_semitones ( num_octaves as i16 * NOTES_PER_OCTAVE as i16 )
204260 }
205261}
206262
263+ /// Example formats: "C_sharp-4", "C-4"
264+ impl Display for Note {
265+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
266+ write ! (
267+ f,
268+ "{}-{}" ,
269+ self . note_name( ) . to_str_sharp( ) ,
270+ self . octave( ) . index( )
271+ )
272+ }
273+ }
274+
275+ /// Expected format: "C_sharp-4", "C-4"
276+ impl FromStr for Note {
277+ type Err = String ;
278+
279+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
280+ let mut split = s. split ( "-" ) ;
281+ if let Some ( name) = split. next ( ) {
282+ if let Some ( name) = NoteName :: from_str_sharp ( name) {
283+ if let Some ( octave_index) = split. next ( ) {
284+ match octave_index. parse :: < u8 > ( ) {
285+ Ok ( octave_index) => {
286+ if octave_index <= Octave :: MAX_OCTAVE {
287+ if split. next ( ) . is_none ( ) {
288+ Ok ( Note :: new (
289+ name,
290+ Octave :: new ( octave_index) ,
291+ ) )
292+ } else {
293+ Err ( format ! (
294+ "Multiple dashes in note string."
295+ ) )
296+ }
297+ } else {
298+ Err ( format ! (
299+ "Octave index {} too high (max is {})." ,
300+ octave_index,
301+ Octave :: MAX_OCTAVE
302+ ) )
303+ }
304+ }
305+ Err ( e) => {
306+ Err ( format ! ( "Failed to parse octave index: {}" , e) )
307+ }
308+ }
309+ } else {
310+ Err ( format ! ( "No dash in note string." ) )
311+ }
312+ } else {
313+ Err ( format ! ( "Failed to parse note name: {}" , name) )
314+ }
315+ } else {
316+ Err ( format ! ( "Failed to parse note name." ) )
317+ }
318+ }
319+ }
320+
207321pub trait IntoNoteFreqHz < N >
208322where
209323 N : SigT < Item = Note > ,
@@ -862,3 +976,27 @@ pub mod note {
862976 pub const B7 : Note = Note :: B7 ;
863977 pub const B8 : Note = Note :: B8 ;
864978}
979+
980+ #[ cfg( test) ]
981+ mod test {
982+ use super :: * ;
983+
984+ #[ test]
985+ fn octave_round_trip ( ) {
986+ assert_eq ! ( Note :: new( note_name:: C , OCTAVE_0 ) . octave( ) , OCTAVE_0 ) ;
987+ }
988+
989+ #[ test]
990+ fn note_name_round_trip ( ) {
991+ assert_eq ! ( Note :: new( note_name:: D , OCTAVE_3 ) . note_name( ) , note_name:: D ) ;
992+ }
993+
994+ #[ test]
995+ fn string_round_trip ( ) {
996+ assert_eq ! ( note:: D6 . to_string( ) . parse:: <Note >( ) . unwrap( ) , note:: D6 ) ;
997+ assert_eq ! (
998+ note:: A_SHARP5 . to_string( ) . parse:: <Note >( ) . unwrap( ) ,
999+ note:: A_SHARP5
1000+ ) ;
1001+ }
1002+ }
0 commit comments