Skip to content

Commit

Permalink
Merge pull request #31 from kilmc/add-extended-chord-support
Browse files Browse the repository at this point in the history
[feat] Add support for extended chords
  • Loading branch information
kilmc authored Nov 6, 2023
2 parents f206287 + 001d49a commit 357ce56
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/shaggy-birds-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kilmc/music-fns": minor
---

Add support for 2nd, 4th, 5th, 6th, 9th, 11th and 13th chords
48 changes: 48 additions & 0 deletions src/converters/chords/chordInfoToBaseIntervals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,66 @@ const seventhMap: Record<string, IntervalShorthand[]> = {
'half-diminished': ['m7'],
};

const ninthMap: Record<string, IntervalShorthand[]> = {
major: ['M7', 'M9'],
dominant: ['m7', 'M9'],
minor: ['m7', 'M9'],
};

const eleventhMap: Record<string, IntervalShorthand[]> = {
major: ['M7', 'M9', 'P11'],
dominant: ['m7', 'M9', 'P11'],
minor: ['m7', 'M9', 'P11'],
};

const thirteenthMap: Record<string, IntervalShorthand[]> = {
major: ['M7', 'M9', 'M13'],
dominant: ['m7', 'M9', 'P11', 'M13'],
minor: ['m7', 'M9', 'M13'],
};

export const chordInfoToBaseIntervals = (
chordInfo: ChordInfo
): IntervalShorthand[] => {
const { type, quality } = chordInfo;

// second handled in createAddNotes as they're aliases

if (type === 'fourth') return ['P4', 'm7', 'm10'];
if (type === 'fifth') return ['P5', 'P8'];

if (type === 'sixth') {
return [...qualityMap[quality === undefined ? 'dominant' : quality], 'M6'];
}

if (type === 'seventh') {
return [
...qualityMap[quality === undefined ? 'dominant' : quality],
...seventhMap[quality === undefined ? 'dominant' : quality],
];
}

if (type === 'ninth') {
return [
...qualityMap[quality === undefined ? 'dominant' : quality],
...ninthMap[quality === undefined ? 'dominant' : quality],
];
}

if (type === 'eleventh') {
return [
...qualityMap[quality === undefined ? 'dominant' : quality],
...eleventhMap[quality === undefined ? 'dominant' : quality],
];
}

if (type === 'thirteenth') {
return [
...qualityMap[quality === undefined ? 'dominant' : quality],
...thirteenthMap[quality === undefined ? 'dominant' : quality],
];
}

if (quality === 'major') return qualityMap[quality];
if (quality === 'minor') return qualityMap[quality];
if (quality === 'diminished') return qualityMap[quality];
Expand Down
3 changes: 3 additions & 0 deletions src/public/getChord/createAddNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { ChordInfo } from '../../readers/chords/readChord.js';
import { transposeNote } from '../transposeNote.js';

export const createAddNotes = (chord: ChordInfo) => {
if (chord.type === 'second') {
return transposeNote(chord.chordRoot, 'M2');
}
if (!chord.isAddChord) return undefined;

const intervalSymbol = /(4|11)/.test(String(chord.addDegree)) ? 'P' : 'M';
Expand Down
24 changes: 24 additions & 0 deletions src/public/getChord/getChord.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,30 @@ describe('chord', () => {
});
});

describe('non-seventh extended chords', () => {
const addChords = [
{ chordName: 'C2', notes: ['C', 'D', 'E', 'G'] },
{ chordName: 'C4', notes: ['C', 'F', 'Bb', 'Eb'] },
{ chordName: 'C5', notes: ['C', 'G', 'C'] },
{ chordName: 'C6', notes: ['C', 'E', 'G', 'A'] },
{ chordName: 'Cm6', notes: ['C', 'Eb', 'G', 'A'] },
{ chordName: 'C6/9', notes: ['C', 'E', 'G', 'A', 'D'] },
{ chordName: 'C9', notes: ['C', 'E', 'G', 'Bb', 'D'] },
{ chordName: 'Cm9', notes: ['C', 'Eb', 'G', 'Bb', 'D'] },
{ chordName: 'Cmaj9', notes: ['C', 'E', 'G', 'B', 'D'] },
{ chordName: 'C11', notes: ['C', 'E', 'G', 'Bb', 'D', 'F'] },
{ chordName: 'Cm11', notes: ['C', 'Eb', 'G', 'Bb', 'D', 'F'] },
{ chordName: 'C13', notes: ['C', 'E', 'G', 'Bb', 'D', 'F', 'A'] },
{ chordName: 'Cm13', notes: ['C', 'Eb', 'G', 'Bb', 'D', 'A'] },
{ chordName: 'Cmaj13', notes: ['C', 'E', 'G', 'B', 'D', 'A'] },
];
it.each(addChords)('$chordName', ({ chordName, notes }) => {
expect(
getChord(chordName)?.notes?.map((note) => note?.name)
).toStrictEqual(notes);
});
});

describe('add chords', () => {
const addChords = [
{ chordName: 'Cadd2', notes: ['C', 'D', 'E', 'G'] },
Expand Down
9 changes: 9 additions & 0 deletions src/readers/chords/readAddInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ export const readAddInfo = (
const regex = /add(2|4|9|11|13)/;
const match = input.match(regex)?.slice(1, 2).at(0);

const sixNineRegex = /6\/9/;

if (sixNineRegex.test(input)) {
return {
isAddChord: true,
addDegree: 9,
};
}

if (match === undefined) {
return {
isAddChord: false,
Expand Down
19 changes: 16 additions & 3 deletions src/readers/chords/readChord.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,26 @@ describe('readChord', () => {
});
});

it('six-nine chords', () => {
expect(readChord('B6/9')).toStrictEqual({
...empty,
input: 'B6/9',
chordRoot: 'B',
rootNote: 'B',
quality: 'major',
type: 'sixth',
isAddChord: true,
addDegree: 9,
});
});

it('ninth chords', () => {
expect(readChord('C9')).toStrictEqual({
...empty,
input: 'C9',
chordRoot: 'C',
rootNote: 'C',
quality: 'major',
quality: 'dominant',
type: 'ninth',
});
});
Expand All @@ -134,7 +147,7 @@ describe('readChord', () => {
input: 'C11',
chordRoot: 'C',
rootNote: 'C',
quality: 'major',
quality: 'dominant',
type: 'eleventh',
});
});
Expand All @@ -145,7 +158,7 @@ describe('readChord', () => {
input: 'C13',
chordRoot: 'C',
rootNote: 'C',
quality: 'major',
quality: 'dominant',
type: 'thirteenth',
});
});
Expand Down
13 changes: 9 additions & 4 deletions src/readers/chords/readChordQuality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ export const readChordQuality = (
const regex = new RegExp(`${noteRegex.source}(${chordQualityRegex.source})?`);
const quality = input.match(regex)?.slice(3, 4)[0];

if (quality === undefined && type === 'seventh') return 'dominant';
const hasQuality = quality !== undefined;
const hasType = type !== undefined;

if (
quality === undefined ||
majorChordIdentifiers.some((i) => quality === i)
) {
!hasQuality &&
hasType &&
['seventh', 'ninth', 'eleventh', 'thirteenth'].includes(type)
)
return 'dominant';

if (!hasQuality || majorChordIdentifiers.some((i) => quality === i)) {
return 'major';
}

Expand Down

0 comments on commit 357ce56

Please sign in to comment.