Skip to content

Commit

Permalink
refactor: PositionedNote composition with Note instead of inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
plammens committed May 13, 2023
1 parent 9115542 commit b217c64
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 37 deletions.
5 changes: 2 additions & 3 deletions lib/src/note/note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ final class Note implements MusicItem, Transposable<Note> {
/// Note.eFlat.difference(Note.bFlat) == 7
/// Note.a.difference(Note.g) == -2
/// ```
int difference(covariant Note other) => other.semitones - semitones;
int difference(Note other) => other.semitones - semitones;

/// Returns this [Note] positioned in the given [octave] as [PositionedNote].
///
Expand All @@ -80,8 +80,7 @@ final class Note implements MusicItem, Transposable<Note> {
/// Note.aFlat.inOctave(2)
/// == const PositionedNote(Notes.a, Accidental.flat, 2);
/// ```
PositionedNote inOctave(int octave) =>
PositionedNote(note, accidental, octave);
PositionedNote inOctave(int octave) => PositionedNote(this, octave);

/// Returns the distance in relation to the circle of fifths.
///
Expand Down
43 changes: 22 additions & 21 deletions lib/src/note/positioned_note.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
part of '../../music_notes.dart';

/// A note octave in the octave range.
final class PositionedNote extends Note {
/// The octave where this [PositionedNote] is.
final class PositionedNote implements MusicItem, Transposable<PositionedNote> {
/// Which of the 12 notes inside the octave.
final Note note;

/// The octave where the [note] is positioned.
final int octave;

/// Creates a new [PositionedNote] from [Note] arguments and [octave].
const PositionedNote(super.note, [super.accidental, this.octave = 4]);
const PositionedNote(this.note, [this.octave = 4]);

/// Returns the [octave] that corresponds to the semitones from root height.
///
Expand All @@ -29,7 +32,8 @@ final class PositionedNote extends Note {
/// Note.a.inOctave(2).semitonesFromRootHeight == 34
/// Note.c.inOctave(0).semitonesFromRootHeight == 1
/// ```
int get semitonesFromRootHeight => semitones + octave * chromaticDivisions;
@override
int get semitones => note.semitones + octave * chromaticDivisions;

/// Returns the difference in semitones between this [PositionedNote] and
/// [other].
Expand All @@ -40,9 +44,7 @@ final class PositionedNote extends Note {
/// Note.eFlat.inOctave(4).difference(Note.bFlat.inOctave(4)) == 7
/// Note.a.inOctave(4).difference(Note.g.inOctave(4)) == -2
/// ```
@override
int difference(covariant PositionedNote other) =>
other.semitonesFromRootHeight - semitonesFromRootHeight;
int difference(PositionedNote other) => other.semitones - semitones;

/// Returns a transposed [PositionedNote] by [interval]
/// from this [PositionedNote].
Expand All @@ -56,16 +58,16 @@ final class PositionedNote extends Note {
/// ```
@override
PositionedNote transposeBy(Interval interval) {
final transposedNote = super.transposeBy(interval);
final transposedNote = note.transposeBy(interval);

return transposedNote.inOctave(
octaveFromSemitones(
semitonesFromRootHeight +
semitones +
interval.semitones -
// We don't want to take the accidental into account when
// calculating the octave height, as it depends on the note name.
// This correctly handles cases with the same number of accidentals
// but different octaves (e.g., C♭4 but B3).
// We don't want to take the accidental into account when
// calculating the octave height, as it depends on the note name.
// This correctly handles cases with the same number of accidentals
// but different octaves (e.g., C♭4 but B3).
transposedNote.accidental.semitones,
),
);
Expand Down Expand Up @@ -110,8 +112,8 @@ final class PositionedNote extends Note {
/// Note.a.inOctave(3).scientificName == 'A3'
/// Note.bFlat.inOctave(1).scientificName == 'B♭1'
/// ```
String get scientificName => '${note.name.toUpperCase()}'
'${accidental != Accidental.natural ? accidental.symbol : ''}'
String get scientificName => '${note.note.name.toUpperCase()}'
'${note.accidental != Accidental.natural ? note.accidental.symbol : ''}'
'$octave';

/// Returns the string representation of this [Note] following
Expand All @@ -125,17 +127,17 @@ final class PositionedNote extends Note {
/// ```
String get helmholtzName {
final accidentalSymbol =
accidental != Accidental.natural ? accidental.symbol : '';
note.accidental != Accidental.natural ? note.accidental.symbol : '';

if (octave >= 3) {
const superPrime = '′';

return '${note.name}$accidentalSymbol${superPrime * (octave - 3)}';
return '${note.note.name}$accidentalSymbol${superPrime * (octave - 3)}';
}

const subPrime = '͵';

return '${note.name.toUpperCase()}$accidentalSymbol'
return '${note.note.name.toUpperCase()}$accidentalSymbol'
'${subPrime * (octave - 2).abs()}';
}

Expand All @@ -144,15 +146,14 @@ final class PositionedNote extends Note {

@override
bool operator ==(Object other) =>
super == other && other is PositionedNote && octave == other.octave;
other is PositionedNote && note == other.note && octave == other.octave;

@override
int get hashCode => Object.hash(super.hashCode, octave);

@override
int compareTo(covariant PositionedNote other) => compareMultiple([
() => octave.compareTo(other.octave),
() => semitones.compareTo(other.semitones),
() => note.value.compareTo(other.note.value),
() => note.compareTo(other.note),
]);
}
27 changes: 14 additions & 13 deletions test/src/note/positioned_note_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ void main() {
'should return the semitones from the root height of this '
'PositionedNote',
() {
expect(Note.c.inOctave(-4).semitonesFromRootHeight, -47);
expect(Note.a.inOctave(-4).semitonesFromRootHeight, -38);
expect(Note.c.inOctave(-3).semitonesFromRootHeight, -35);
expect(Note.c.inOctave(-2).semitonesFromRootHeight, -23);
expect(Note.c.inOctave(-1).semitonesFromRootHeight, -11);
expect(Note.c.inOctave(0).semitonesFromRootHeight, 1);
expect(Note.c.inOctave(1).semitonesFromRootHeight, 13);
expect(Note.c.inOctave(2).semitonesFromRootHeight, 25);
expect(Note.a.inOctave(2).semitonesFromRootHeight, 34);
expect(Note.c.inOctave(3).semitonesFromRootHeight, 37);
expect(Note.a.inOctave(4).semitonesFromRootHeight, 58);
expect(Note.c.inOctave(-4).semitones, -47);
expect(Note.a.inOctave(-4).semitones, -38);
expect(Note.c.inOctave(-3).semitones, -35);
expect(Note.c.inOctave(-2).semitones, -23);
expect(Note.c.inOctave(-1).semitones, -11);
expect(Note.c.inOctave(0).semitones, 1);
expect(Note.c.inOctave(1).semitones, 13);
expect(Note.c.inOctave(2).semitones, 25);
expect(Note.a.inOctave(2).semitones, 34);
expect(Note.c.inOctave(3).semitones, 37);
expect(Note.a.inOctave(4).semitones, 58);
},
);
});
Expand All @@ -52,20 +52,21 @@ void main() {
expect(Note.c.inOctave(4).difference(Note.c.inOctave(4)), 0);
expect(
const Note(Notes.e, Accidental.sharp)
.inOctave(4)
.difference(Note.f.inOctave(4)),
0,
);
expect(Note.c.inOctave(4).difference(Note.dFlat.inOctave(4)), 1);
expect(Note.c.inOctave(4).difference(Note.cSharp.inOctave(4)), 1);
expect(
Note.b.inOctave(4).difference(Note.c.inOctave(4).inOctave(5)),
Note.b.inOctave(4).difference(Note.c.inOctave(5)),
1,
);
expect(Note.f.inOctave(4).difference(Note.g.inOctave(4)), 2);
expect(Note.f.inOctave(4).difference(Note.aFlat.inOctave(4)), 3);
expect(Note.e.inOctave(4).difference(Note.aFlat.inOctave(4)), 4);
expect(
Note.a.inOctave(4).difference(Note.d.inOctave(4).inOctave(5)),
Note.a.inOctave(4).difference(Note.d.inOctave(5)),
5,
);
expect(Note.d.inOctave(4).difference(Note.aFlat.inOctave(4)), 6);
Expand Down

0 comments on commit b217c64

Please sign in to comment.