Skip to content

Commit

Permalink
feat(harmonic_function): add class and Scale method (#176)
Browse files Browse the repository at this point in the history
* feat(harmonic_function): add class and `Scale` method

* refactor(harmonic_function): rename static constants

* test(harmonic_function): add test cases

* docs(harmonic_function): address typo in example
  • Loading branch information
albertms10 committed Jun 11, 2023
1 parent d97b5f6 commit ccb5f57
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/music_notes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ part 'src/chordable.dart';
part 'src/enharmonic.dart';
part 'src/harmony/chord.dart';
part 'src/harmony/chord_pattern.dart';
part 'src/harmony/harmonic_function.dart';
part 'src/interval/enharmonic_interval.dart';
part 'src/interval/interval.dart';
part 'src/interval/interval_size_extension.dart';
Expand Down
46 changes: 46 additions & 0 deletions lib/src/harmony/harmonic_function.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
part of '../../music_notes.dart';

class HarmonicFunction {
final List<ScaleDegree> scaleDegrees;

/// Creates a new [HarmonicFunction] from [scaleDegrees].
const HarmonicFunction(this.scaleDegrees);

static const tonic = HarmonicFunction([ScaleDegree.i]);
static const ii = HarmonicFunction([ScaleDegree.ii]);
static const neapolitanSixth =
HarmonicFunction([ScaleDegree.neapolitanSixth]);
static const iii = HarmonicFunction([ScaleDegree.iii]);
static const iv = HarmonicFunction([ScaleDegree.iv]);
static const dominantV =
HarmonicFunction([ScaleDegree(5, quality: ImperfectQuality.major)]);
static const vi = HarmonicFunction([ScaleDegree.vi]);
static const vii = HarmonicFunction([ScaleDegree.vii]);

@override
String toString() => scaleDegrees.join('/');

/// Returns a new [HarmonicFunction] relating this [HarmonicFunction] to
/// [other].
///
/// Example:
/// ```dart
/// HarmonicFunction.dominantV /
/// HarmonicFunction.dominantV /
/// HarmonicFunction.dominantV
/// == HarmonicFunction([
/// ScaleDegree.v.major, ScaleDegree.v.major, ScaleDegree.v.major,
/// ])
/// ```
HarmonicFunction operator /(HarmonicFunction other) =>
HarmonicFunction([...scaleDegrees, ...other.scaleDegrees]);

@override
bool operator ==(Object other) =>
other is HarmonicFunction &&
const ListEquality<ScaleDegree>()
.equals(scaleDegrees, other.scaleDegrees);

@override
int get hashCode => Object.hashAll(scaleDegrees);
}
22 changes: 22 additions & 0 deletions lib/src/scale/scale.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ class Scale<T extends Scalable<T>> implements Transposable<Scale<T>> {
Chord<T> degreeChord(ScaleDegree scaleDegree) =>
pattern.degreePattern(scaleDegree).on(degree(scaleDegree));

/// Returns the [Chord<T>] for the [harmonicFunction] of this [Scale<T>].
///
/// Example:
/// ```dart
/// Note.g.major.scale.functionChord(ScaleDegree.v / ScaleDegree.v)
/// == Note.a.majorTriad
/// Note.b.flat.minor.scale.functionChord(ScaleDegree.ii / ScaleDegree.v)
/// == Note.g.minorTriad
/// ```
Chord<T> functionChord(HarmonicFunction harmonicFunction) =>
harmonicFunction.scaleDegrees
.skip(1)
.toList()
.reversed
.fold(
this,
(scale, scaleDegree) => ScalePattern.fromChordPattern(
scale.pattern.degreePattern(scaleDegree),
).on(scale.degree(scaleDegree)),
)
.degreeChord(harmonicFunction.scaleDegrees.first);

/// Returns this [Scale<T>] transposed by [interval].
///
/// Example:
Expand Down
94 changes: 94 additions & 0 deletions test/src/harmony/harmonic_function_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'package:music_notes/music_notes.dart';
import 'package:test/test.dart';

void main() {
group('HarmonicFunction', () {
group('operator /', () {
test('should return the HarmonicFunction relating this to other', () {
expect(
HarmonicFunction.dominantV / HarmonicFunction.dominantV,
HarmonicFunction([ScaleDegree.v.major, ScaleDegree.v.major]),
);
expect(
HarmonicFunction.ii / HarmonicFunction.ii,
const HarmonicFunction([ScaleDegree.ii, ScaleDegree.ii]),
);
expect(
HarmonicFunction.vi / HarmonicFunction.iv,
const HarmonicFunction([ScaleDegree.vi, ScaleDegree.iv]),
);
expect(
HarmonicFunction.tonic / HarmonicFunction.ii / HarmonicFunction.iii,
const HarmonicFunction(
[ScaleDegree.i, ScaleDegree.ii, ScaleDegree.iii],
),
);
});
});

group('.toString()', () {
test(
'should return the string representation of this HarmonicFunction',
() {
expect(HarmonicFunction.tonic.toString(), 'I');
expect(HarmonicFunction.vii.toString(), 'VII');
expect(
(HarmonicFunction.dominantV / HarmonicFunction.dominantV)
.toString(),
'V/V',
);
expect(
(HarmonicFunction([ScaleDegree.iv.minor]) /
HarmonicFunction.neapolitanSixth /
HarmonicFunction.dominantV)
.toString(),
'iv/♭II6/V',
);
},
);
});

group('.hashCode', () {
test('should return the same hashCode for equal HarmonicFunctions', () {
expect(
HarmonicFunction.tonic.hashCode,
HarmonicFunction.tonic.hashCode,
);
expect(
HarmonicFunction.neapolitanSixth.hashCode,
HarmonicFunction.neapolitanSixth.hashCode,
);
});

test(
'should return different hashCodes for different HarmonicFunctions',
() {
expect(
HarmonicFunction.tonic.hashCode,
isNot(equals(HarmonicFunction.ii.hashCode)),
);
expect(
const HarmonicFunction([ScaleDegree.vi, ScaleDegree.i]).hashCode,
isNot(equals(HarmonicFunction.vi.hashCode)),
);
},
);

test('should ignore equal HarmonicFunction instances in a Set', () {
final collection = {
HarmonicFunction.tonic,
HarmonicFunction.neapolitanSixth,
HarmonicFunction.iii,
HarmonicFunction.iv / HarmonicFunction.iv,
};
collection.addAll(collection);
expect(collection.toList(), [
HarmonicFunction.tonic,
HarmonicFunction.neapolitanSixth,
HarmonicFunction.iii,
HarmonicFunction.iv / HarmonicFunction.iv,
]);
});
});
});
}
2 changes: 2 additions & 0 deletions test/src/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'harmony/chord_pattern_test.dart' as chord_pattern_test;
import 'harmony/chord_test.dart' as chord_test;
import 'harmony/harmonic_function_test.dart' as harmonic_function_test;
import 'interval/enharmonic_interval_test.dart' as enharmonic_interval_test;
import 'interval/interval_size_extension_test.dart'
as interval_size_extension_test;
Expand All @@ -22,6 +23,7 @@ import 'tonality/tonality_test.dart' as tonality_test;
void main() {
chord_pattern_test.main();
chord_test.main();
harmonic_function_test.main();
enharmonic_interval_test.main();
interval_size_extension_test.main();
interval_test.main();
Expand Down
71 changes: 71 additions & 0 deletions test/src/scale/scale_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,77 @@ void main() {
});
});

