Skip to content

Commit

Permalink
refactor(frequency): ♻️ rewrite ClosestPitch record into a class (#339
Browse files Browse the repository at this point in the history
)

* refactor(frequency): ♻️ rewrite `ClosestPitch` record into a class

* docs(frequency): 📖 update `harmonic` documentation example
  • Loading branch information
albertms10 committed Jan 3, 2024
1 parent bccd9ef commit fd6a79c
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 82 deletions.
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
17 changes: 6 additions & 11 deletions example/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
82 changes: 59 additions & 23 deletions lib/src/note/frequency.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ part of '../../music_notes.dart';
/// ---
/// See also:
/// * [Pitch].
/// * [ClosestPitch].
@immutable
class Frequency implements Comparable<Frequency> {
/// The value of this [Frequency] in Hertz.
Expand All @@ -31,17 +32,16 @@ class Frequency implements Comparable<Frequency> {
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
Expand Down Expand Up @@ -76,7 +76,7 @@ class Frequency implements Comparable<Frequency> {
closestPitch.note.accidental == Accidental.sharp &&
!hertzDelta.isNegative;

return (
return ClosestPitch(
isCloserToUpwardsSpelling ? closestPitch.respelledUpwards : closestPitch,
cents: Ratio(hertz / closestPitchFrequency.hertz).cents,
hertz: hertzDelta,
Expand All @@ -91,8 +91,8 @@ class Frequency implements Comparable<Frequency> {
/// 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);
Expand All @@ -108,11 +108,13 @@ class Frequency implements Comparable<Frequency> {
/// 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<Frequency> harmonics({required int upToIndex}) => {
for (var i = 0; i <= upToIndex.abs(); i++) harmonic(i * upToIndex.sign),
};
Expand Down Expand Up @@ -163,24 +165,58 @@ class Frequency implements Comparable<Frequency> {
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<Frequency> {
/// 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<ClosestPitch> get closestPitches =>
map((frequency) => frequency.closestPitch()).toSet();
}
114 changes: 77 additions & 37 deletions test/src/note/frequency_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,27 @@ 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,
),
);
expect(
const Frequency(467).closestPitch(),
(
ClosestPitch(
Note.b.flat.inOctave(4),
cents: const Cent(3.1028314220028586),
hertz: 0.8362384819100726,
),
);
expect(
const Frequency(256).closestPitch(),
(
ClosestPitch(
Note.c.inOctave(4),
cents: const Cent(-37.63165622959142),
hertz: -5.625565300598623,
Expand All @@ -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,
Expand All @@ -67,15 +67,15 @@ 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(
referenceFrequency: const Frequency(512),
tuningSystem:
EqualTemperament.edo12(referencePitch: Note.c.inOctave(5)),
),
(
ClosestPitch(
Note.a.inOctave(4),
cents: const Cent(37.63165622959145),
hertz: 9.461035390098175,
Expand Down Expand Up @@ -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),
]);
});
});
});
}

0 comments on commit fd6a79c

Please sign in to comment.