Skip to content

Commit

Permalink
Reworking chord detection to be order independent.
Browse files Browse the repository at this point in the history
Resolves tonaljs#160.
  • Loading branch information
jhmcstanton committed Oct 8, 2020
1 parent 8084482 commit 173e780
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 16 deletions.
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ If you are adding new functionality or fixing a bug, please add a test for it.
yarn test:ci
```

###How to add a new module
### How to add a new module

To create a new module:

Expand Down
52 changes: 43 additions & 9 deletions packages/chord-detect/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { all } from "@tonaljs/chord-type";
import { note } from "@tonaljs/core";
import { note, distance } from "@tonaljs/core";
import { name, sortedNames } from "@tonaljs/note";
import { simplify } from "@tonaljs/interval";
import { modes } from "@tonaljs/pcset";

interface FoundChord {
Expand All @@ -20,7 +22,9 @@ const namedSet = (notes: string[]) => {
};

export function detect(source: string[]): string[] {
const notes = source.map((n) => note(n).pc).filter((x) => x);
const notes = sortedNames(source)
.map((n) => note(n).pc)
.filter((x) => x);
if (note.length === 0) {
return [];
}
Expand All @@ -33,12 +37,41 @@ export function detect(source: string[]): string[] {
.map((chord) => chord.name);
}

// Assumes that chord is presorted
function findRoot(chord: string[]): string {
let foundRoot = null;
chord.every((note) => {
const workComplete = chord.some((otherNote) => {
const interval = simplify(distance(note, otherNote));
const orderedNotes = sortedNames([note, otherNote]);
if (interval === "5P") {
foundRoot = orderedNotes[0];
return foundRoot; // Loop is complete
} else if (interval === "4P") {
foundRoot = orderedNotes[1];
return foundRoot; // Loop is complete
}
return false; // continue looping
});
// Continue looping if the root note was not found
return !workComplete;
});
if (foundRoot) {
return foundRoot;
} else {
// Defaults to the old behavior if the chord is complex and the root note cannot be easily found
return chord[0];
}
}

// assumes that notes is presorted
function findExactMatches(notes: string[], weight: number): FoundChord[] {
const tonic = notes[0];
const tonicChroma = note(tonic).chroma;
const root = findRoot(notes);
const noteName = namedSet(notes);
// we need to test all chormas to get the correct baseNote
// we need to test all chromas to get the correct baseNote
const allModes = modes(notes, false);
const baseNote = notes[0];
const baseChroma = note(baseNote).chroma;

const found: FoundChord[] = [];
allModes.forEach((mode, index) => {
Expand All @@ -47,15 +80,16 @@ function findExactMatches(notes: string[], weight: number): FoundChord[] {

chordTypes.forEach((chordType) => {
const chordName = chordType.aliases[0];
const baseNote = noteName(index);
const isInversion = index !== tonicChroma;
const rootNote = noteName(index);
const isInversion = note(rootNote).chroma !== baseChroma;

if (isInversion) {
found.push({
weight: 0.5 * weight,
name: `${baseNote}${chordName}/${tonic}`,
name: `${rootNote}${chordName}/${baseNote}`,
});
} else {
found.push({ weight: 1 * weight, name: `${baseNote}${chordName}` });
found.push({ weight: 1 * weight, name: `${root}${chordName}` });
}
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/chord-detect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"dependencies": {
"@tonaljs/chord-type": "^3.6.0",
"@tonaljs/core": "^3.5.4",
"@tonaljs/interval": "^3.5.4",
"@tonaljs/note": "^3.5.4",
"@tonaljs/pcset": "^3.5.4"
},
"author": "[email protected]",
Expand Down
14 changes: 10 additions & 4 deletions packages/chord-detect/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ import { detect } from "./index";

describe("@tonal/chord-detect", () => {
test("detect", () => {
expect(detect(["D", "F#", "A", "C"])).toEqual(["D7"]);
expect(detect(["F#", "A", "C", "D"])).toEqual(["D7/F#"]);
expect(detect(["A", "C", "D", "F#"])).toEqual(["D7/A"]);
expect(detect(["E", "G#", "B", "C#"])).toEqual(["E6", "C#m7/E"]);
expect(detect(["D", "F#", "A", "C"])).toEqual(["D7/C"]);
expect(detect(["D3", "F#4", "A3", "C4"])).toEqual(["D7"]);
expect(detect(["F#4", "A3", "C4", "D3"])).toEqual(["D7"]);
expect(detect(["F#2", "A3", "C4", "D3"])).toEqual(["D7/F#"]);
expect(detect(["A3", "C4", "D3", "F#4"])).toEqual(["D7"]);
expect(detect(["A2", "C4", "D3", "F#4"])).toEqual(["D7/A"]);
expect(detect(["E3", "G#4", "B4", "C#4"])).toEqual(["E6", "C#m7/E"]);
expect(detect(["C4", "E4", "G4"])).toEqual(["CM", "Em#5/C"]);
expect(detect(["E4", "G4", "C5"])).toEqual(["Gm#5", "CM/E"]);
});

test("(regression) detect aug", () => {
expect(detect(["C", "E", "G#"])).toEqual(["Caug", "Eaug/C", "G#aug/C"]);
expect(detect(["E", "G#", "C"])).toEqual(["Caug", "Eaug/C", "G#aug/C"]);
});

test("edge cases", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/tonal/browser/tonal.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/tonal/browser/tonal.min.js.map

Large diffs are not rendered by default.

0 comments on commit 173e780

Please sign in to comment.