group('.functionChord()', () {
test(
'should return the Chord for the HarmonicFunction of this Scale',
() {
expect(
Note.c.major.scale.functionChord(HarmonicFunction.tonic),
Note.c.majorTriad,
);
expect(
Note.d.major.scale.functionChord(HarmonicFunction.vii),
Note.c.sharp.diminishedTriad,
);

expect(
Note.g.major.scale.functionChord(
HarmonicFunction.dominantV / HarmonicFunction.dominantV,
),
Note.a.majorTriad,
);
expect(
Note.f.major.scale
.functionChord(HarmonicFunction.iv / HarmonicFunction.vi),
Note.g.minorTriad,
);
expect(
Note.b.flat.major.scale
.functionChord(HarmonicFunction.vi / HarmonicFunction.iv),
Note.c.minorTriad,
);
expect(
Note.c.sharp.minor.scale.functionChord(
HarmonicFunction.ii / HarmonicFunction.dominantV,
),
Note.a.sharp.minorTriad,
);

expect(
Note.d.flat.major.scale.functionChord(
HarmonicFunction.ii /
HarmonicFunction.vi /
HarmonicFunction.dominantV,
),
Note.g.diminishedTriad,
);
expect(
Note.b.major.scale.functionChord(
HarmonicFunction.iv / HarmonicFunction.iv / HarmonicFunction.iv,
),
Note.d.majorTriad,
);
expect(
Note.a.major.scale.functionChord(
HarmonicFunction.dominantV /
HarmonicFunction.dominantV /
HarmonicFunction.dominantV,
),
Note.f.sharp.majorTriad,
);
expect(
Note.e.flat.major.scale.functionChord(
HarmonicFunction.dominantV /
HarmonicFunction.dominantV /
HarmonicFunction.dominantV /
HarmonicFunction.dominantV,
),
Note.g.majorTriad,
);
},
);
});

group('.transposeBy()', () {
test('should return this Scale transposed by Interval', () {
expect(
Expand Down

0 comments on commit ccb5f57

Please sign in to comment.