Skip to content

Commit

Permalink
feat(harmony): add Chord and ChordPattern classes (#154)
Browse files Browse the repository at this point in the history
* feat(harmony): add `Chord` and `ChordPattern` classes

* chore(vscode): ignore `sublist` cSpell word

* feat(chord): override `toString` method and document `pattern` getter

* feat(chord_pattern): add missing methods and override `toString`

* feat(interval): add common compound intervals

* test(harmony): add more test cases for new members

* refactor(interval): use static constants where possible

* test(harmony): add test cases for `hashCode`

* test(harmony): add tests for missing methods
  • Loading branch information
albertms10 authored May 26, 2023
1 parent 4bb96ec commit 50535fa
Show file tree
Hide file tree
Showing 10 changed files with 697 additions and 15 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"lydian",
"mixolydian",
"octatonic",
"phrygian"
"phrygian",
"sublist"
],
"editor.unicodeHighlight.allowedCharacters": {
"′": true
Expand Down
2 changes: 2 additions & 0 deletions lib/music_notes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'utils/iterable.dart';
import 'utils/num_extension.dart';

part 'src/enharmonic.dart';
part 'src/harmony/chord.dart';
part 'src/harmony/chord_pattern.dart';
part 'src/interval/enharmonic_interval.dart';
part 'src/interval/interval.dart';
part 'src/interval/interval_size_extension.dart';
Expand Down
35 changes: 35 additions & 0 deletions lib/src/harmony/chord.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of '../../music_notes.dart';

class Chord<T extends Scalable<T>> {
/// The [Scalable<T>] items this [Chord] is built of.
final List<T> items;

/// Creates a new [Chord] from [items].
const Chord(this.items);

/// The root [Scalable<T>] of this [Chord].
T get root => items.first;

/// Returns the [ChordPattern] for this [Chord].
///
/// Example:
/// ```dart
/// const Chord([Note.a, Note.c, Note.e]).pattern == ChordPattern.minorTriad
/// const Chord([Note.g, Note.b, Note.d, Note.f]).pattern
/// == ChordPattern.majorTriad.add7()
/// ```
ChordPattern get pattern => ChordPattern([
// We skip the root of the chord.
for (final scalable in items.skip(1)) root.interval(scalable),
]);

@override
String toString() => '$root ${pattern.abbreviation} (${items.join(' ')})';

@override
bool operator ==(Object other) =>
other is Chord<T> && ListEquality<T>().equals(items, other.items);

@override
int get hashCode => Object.hashAll(items);
}
130 changes: 130 additions & 0 deletions lib/src/harmony/chord_pattern.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
part of '../../music_notes.dart';

class ChordPattern {
/// The intervals from the root note.
final List<Interval> intervals;

/// Creates a new [ChordPattern] from [intervals].
const ChordPattern(this.intervals);

static const augmentedTriad = ChordPattern([
Interval.majorThird,
Interval.augmentedFifth,
]);

static const majorTriad = ChordPattern([
Interval.majorThird,
Interval.perfectFifth,
]);

static const minorTriad = ChordPattern([
Interval.minorThird,
Interval.perfectFifth,
]);

static const diminishedTriad = ChordPattern([
Interval.minorThird,
Interval.diminishedFifth,
]);

/// Returns the [Chord<T>] from [scalable].
///
/// Example:
/// ```dart
/// ChordPattern.majorTriad.from(Note.c)
/// == const Chord([Note.c, Note.e, Note.g])
/// ```
Chord<T> from<T extends Scalable<T>>(T scalable) => Chord(
intervals.fold(
[scalable],
(chordItems, interval) =>
[...chordItems, scalable.transposeBy(interval)],
),
);

/// Returns the root triad of this [ChordPattern].
///
/// Example:
/// ```dart
/// ChordPattern.majorTriad.add7().add9().rootTriad == ChordPattern.majorTriad
/// ```
ChordPattern get rootTriad => ChordPattern(intervals.sublist(0, 2));

/// Whether this [ChordPattern] is [ImperfectQuality.augmented].
bool get isAugmented => rootTriad == augmentedTriad;

/// Whether this [ChordPattern] is [ImperfectQuality.major].
bool get isMajor => rootTriad == majorTriad;

/// Whether this [ChordPattern] is [ImperfectQuality.minor].
bool get isMinor => rootTriad == minorTriad;

/// Whether this [ChordPattern] is [ImperfectQuality.diminished].
bool get isDiminished => rootTriad == diminishedTriad;

/// Returns a new [ChordPattern] with a suspended [Interval.majorSecond].
ChordPattern sus2() => add(Interval.majorSecond, replaceSizes: const [3, 4]);

/// Returns a new [ChordPattern] with a suspended [Interval.perfectFourth].
ChordPattern sus4() =>
add(Interval.perfectFourth, replaceSizes: const [2, 3]);

/// Returns a new [ChordPattern] adding a [quality] 6th.
ChordPattern add6([ImperfectQuality quality = ImperfectQuality.major]) =>
add(Interval.imperfect(6, quality));

/// Returns a new [ChordPattern] adding a [quality] 7th.
ChordPattern add7([ImperfectQuality quality = ImperfectQuality.minor]) =>
add(Interval.imperfect(7, quality));

/// Returns a new [ChordPattern] adding a [quality] 9th.
ChordPattern add9([ImperfectQuality quality = ImperfectQuality.major]) =>
add(Interval.imperfect(9, quality));

/// Returns a new [ChordPattern] adding an [quality] 11th.
ChordPattern add11([PerfectQuality quality = PerfectQuality.perfect]) =>
add(Interval.perfect(11, quality));

/// Returns a new [ChordPattern] adding a [quality] 13th.
ChordPattern add13([ImperfectQuality quality = ImperfectQuality.major]) =>
add(Interval.imperfect(13, quality));

/// Returns a new [ChordPattern] adding [interval].
ChordPattern add(Interval interval, {List<int>? replaceSizes}) {
final sizesToReplace = [interval.size, ...?replaceSizes];
final filteredIntervals = intervals.whereNot(
(chordInterval) => sizesToReplace.contains(chordInterval.size),
);

return ChordPattern(
// Keep the intervals sorted after these operations.
SplayTreeSet.of([...filteredIntervals, interval]).toList(),
);
}

/// Returns the abbreviated quality representing this [ChordPattern].
///
/// Example:
/// ```dart
/// ChordPattern.majorTriad.abbreviation == 'maj.'
/// ChordPattern.diminishedTriad.abbreviation == 'dim.'
/// ```
String get abbreviation => switch (this) {
_ when isAugmented => 'aug.',
_ when isMajor => 'maj.',
_ when isMinor => 'min.',
_ when isDiminished => 'dim.',
_ => '?',
};

@override
String toString() => '$abbreviation (${intervals.join(' ')})';

@override
bool operator ==(Object other) =>
other is ChordPattern &&
const ListEquality<Interval>().equals(intervals, other.intervals);

@override
int get hashCode => Object.hashAll(intervals);
}
12 changes: 12 additions & 0 deletions lib/src/interval/interval.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ final class Interval implements Comparable<Interval> {
static const perfectOctave = Interval.perfect(8, PerfectQuality.perfect);
static const augmentedOctave = Interval.perfect(8, PerfectQuality.augmented);

static const minorNinth = Interval.imperfect(9, ImperfectQuality.minor);
static const majorNinth = Interval.imperfect(9, ImperfectQuality.major);

static const diminishedEleventh =
Interval.perfect(11, PerfectQuality.diminished);
static const perfectEleventh = Interval.perfect(11, PerfectQuality.perfect);
static const augmentedEleventh =
Interval.perfect(11, PerfectQuality.augmented);

static const minorThirteenth = Interval.imperfect(13, ImperfectQuality.minor);
static const majorThirteenth = Interval.imperfect(13, ImperfectQuality.major);

/// Creates a new [Interval] allowing only perfect quality [size]s.
const Interval.perfect(this.size, PerfectQuality this.quality)
: assert(size != 0, 'Size must be non-zero'),
Expand Down
Loading

0 comments on commit 50535fa

Please sign in to comment.