Skip to content

Commit

Permalink
New Chord.getChord function (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb authored Apr 27, 2020
1 parent 98636cc commit 20f85b5
Show file tree
Hide file tree
Showing 8 changed files with 1,345 additions and 798 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
]
},
"devDependencies": {
"@types/jest": "^24.0.23",
"@types/node": "^13.1.4",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.4",
"husky": "^3.1.0",
"jest": "^24.8.0",
"jest-config": "^24.8.0",
"jest": "^25.4.0",
"jest-config": "^25.4.0",
"lerna": "^3.19.0",
"lint-staged": "^10.0.8",
"lodash": "^4.17.15",
Expand All @@ -74,7 +74,7 @@
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-typescript2": "^0.25.3",
"ts-jest": "^24.2.0",
"ts-jest": "^25.4.0",
"tslint": "^5.20.1",
"tslint-config-prettier": "^1.18.0",
"typescript": "^3.7.3"
Expand Down
6 changes: 3 additions & 3 deletions packages/chord-type/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ describe("@tonaljs/chord-type", () => {
// sorted
expect(ChordType.names().slice(0, 5)).toEqual([
"fifth",
"suspended 4th",
"suspended 4th seventh",
"suspended fourth",
"suspended fourth seventh",
"augmented",
"major seventh b6"
"major seventh flat sixth"
]);
});

Expand Down
51 changes: 39 additions & 12 deletions packages/chord/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,54 @@ const { Chord } = require("@tonaljs/tonal");

## API

#### `Chord.get(name: string) => Scale`
#### `Chord.getChord(type: string, tonic?: string, root?: string) => Chord`

Get a chord from a chord name. Unlike `chordType`, `chord` accepts tonics in the chord name and returns the a Scale data object (a ChordType with extended properties):
Get the chord properties from a chord type and an (optional) tonic and (optional) root. Notice that the tonic must be present if the root is present. Also the root must be part of the chord notes.

- tonic: the tonic of the chord, or "" if not present
- notes: an array of notes, or empty array if tonic is not present
It returns the same object that `ChordType.get` but with additional properties:

- symbol: the chord symbol (a combiation of the tonic, chord type shortname and root, if present). For example: `Cmaj7`, `Db7b5/F`. The symbol always uses pitch classes (note names without octaves) for both the tonic and root.
- tonic: the tonic of the chord (or an empty string if not present)
- root: the root of the chord (or an empty string if not present)
- rootDegree: the degree of the root. 0 if root not present. A number greater than 0 if present, where 1 indicates the tonic, 2 the second note (normally the 3th), 2 the third note (normally the 5th), etc.
- notes: an array of notes, or empty array if tonic is not present. If the root is pres

Example:

```js
Chord.get("Cmaj7");
// =>
Chord.getChord("maj7", "G4", "B4"); // =>
// {
// name: "C major seventh",
// tonic: "C",
// empty: false,
// name: "G major seventh over B",
// symbol: "Gmaj7/B",
// tonic: "G4",
// root: "B4",
// rootDegree: 2,
// setNum: 2193,
// type: "major seventh",
// aliases: ["maj7", "Δ", "ma7", "M7", "Maj7"],
// chroma: "100010010001",
// intervals: ["1P", "3M", "5P", "7M"],
// notes: ["C", "E", "G", "B"],
// quality: "Major"
// };
// normalized: "100010010001",
// notes: ["G4", "B4", "D5", "F#5"],
// quality: "Major",
// }
```

#### `Chord.get(name: string) =>`

An alias of `Chord.getChord` but accepts a chord symbol as parameter.

```js
Chord.get("Cmaj7");
// same as
Chord.getChord("maj7", "C");
```

Important: currently chord with roots are NOT allowed (will be implemented in next version):

```js
Chord.get("Cmaj7/E"); // => { empty: true }
```

#### `Chord.detect(notes: string[]) => string[]`
Expand All @@ -52,7 +79,7 @@ Alias of [chord-detect](/packages/chord-detect)

#### `Chord.transpose(chordName: string, intervalName: string) => string`

Transpose a chord name by an interval:
Transpose a chord symbol by an interval:

```js
Chord.transpose("Eb7b9", "5P"); // => "Bb7b9"
Expand Down
75 changes: 67 additions & 8 deletions packages/chord/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {

import {
deprecate,
distance,
note,
NoteName,
tokenizeNote,
transpose as transposeNote
} from "@tonaljs/core";

import { isSubsetOf, isSupersetOf, modes } from "@tonaljs/pcset";
import { isSubsetOf, isSupersetOf } from "@tonaljs/pcset";

import { all as scaleTypes } from "@tonaljs/scale-type";
export { detect } from "@tonaljs/chord-detect";
Expand All @@ -24,12 +25,18 @@ type ChordNameTokens = [string, string]; // [TONIC, SCALE TYPE]
interface Chord extends ChordType {
tonic: string | null;
type: string;
root: string;
rootDegree: number;
symbol: string;
notes: NoteName[];
}

const NoChord: Chord = {
empty: true,
name: "",
symbol: "",
root: "",
rootDegree: 0,
type: "",
tonic: null,
setNum: NaN,
Expand Down Expand Up @@ -85,17 +92,68 @@ export function tokenize(name: string): ChordNameTokens {
* Get a Chord from a chord name.
*/
export function get(src: ChordName | ChordNameTokens): Chord {
const { type, tonic } = findChord(src);
if (src === "") {
return NoChord;
}
if (Array.isArray(src) && src.length === 2) {
return getChord(src[1], src[0]);
} else {
const [tonic, type] = tokenize(src);
const chord = getChord(type, tonic);
return chord.empty ? getChord(src) : chord;
}
}

/**
* Get chord properties
*
* @param typeName - the chord type name
* @param [tonic] - Optional tonic
* @param [root] - Optional root (requires a tonic)
*/
export function getChord(
typeName: string,
optionalTonic?: string,
optionalRoot?: string
): Chord {
const type = getChordType(typeName);
const tonic = note(optionalTonic || "");
const root = note(optionalRoot || "");

if (
type.empty ||
(optionalTonic && tonic.empty) ||
(optionalRoot && root.empty)
) {
return NoChord;
}

if (!type || type.empty) {
const rootInterval = distance(tonic.pc, root.pc);
const rootDegree = type.intervals.indexOf(rootInterval) + 1;
if (!root.empty && !rootDegree) {
return NoChord;
}

const notes: string[] = tonic
? type.intervals.map(i => transposeNote(tonic, i))
: [];
const name = tonic ? tonic + " " + type.name : type.name;
return { ...type, name, type: type.name, tonic: tonic || "", notes };
const notes = tonic.empty
? []
: type.intervals.map(i => transposeNote(tonic, i));
typeName = type.aliases.indexOf(typeName) !== -1 ? typeName : type.aliases[0];
const symbol = `${tonic.empty ? "" : tonic.pc}${typeName}${
root.empty ? "" : "/" + root.pc
}`;
const name = `${optionalTonic ? tonic.pc + " " : ""}${type.name}${
optionalRoot ? " over " + root.pc : ""
}`;
return {
...type,
name,
symbol,
type: type.name,
root: root.name,
rootDegree,
tonic: tonic.name,
notes
};
}

export const chord = deprecate("Chord.chord", "Chord.get", get);
Expand Down Expand Up @@ -180,6 +238,7 @@ export function reduced(chordName: string): string[] {
}

export default {
getChord,
get,
detect,
chordScales,
Expand Down
69 changes: 55 additions & 14 deletions packages/chord/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,64 @@ describe("tonal-chord", () => {
expect(Chord.tokenize("C4")).toEqual(["C", "4"]);
});

describe("getChord", () => {
test("Chord properties", () => {
expect(Chord.getChord("maj7", "G4", "B4")).toEqual({
empty: false,
name: "G major seventh over B",
symbol: "Gmaj7/B",
tonic: "G4",
root: "B4",
rootDegree: 2,
setNum: 2193,
type: "major seventh",
aliases: ["maj7", "Δ", "ma7", "M7", "Maj7"],
chroma: "100010010001",
intervals: ["1P", "3M", "5P", "7M"],
normalized: "100010010001",
notes: ["G4", "B4", "D5", "F#5"],
quality: "Major"
});
});
test("without root", () => {
expect(Chord.getChord("M7", "G")).toMatchObject({
symbol: "GM7",
name: "G major seventh",
notes: ["G", "B", "D", "F#"]
});
});
test("rootDegrees", () => {
expect(Chord.getChord("maj7", "C", "C").rootDegree).toBe(1);
expect(Chord.getChord("maj7", "C", "D").empty).toBe(true);
});
test("without tonic nor root", () => {
expect(Chord.getChord("dim")).toEqual({
symbol: "dim",
name: "diminished",
tonic: "",
root: "",
rootDegree: 0,
type: "diminished",
aliases: ["dim", "°", "o"],
chroma: "100100100000",
empty: false,
intervals: ["1P", "3m", "5d"],
normalized: "100000100100",
notes: [],
quality: "Diminished",
setNum: 2336
});
});
});

test("chord", () => {
expect(Chord.get("Cmaj7")).toEqual({
empty: false,
symbol: "Cmaj7",
name: "C major seventh",
tonic: "C",
root: "",
rootDegree: 0,
setNum: 2193,
type: "major seventh",
aliases: ["maj7", "Δ", "ma7", "M7", "Maj7"],
Expand All @@ -37,23 +90,11 @@ describe("tonal-chord", () => {
});
expect(Chord.get("hello").empty).toBe(true);
expect(Chord.get("").empty).toBe(true);
expect(Chord.get("C")).toEqual(Chord.get("C major"));
expect(Chord.get("C").name).toEqual("C major");
});

test("chord without tonic", () => {
expect(Chord.get("dim")).toEqual({
aliases: ["dim", "°", "o"],
chroma: "100100100000",
empty: false,
intervals: ["1P", "3m", "5d"],
name: "diminished",
normalized: "100000100100",
notes: [],
quality: "Diminished",
setNum: 2336,
tonic: "",
type: "diminished"
});
expect(Chord.get("dim")).toMatchObject({ name: "diminished" });
expect(Chord.get("dim7")).toMatchObject({ name: "diminished seventh" });
expect(Chord.get("alt7")).toMatchObject({ name: "altered" });
});
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.

Loading

0 comments on commit 20f85b5

Please sign in to comment.