Skip to content

Commit 74a96b8

Browse files
committed
Fix code through Cents
1 parent db168ae commit 74a96b8

File tree

2 files changed

+132
-40
lines changed

2 files changed

+132
-40
lines changed

post.md

Lines changed: 132 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,64 @@ impl AddAssign for Interval {
10951095

10961096
We 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
10991157
impl 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
11341192
impl NoteLetter {
@@ -1148,6 +1206,26 @@ impl NoteLetter {
11481206
}
11491207

11501208
impl 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
14701548
whole, 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+
14731563
It'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

14751565
There'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
17501840
Beyond 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

17561847
We 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() {
17661857
We 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:
17851876
const SEMITONE_CENTS: Cents = Cents(100.0);
17861877

17871878
impl 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

18061897
We 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-
18161899
With 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:
18541937
fn 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
18801963
use std::ops::AddAssign
18811964

18821965
impl 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.0f64.powf((rhs / Cents::from(Interval::Octave)).into())
18851969
}
18861970
}
18871971
```
18881972

18891973
Oops, we also need to `*=` an `f64` to a `Hertz`:
18901974

18911975
```rust
1976+
use std::ops::MulAssign;
1977+
18921978
impl 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

19031989
Sadly, 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

19131993
Floating 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

19552035
That's pretty easy with the work we've already done:
19562036

19572037
```rust
19582038
impl 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

19772057
Naturally, this is also trivial:
19782058

19792059
```rust
19802060
impl 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

19952075
This 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-
19992077
We 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)*

src/lib.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,6 @@ impl Note {
222222
};
223223
ret
224224
}
225-
fn get_offset_from_interval(self, other: Interval) -> Interval {
226-
let self_interval_from_c = self.interval_from_c();
227-
self_interval_from_c - other
228-
}
229225
fn get_offset(self, other: Self) -> Interval {
230226
let self_interval_from_c = self.interval_from_c();
231227
let other_interval_from_c = other.interval_from_c();

0 commit comments

Comments
 (0)