@@ -1095,6 +1095,64 @@ impl AddAssign for Interval {
10951095
10961096We can also relate Notes to Intervals pretty well:
10971097
1098+ ``` rust
1099+ #[test]
1100+ fn test_get_note_interval_from_c () {
1101+ use Interval :: * ;
1102+ assert_eq! (Note :: from_str (" A" ). unwrap (). interval_from_c (), Maj6 );
1103+ assert_eq! (Note :: from_str (" A#" ). unwrap (). interval_from_c (), Min7 );
1104+ assert_eq! (Note :: from_str (" Bb" ). unwrap (). interval_from_c (), Min7 );
1105+ assert_eq! (Note :: from_str (" B" ). unwrap (). interval_from_c (), Maj7 );
1106+ assert_eq! (Note :: from_str (" C" ). unwrap (). interval_from_c (), Unison );
1107+ assert_eq! (Note :: from_str (" C#" ). unwrap (). interval_from_c (), Min2 );
1108+ assert_eq! (Note :: from_str (" D" ). unwrap (). interval_from_c (), Maj2 );
1109+ assert_eq! (Note :: from_str (" D#" ). unwrap (). interval_from_c (), Min3 );
1110+ assert_eq! (Note :: from_str (" E" ). unwrap (). interval_from_c (), Maj3 );
1111+ assert_eq! (Note :: from_str (" F" ). unwrap (). interval_from_c (), Perfect4 );
1112+ assert_eq! (Note :: from_str (" F#" ). unwrap (). interval_from_c (), Tritone );
1113+ assert_eq! (Note :: from_str (" G" ). unwrap (). interval_from_c (), Perfect5 );
1114+ assert_eq! (Note :: from_str (" G#" ). unwrap (). interval_from_c (), Min6 );
1115+ }
1116+
1117+ #[test]
1118+ fn test_get_note_offset () {
1119+ use Interval :: * ;
1120+ let a = Note :: from_str (" A" ). unwrap ();
1121+ assert_eq! (Note :: from_str (" A" ). unwrap (). get_offset (a ), Unison );
1122+ assert_eq! (Note :: from_str (" A#" ). unwrap (). get_offset (a ), Min2 );
1123+ assert_eq! (Note :: from_str (" B" ). unwrap (). get_offset (a ), Maj2 );
1124+ assert_eq! (Note :: from_str (" C" ). unwrap (). get_offset (a ), Min3 );
1125+ assert_eq! (Note :: from_str (" C#" ). unwrap (). get_offset (a ), Maj3 );
1126+ assert_eq! (Note :: from_str (" D" ). unwrap (). get_offset (a ), Perfect4 );
1127+ assert_eq! (Note :: from_str (" D#" ). unwrap (). get_offset (a ), Tritone );
1128+ assert_eq! (Note :: from_str (" E" ). unwrap (). get_offset (a ), Perfect5 );
1129+ assert_eq! (Note :: from_str (" F" ). unwrap (). get_offset (a ), Min6 );
1130+ assert_eq! (Note :: from_str (" F#" ). unwrap (). get_offset (a ), Maj6 );
1131+ assert_eq! (Note :: from_str (" G" ). unwrap (). get_offset (a ), Min7 );
1132+ assert_eq! (Note :: from_str (" G#" ). unwrap (). get_offset (a ), Maj7 );
1133+ }
1134+
1135+ #[test]
1136+ fn test_add_interval_to_note () {
1137+ use Interval :: * ;
1138+ let a = Note :: from_str (" A" ). unwrap ();
1139+ assert_eq! (a + Unison , a );
1140+ assert_eq! (a + Min2 , Note :: from_str (" A#" ). unwrap ());
1141+ assert_eq! (a + Maj2 , Note :: from_str (" B" ). unwrap ());
1142+ assert_eq! (a + Min3 , Note :: from_str (" C" ). unwrap ());
1143+ assert_eq! (a + Maj3 , Note :: from_str (" C#" ). unwrap ());
1144+ assert_eq! (a + Perfect4 , Note :: from_str (" D" ). unwrap ());
1145+ assert_eq! (a + Tritone , Note :: from_str (" D#" ). unwrap ());
1146+ assert_eq! (a + Perfect5 , Note :: from_str (" E" ). unwrap ());
1147+ assert_eq! (a + Min6 , Note :: from_str (" F" ). unwrap ());
1148+ assert_eq! (a + Maj6 , Note :: from_str (" F#" ). unwrap ());
1149+ assert_eq! (a + Min7 , Note :: from_str (" G" ). unwrap ());
1150+ assert_eq! (a + Maj7 , Note :: from_str (" G#" ). unwrap ());
1151+ }
1152+ ```
1153+
1154+ This all works with the logic we've already modelled:
1155+
10981156``` rust
10991157impl From <Interval > for Note {
11001158 // Take an interval from C
@@ -1128,7 +1186,7 @@ impl AddAssign<Interval> for Note {
11281186}
11291187```
11301188
1131- For ` Add<Interval> for Note ` to work, we need to increment a ` Note ` with ` inc() ` :
1189+ For ` Add<Interval> for Note ` to work, we need to add some extra helper methods `:
11321190
11331191``` rust
11341192impl NoteLetter {
@@ -1148,6 +1206,26 @@ impl NoteLetter {
11481206}
11491207
11501208impl Note {
1209+ fn interval_from_c (self ) -> Interval {
1210+ use Accidental :: * ;
1211+ let ret = self . letter. interval_from_c ();
1212+ if let Some (acc ) = self . accidental {
1213+ match acc {
1214+ Flat => return Interval :: from (Semitones :: from (i8 :: from (Semitones :: from (ret )) - 1 )),
1215+ Sharp => return ret + Interval :: Min2 ,
1216+ }
1217+ };
1218+ ret
1219+ }
1220+ fn get_offset_from_interval (self , other : Interval ) -> Interval {
1221+ let self_interval_from_c = self . interval_from_c ();
1222+ self_interval_from_c - other
1223+ }
1224+ fn get_offset (self , other : Self ) -> Interval {
1225+ let self_interval_from_c = self . interval_from_c ();
1226+ let other_interval_from_c = other . interval_from_c ();
1227+ self_interval_from_c - other_interval_from_c
1228+ }
11511229 fn inc (& mut self ) {
11521230 use Accidental :: * ;
11531231 use NoteLetter :: * ;
@@ -1470,6 +1548,18 @@ The natural minor scale, is obtained by starting at A4 and counted up white keys
14701548whole, half, whole, whole, half, whole, whole
14711549```
14721550
1551+ ``` rust
1552+ #[test]
1553+ fn test_a_minor () {
1554+ use Mode :: * ;
1555+ use Scale :: * ;
1556+ assert_eq! (
1557+ & Key :: new (Diatonic (Aeolian ), PianoKey :: from_str (" A4" ). unwrap (), 1 ). to_string (),
1558+ " [ A B C D E F G A ]"
1559+ )
1560+ }
1561+ ```
1562+
14731563It's the same pattern, just starting at a different offset. You can play a corresponding minor scale using only the white keys by simply starting at the sixth note of the C major scale (or incrementing a major sixth), which is A. Try counting it out yourself up from A4.
14741564
14751565There's an absurdly fancy name for each offset:
@@ -1503,7 +1593,7 @@ impl Mode {
15031593}
15041594```
15051595
1506- Let's also hardcode the ScaleLenth :
1596+ Let's also hardcode the scale length :
15071597
15081598``` rust
15091599#[derive(Debug , Clone , Copy , PartialEq )]
@@ -1750,7 +1840,8 @@ Discrete units like `Semitones` are useful for working with a keyboard, but as w
17501840Beyond the twelve 12 semitones in an octave, each semitone is divided into 100 [ cents] ( https://en.wikipedia.org/wiki/Cent_(music) ) . This means a full octave, representing a 2:1 ratio in frequency, spans 1200 cents, and each cent can be divided without losing the ratio as well if needed:
17511841
17521842``` rust
1753- struct Cents (f64 );
1843+ #[derive(Debug , Default , Clone , Copy , PartialEq )]
1844+ pub struct Cents (f64 );
17541845```
17551846
17561847We need to do a little plumbing to let ourselves work at this higher level of abstraction. We need to be able to translate our discrete ` Semitones ` into ` Cents ` ergonomically:
@@ -1766,15 +1857,15 @@ fn test_semitones_to_cents() {
17661857We can give ourselves some conversions to the inner primitive:
17671858
17681859``` rust
1769- impl From <Cents > for f64 {
1770- fn from (cents : Cents ) -> Self {
1771- cents . 0
1860+ impl From <f64 > for Cents {
1861+ fn from (f : f64 ) -> Self {
1862+ Cents ( f )
17721863 }
17731864}
17741865
1775- impl From <Semitones > for i8 {
1776- fn from (semitones : Semitones ) -> Self {
1777- semitones . 0
1866+ impl From <Cents > for f64 {
1867+ fn from (c : Cents ) -> Self {
1868+ c . 0
17781869 }
17791870}
17801871```
@@ -1785,8 +1876,8 @@ Now we can encode the conversion factor:
17851876const SEMITONE_CENTS : Cents = Cents (100.0 );
17861877
17871878impl From <Semitones > for Cents {
1788- fn from (semitones : Semitones ) -> Self {
1789- Cents (i8 :: from (semitones ) as f64 * f64 :: from (SEMITONE_CENTS ))
1879+ fn from (s : Semitones ) -> Self {
1880+ Cents (i8 :: from (s ) as f64 * f64 :: from (SEMITONE_CENTS ))
17901881 }
17911882}
17921883```
@@ -1805,14 +1896,6 @@ fn test_interval_to_cents() {
18051896
18061897We need ` Interval ` variants to map directly to ` Semitones ` instead of plain integers, to make sure they're always turned into ` Cents ` correctly:
18071898
1808- ``` rust
1809- impl From <Interval > for Semitones {
1810- fn from (i : Interval ) -> Self {
1811- Semitones (i as i8 )
1812- }
1813- }
1814- ```
1815-
18161899With that, it's easy to map ` Interval ` s to ` Cents ` :
18171900
18181901``` rust
@@ -1854,7 +1937,7 @@ Lets try to increase the standard pitch by single Hertz using the value above:
18541937fn test_add_cents_to_pitch () {
18551938 let mut pitch = Pitch :: default ();
18561939 pitch += Cents (3.9302 );
1857- assert_eq! (pitch , Pitch :: new (441.0 ));
1940+ assert_eq! (pitch , Pitch :: new (Hertz ( 441.0 ) ));
18581941}
18591942```
18601943
@@ -1880,15 +1963,18 @@ The [`AddAssign`](https://doc.rust-lang.org/std/ops/trait.AddAssign.html) trait
18801963use std :: ops :: AddAssign
18811964
18821965impl AddAssign <Cents > for Pitch {
1966+ #[allow(clippy :: suspicious_op_assign_impl)] // needed to stop clippy from yelling
18831967 fn add_assign(& mut self , rhs: Cents ) {
1884- self . frequency *= 2. 0_ f64 . powf((rhs / Cents :: from(Interval :: Octave )). into())
1968+ self . 0 *= 2. 0 f64 . powf((rhs / Cents :: from(Interval :: Octave )). into())
18851969 }
18861970}
18871971```
18881972
18891973Oops , we also need to `*= ` an `f64 ` to a `Hertz `:
18901974
18911975```rust
1976+ use std :: ops :: MulAssign ;
1977+
18921978impl MulAssign <f64 > for Hertz {
18931979 fn mul_assign (& mut self , rhs : f64 ) {
18941980 self . 0 *= rhs ;
@@ -1902,13 +1988,7 @@ If that's not quite clear, this is the exact equation shown above with a bit of
19021988
19031989Sadly, though, ` cargo test ` tells us we have a problem:
19041990
1905- ``` txt
1906- Diff < left / right > :
1907- Pitch {
1908- < frequency: 441.0000105867894,
1909- > frequency: 441.0,
1910- }
1911- ```
1991+ ![ fail float] ( https://thepracticaldev.s3.amazonaws.com/i/bu70ahx1w5rfln6sa3jq.png )
19121992
19131993Floating point arithmetic is not precise. However, a delta of as much as a whole Hertz, or almost 4 cents, isn't large enough for any human to perceive. The [ just-noticeable difference] ( https://en.wikipedia.org/wiki/Just-noticeable_difference ) is about 5 or 6 cents, or 5* 2^(1/1200). In this type we just care that it's "close enough". At a glance we can look at those results and understand that we got where we need to be. To convince Rust we're good to go, we can override the compiler-derived [ ` PartialEq ` ] ( https://doc.rust-lang.org/std/cmp/trait.PartialEq.html ) behavior for this type:
19141994
@@ -1948,16 +2028,16 @@ fn test_add_semitones_to_pitch() {
19482028 use Interval :: Octave ;
19492029 let mut pitch = Pitch :: default ();
19502030 pitch += Semitones :: from (Octave );
1951- assert_eq! (pitch , Pitch :: new (880.0 ))
2031+ assert_eq! (pitch , Pitch :: new (Hertz ( 880.0 ) ))
19522032}
19532033```
19542034
19552035That's pretty easy with the work we've already done:
19562036
19572037``` rust
19582038impl AddAssign <Semitones > for Pitch {
1959- fn add_assign (& mut self , semitones : Semitones ) {
1960- * self += Cents :: from (semitones )
2039+ fn add_assign (& mut self , rhs : Semitones ) {
2040+ * self += Cents :: from (rhs )
19612041 }
19622042}
19632043```
@@ -1970,16 +2050,16 @@ fn test_add_interval_to_pitch() {
19702050 use Interval :: Min2 ;
19712051 let mut pitch = Pitch :: default ();
19722052 pitch += Min2 ;
1973- assert_eq! (pitch , Pitch :: new (466.1 ))
2053+ assert_eq! (pitch , Pitch :: new (Hertz ( 466.1 ) ))
19742054}
19752055```
19762056
19772057Naturally, this is also trivial:
19782058
19792059``` rust
19802060impl AddAssign <Interval > for Pitch {
1981- fn add_assign (& mut self , i : Interval ) {
1982- * self += Cents :: from (i )
2061+ fn add_assign (& mut self , rhs : Interval ) {
2062+ * self += Cents :: from (rhs )
19832063 }
19842064}
19852065```
@@ -1994,8 +2074,6 @@ pub const C_ZERO: Hertz = Hertz(16.352);
19942074
19952075This is super low - most humans bottom out around 20Hz. The 88-key piano's lowest note is up at A0, a 9-semitone [ ` major sixth ` ] ( https://en.wikipedia.org/wiki/Major_sixth ) higher. Note how even though this is a different abstraction for working with pitches, the frequencies baked in to the standard are still pinned to the A440 scale.
19962076
1997- // TODO go through the rest of FromStr
1998-
19992077We want to be able to convert from piano keys to pitches and have the frequencies work out for both standards:
20002078
20012079``` rust
@@ -2006,6 +2084,24 @@ fn test_piano_key_to_pitch() {
20062084}
20072085```
20082086
2087+ To get there, we can add octaves and smaller intervals up from ` C0 ` to whatever note we need;
2088+
2089+ ``` rust
2090+ impl From <PianoKey > for Pitch {
2091+ fn from (sp : PianoKey ) -> Self {
2092+ use Interval :: * ;
2093+ let mut ret = Pitch :: new (C_ZERO );
2094+ // Add octaves
2095+ for _ in 0 .. sp . octave {
2096+ ret += Octave ;
2097+ }
2098+ // Add note offset
2099+ ret += sp . note. letter. interval_from_c ();
2100+ ret
2101+ }
2102+ }
2103+ ```
2104+
20092105##### Random Notes
20102106
20112107* [ top] ( #table-of-contents ) *
0 commit comments