diff --git a/README.md b/README.md index 472f863b..806f3a82 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Note.c.major.scale.functionChord( ### Chords -Create a `Chord` from a series of `Note`s or from a `ChordPattern`: +Create a `Chord` from a series of `Note`s or a `ChordPattern`: ```dart Chord([Note.a, Note.c.sharp, Note.e]); // A maj. (A C♯ E) @@ -224,21 +224,16 @@ Note.b.flat.inOctave(4).frequency( Get the closest note from a given `Frequency`: ```dart -const Frequency(415).closestPitch(); -// (G♯4, cents: -1.2706247484469828 ¢, hertz: -0.3046975799451275) +const Frequency(432).closestPitch(); // A4-32 +const Frequency(314).closestPitch(); // E♭4+16 ``` And combining both methods, the harmonic series of a given `Pitch`: ```dart -Note.c - .inOctave(1) - .frequency() - .harmonics(upToIndex: 15) - .map((frequency) => frequency.closestPitch().displayString()) - .toSet(); -// {C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4, D4+4, E4-14, F♯4-49, G4+2, -// A♭4+41, A♯4-31, B4-12, C5} +Note.c.inOctave(1).frequency().harmonics(upToIndex: 15).closestPitches; +// {C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4, +// D4+4, E4-14, F♯4-49, G4+2, A♭4+41, A♯4-31, B4-12, C5} ``` ### In a nutshell diff --git a/example/main.dart b/example/main.dart index 5270ab7c..87253092 100644 --- a/example/main.dart +++ b/example/main.dart @@ -99,17 +99,12 @@ void main() { EqualTemperament.edo12(referencePitch: Note.c.inOctave(4)), ); // 456.1401436878537 Hz - const Frequency(415).closestPitch(); - // (G♯4, cents: -1.2706247484469828 ¢, hertz: -0.3046975799451275) - - Note.c - .inOctave(1) - .frequency() - .harmonics(upToIndex: 15) - .map((frequency) => frequency.closestPitch().displayString()) - .toSet(); - // {C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4, D4+4, E4-14, F♯4-49, G4+2, - // A♭4+41, A♯4-31, B4-12, C5} + const Frequency(432).closestPitch(); // A4-32 + const Frequency(314).closestPitch(); // E♭4+16 + + Note.c.inOctave(1).frequency().harmonics(upToIndex: 15).closestPitches; + // {C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4, + // D4+4, E4-14, F♯4-49, G4+2, A♭4+41, A♯4-31, B4-12, C5} // In a nutshell ScalePattern.lydian // Lydian (M2 M2 M2 m2 M2 M2 m2) diff --git a/lib/src/note/frequency.dart b/lib/src/note/frequency.dart index e638f573..73d9881e 100644 --- a/lib/src/note/frequency.dart +++ b/lib/src/note/frequency.dart @@ -5,6 +5,7 @@ part of '../../music_notes.dart'; /// --- /// See also: /// * [Pitch]. +/// * [ClosestPitch]. @immutable class Frequency implements Comparable { /// The value of this [Frequency] in Hertz. @@ -31,17 +32,16 @@ class Frequency implements Comparable { return hertz >= minFrequency && hertz <= maxFrequency; } - /// Returns the closest [Pitch] to this [Frequency] from - /// [referenceFrequency] and [tuningSystem], with the difference in `cents` - /// and `hertz`. + /// Returns the [ClosestPitch] to this [Frequency] from [referenceFrequency] + /// and [tuningSystem]. /// /// Example: /// ```dart /// const Frequency(467).closestPitch() - /// == (Note.a.sharp.inOctave(4), cents: const Cent(3.1028), hertz: 0.8362) + /// == ClosestPitch(Note.a.sharp.inOctave(4), cents: Cent(3.1), hertz: 0.84) /// /// const Frequency(260).closestPitch() - /// == (Note.c.inOctave(4), cents: const Cent(-10.7903), hertz: -1.6256) + /// == ClosestPitch(Note.c.inOctave(4), cents: Cent(-10.79), hertz: -1.63) /// ``` /// /// This method and [Pitch.frequency] are inverses of each other for a @@ -76,7 +76,7 @@ class Frequency implements Comparable { closestPitch.note.accidental == Accidental.sharp && !hertzDelta.isNegative; - return ( + return ClosestPitch( isCloserToUpwardsSpelling ? closestPitch.respelledUpwards : closestPitch, cents: Ratio(hertz / closestPitchFrequency.hertz).cents, hertz: hertzDelta, @@ -91,8 +91,8 @@ class Frequency implements Comparable { /// const Frequency(220).harmonic(1) == const Frequency(440) /// const Frequency(880).harmonic(-3) == const Frequency(220) /// - /// Note.c.inOctave(1).frequency().harmonic(3) - /// .closestPitch().displayString() == 'E3-14' + /// Note.c.inOctave(1).frequency().harmonic(3).closestPitch().toString() + /// == 'E3-14' /// ``` Frequency harmonic(int index) => index.isNegative ? this / (index.abs() + 1) : this * (index + 1); @@ -108,11 +108,13 @@ class Frequency implements Comparable { /// Note.a.inOctave(5).frequency().harmonics(upToIndex: -2) /// == {const Frequency(880), const Frequency(440), const Frequency(293.33)} /// - /// Note.c.inOctave(1).frequency().harmonics(upToIndex: 7) - /// .map((frequency) => frequency.closestPitch().displayString()) - /// .toSet() - /// == const {'C1', 'C2', 'G2+2', 'C3', 'E3-14', 'G3+2', 'A♯3-31', 'C4'} + /// Note.c.inOctave(1).frequency().harmonics(upToIndex: 7).closestPitches + /// .toString() == '{C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4}' /// ``` + /// + /// --- + /// See also: + /// - [FrequencyIterableExtension.closestPitches]. Set harmonics({required int upToIndex}) => { for (var i = 0; i <= upToIndex.abs(); i++) harmonic(i * upToIndex.sign), }; @@ -163,24 +165,58 @@ class Frequency implements Comparable { int compareTo(Frequency other) => hertz.compareTo(other.hertz); } -/// A record containing the closest [Pitch], with delta `cents` and `hertz`. -typedef ClosestPitch = (Pitch closestPitch, {Cent cents, double hertz}); +/// An abstraction of the closest representation of a [Frequency] as a [Pitch]. +@immutable +class ClosestPitch { + /// The pitch closest to the original [Frequency]. + final Pitch pitch; + + /// The difference in cents. + final Cent cents; + + /// The difference in hertz. + final double hertz; + + /// Creates a new [ClosestPitch] from [pitch], [cents] and [hertz]. + const ClosestPitch(this.pitch, {this.cents = const Cent(0), this.hertz = 0}); -/// A [ClosestPitch] extension. -extension ClosestPitchExtension on ClosestPitch { /// Returns the string representation of this [ClosestPitch] record. /// /// Example: /// ```dart - /// const Frequency(440).closestPitch().displayString() == 'A4' - /// const Frequency(98.1).closestPitch().displayString() == 'G2+2' - /// const Frequency(163.5).closestPitch().displayString() == 'E3-14' - /// const Frequency(228.9).closestPitch().displayString() == 'A♯3-31' + /// const Frequency(440).closestPitch().toString() == 'A4' + /// const Frequency(98.1).closestPitch().toString() == 'G2+2' + /// const Frequency(163.5).closestPitch().toString() == 'E3-14' + /// const Frequency(228.9).closestPitch().toString() == 'A♯3-31' /// ``` - String displayString() { + @override + String toString() { final roundedCents = cents.value.round(); - if (roundedCents == 0) return '${$1}'; + if (roundedCents == 0) return '$pitch'; - return '${$1}${roundedCents.toDeltaString()}'; + return '$pitch${roundedCents.toDeltaString()}'; } + + @override + bool operator ==(Object other) => + other is ClosestPitch && + pitch == other.pitch && + cents == other.cents && + hertz == other.hertz; + + @override + int get hashCode => Object.hash(pitch, cents, hertz); +} + +/// A [Frequency] Iterable extension. +extension FrequencyIterableExtension on Iterable { + /// Returns the set of [ClosestPitch] for each [Frequency] element. + /// + /// Example: + /// ```dart + /// Note.c.inOctave(1).frequency().harmonics(upToIndex: 7).closestPitches + /// .toString() == '{C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4}' + /// ``` + Set get closestPitches => + map((frequency) => frequency.closestPitch()).toSet(); } diff --git a/test/src/note/frequency_test.dart b/test/src/note/frequency_test.dart index 7823c126..2b43948e 100644 --- a/test/src/note/frequency_test.dart +++ b/test/src/note/frequency_test.dart @@ -25,11 +25,11 @@ void main() { test('should return the closest Pitch to this Frequency', () { expect( const Frequency(440).closestPitch(), - (Note.a.inOctave(4), cents: const Cent(0), hertz: 0.0), + ClosestPitch(Note.a.inOctave(4)), ); expect( const Frequency(455).closestPitch(), - ( + ClosestPitch( Note.a.sharp.inOctave(4), cents: const Cent(-41.96437412632116), hertz: -11.163761518089927, @@ -37,7 +37,7 @@ void main() { ); expect( const Frequency(467).closestPitch(), - ( + ClosestPitch( Note.b.flat.inOctave(4), cents: const Cent(3.1028314220028586), hertz: 0.8362384819100726, @@ -45,7 +45,7 @@ void main() { ); expect( const Frequency(256).closestPitch(), - ( + ClosestPitch( Note.c.inOctave(4), cents: const Cent(-37.63165622959142), hertz: -5.625565300598623, @@ -55,7 +55,7 @@ void main() { expect( const Frequency(440) .closestPitch(referenceFrequency: const Frequency(415)), - ( + ClosestPitch( Note.b.flat.inOctave(4), cents: const Cent(1.270624748447127), hertz: 0.32281584089247417, @@ -67,7 +67,7 @@ void main() { tuningSystem: EqualTemperament.edo12(referencePitch: Note.c.inOctave(5)), ), - (Note.c.inOctave(5), cents: const Cent(0), hertz: 0.0), + ClosestPitch(Note.c.inOctave(5)), ); expect( const Frequency(440).closestPitch( @@ -75,7 +75,7 @@ void main() { tuningSystem: EqualTemperament.edo12(referencePitch: Note.c.inOctave(5)), ), - ( + ClosestPitch( Note.a.inOctave(4), cents: const Cent(37.63165622959145), hertz: 9.461035390098175, @@ -249,41 +249,81 @@ void main() { }); }); - group('ClosestPitchExtension', () { - group('.displayString()', () { + group('ClosestPitch', () { + group('.toString()', () { + test('should return the string representation of this ClosestPitch', () { + expect( + Note.c + .inOctave(1) + .frequency() + .harmonics(upToIndex: 15) + .closestPitches + .toString(), + '{C1, C2, G2+2, C3, E3-14, G3+2, A♯3-31, C4, ' + 'D4+4, E4-14, F♯4-49, G4+2, A♭4+41, A♯4-31, B4-12, C5}', + ); + }); + }); + + group('.hashCode', () { + test('should return the same hashCode for equal ClosestPitches', () { + expect( + ClosestPitch(Note.a.inOctave(4)), + ClosestPitch(Note.a.inOctave(4)), + ); + expect( + ClosestPitch( + Note.c.sharp.inOctave(3), + cents: const Cent(-2.123), + hertz: -2.021, + ), + ClosestPitch( + Note.c.sharp.inOctave(3), + cents: const Cent(-2.123), + hertz: -2.021, + ), + ); + }); + test( - 'should return the string representation of this ' - 'ClosestPitch', + 'should return different hashCodes for different ClosestPitches', () { expect( - Note.c - .inOctave(1) - .frequency() - .harmonics(upToIndex: 15) - .map( - (frequency) => frequency.closestPitch().displayString(), - ) - .toSet(), - const { - 'C1', - 'C2', - 'G2+2', - 'C3', - 'E3-14', - 'G3+2', - 'A♯3-31', - 'C4', - 'D4+4', - 'E4-14', - 'F♯4-49', - 'G4+2', - 'A♭4+41', - 'A♯4-31', - 'B4-12', - 'C5', - }); + ClosestPitch(Note.a.inOctave(4)), + isNot(equals(ClosestPitch(Note.g.inOctave(4)))), + ); + expect( + ClosestPitch( + Note.c.sharp.inOctave(3), + cents: const Cent(-2.123), + hertz: -2.021, + ), + isNot( + equals( + ClosestPitch( + Note.c.sharp.inOctave(3), + cents: const Cent(-0.345), + hertz: -4.989, + ), + ), + ), + ); }, ); + + test('should ignore equal ClosestPitch instances in a Set', () { + final collection = { + ClosestPitch(Note.a.inOctave(4)), + ClosestPitch(Note.b.flat.inOctave(3)), + ClosestPitch(Note.c.inOctave(3), cents: const Cent(2), hertz: 2), + }; + collection.addAll(collection); + expect(collection.toList(), [ + ClosestPitch(Note.a.inOctave(4)), + ClosestPitch(Note.b.flat.inOctave(3)), + ClosestPitch(Note.c.inOctave(3), cents: const Cent(2), hertz: 2), + ]); + }); }); }); }