Browse Source

Initial commit

Add files from pridepack.
master
TheoryOfNekomata 1 year ago
commit
2c84375b7f
13 changed files with 5278 additions and 0 deletions
  1. +9
    -0
      .eslintrc
  2. +109
    -0
      .gitignore
  3. +7
    -0
      LICENSE
  4. +49
    -0
      package.json
  5. +3
    -0
      pridepack.json
  6. +428
    -0
      src/analyze.ts
  7. +93
    -0
      src/common.ts
  8. +274
    -0
      src/index.ts
  9. +268
    -0
      src/utils.ts
  10. +427
    -0
      test/index.test.ts
  11. +21
    -0
      tsconfig.eslint.json
  12. +21
    -0
      tsconfig.json
  13. +3569
    -0
      yarn.lock

+ 9
- 0
.eslintrc View File

@@ -0,0 +1,9 @@
{
"root": true,
"extends": [
"lxsmnsyc/typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}

+ 109
- 0
.gitignore View File

@@ -0,0 +1,109 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.production
.env.development

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

.npmrc
.idea/
/types/

+ 7
- 0
LICENSE View File

@@ -0,0 +1,7 @@
MIT License Copyright (c) 2023 TheoryOfNekomata <allan.crisostomo@outlook.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 49
- 0
package.json View File

@@ -0,0 +1,49 @@
{
"name": "@modal-sh/chordova-core",
"version": "0.0.0",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=12"
},
"license": "MIT",
"keywords": [
"pridepack"
],
"devDependencies": {
"@types/node": "^18.14.1",
"eslint": "^8.35.0",
"eslint-config-lxsmnsyc": "^0.5.0",
"pridepack": "2.4.4",
"tslib": "^2.5.0",
"typescript": "^4.9.5",
"vitest": "^0.28.1"
},
"scripts": {
"prepublishOnly": "pridepack clean && pridepack build",
"build": "pridepack build",
"type-check": "pridepack check",
"lint": "pridepack lint",
"clean": "pridepack clean",
"watch": "pridepack watch",
"start": "pridepack start",
"dev": "pridepack dev",
"test": "vitest"
},
"private": false,
"description": "Look up chords from pitches.",
"repository": {
"url": "https://code.modal.sh/modal-soft/chords-core",
"type": "git"
},
"homepage": "https://code.modal.sh/modal-soft/chords-core",
"bugs": {
"url": "https://code.modal.sh/modal-soft/chords-core/issues"
},
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"publishConfig": {
"access": "public"
}
}

+ 3
- 0
pridepack.json View File

@@ -0,0 +1,3 @@
{
"target": "es2018"
}

+ 428
- 0
src/analyze.ts View File

@@ -0,0 +1,428 @@
import {
CHORD_COMPONENT_ORDERS,
ChordAnalysis,
ChordBase,
ChordComponent,
ChordExtensionType,
ChordModificationType,
Interval,
} from './common';

const INTERVAL_COMPONENT_MAPPING = {
[Interval.UNISON]: ChordComponent.ROOT,
[Interval.MINOR_SECOND]: ChordComponent.SECOND,
[Interval.MAJOR_SECOND]: ChordComponent.SECOND,
[Interval.MINOR_THIRD]: ChordComponent.THIRD,
[Interval.MAJOR_THIRD]: ChordComponent.THIRD,
[Interval.PERFECT_FOURTH]: ChordComponent.FOURTH,
[Interval.DIMINISHED_FIFTH]: ChordComponent.FIFTH,
[Interval.PERFECT_FIFTH]: ChordComponent.FIFTH,
[Interval.AUGMENTED_FIFTH]: ChordComponent.FIFTH,
[Interval.MAJOR_SIXTH]: ChordComponent.SIXTH,
[Interval.MINOR_SEVENTH]: ChordComponent.SEVENTH,
[Interval.MAJOR_SEVENTH]: ChordComponent.SEVENTH,
[Interval.OCTAVE]: ChordComponent.ROOT,
[Interval.MINOR_NINTH]: ChordComponent.NINTH,
[Interval.MAJOR_NINTH]: ChordComponent.NINTH,
[Interval.AUGMENTED_NINTH]: ChordComponent.NINTH,
[Interval.TENTH]: ChordComponent.TENTH,
[Interval.MINOR_ELEVENTH]: ChordComponent.ELEVENTH,
[Interval.MAJOR_ELEVENTH]: ChordComponent.ELEVENTH,
[Interval.TWELFTH]: ChordComponent.TWELFTH,
[Interval.MINOR_THIRTEENTH]: ChordComponent.THIRTEENTH,
[Interval.MAJOR_THIRTEENTH]: ChordComponent.THIRTEENTH,
[Interval.AUGMENTED_THIRTEENTH]: ChordComponent.THIRTEENTH,
} as const;

const normalizeIntervalsFromRoot = (intervalsFromRoot: Interval[]) => intervalsFromRoot.reduce(
(normalized, intervalFromRoot) => {
const theNormalized = [...normalized];
const { [intervalFromRoot]: component } = INTERVAL_COMPONENT_MAPPING;
theNormalized[CHORD_COMPONENT_ORDERS.indexOf(component)] = intervalFromRoot;
return theNormalized;
},
[] as Interval[],
);

const getChordBase = (normalizedIntervals: Interval[]) => {
const [, second, third, fourth, fifth] = normalizedIntervals;

switch (third) {
case Interval.MINOR_THIRD:
return {
base: fifth === Interval.DIMINISHED_FIFTH ? ChordBase.DIMINISHED : ChordBase.MINOR,
};
case Interval.MAJOR_THIRD:
if (fifth === Interval.AUGMENTED_FIFTH) {
return {
base: ChordBase.AUGMENTED,
};
}
if (fifth === Interval.DIMINISHED_FIFTH) {
return {
base: ChordBase.MAJOR,
modifications: [
{
type: ChordModificationType.LOWERED,
component: ChordComponent.FIFTH,
},
],
};
}
return {
base: ChordBase.MAJOR,
};
default:
break;
}

if (second === Interval.MAJOR_SECOND) {
return {
modifications: [
{
type: ChordModificationType.SUSPENDED,
component: ChordComponent.SECOND,
},
],
};
}

if (fourth === Interval.PERFECT_FOURTH) {
return {
modifications: [
{
type: ChordModificationType.SUSPENDED,
component: ChordComponent.FOURTH,
},
],
};
}

return {};
};

const getChordSixth = (normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis) => {
const { extensions: priorExtensions = [] } = priorAnalysis;
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.FIFTH)]: fifth,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth,
} = normalizedIntervals;

if (fifth === Interval.PERFECT_FIFTH && sixth === Interval.MAJOR_SIXTH) {
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MAJOR,
component: ChordComponent.SIXTH,
}],
};
}

return priorAnalysis;
};

const getChordSeventh = (
normalizedIntervals: Interval[],
priorAnalysis: ChordAnalysis,
) => {
const { extensions: priorExtensions = [] } = priorAnalysis;
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh,
} = normalizedIntervals;
if (priorAnalysis.base === ChordBase.DIMINISHED && sixth === Interval.DIMINISHED_SEVENTH) {
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.DIMINISHED,
component: ChordComponent.SEVENTH,
}],
};
}

switch (seventh) {
case Interval.MAJOR_SEVENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
};
case Interval.MINOR_SEVENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
};
default:
break;
}

return priorAnalysis;
};

const getChordNinth = (
normalizedIntervals: Interval[],
priorAnalysis: ChordAnalysis,
) => {
const { extensions: priorExtensions = [] } = priorAnalysis;
const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth } = normalizedIntervals;

switch (ninth) {
case Interval.MINOR_NINTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MINOR,
component: ChordComponent.NINTH,
}],
};
case Interval.MAJOR_NINTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MAJOR,
component: ChordComponent.NINTH,
}],
};
case Interval.AUGMENTED_NINTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.AUGMENTED,
component: ChordComponent.NINTH,
}],
};
default:
break;
}

return priorAnalysis;
};

const getChordEleventh = (
normalizedIntervals: Interval[],
priorAnalysis: ChordAnalysis,
) => {
const { extensions: priorExtensions = [] } = priorAnalysis;
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh,
} = normalizedIntervals;

switch (eleventh) {
case Interval.MINOR_ELEVENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MINOR,
component: ChordComponent.ELEVENTH,
}],
};
case Interval.MAJOR_ELEVENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MAJOR,
component: ChordComponent.ELEVENTH,
}],
};
default:
break;
}

return priorAnalysis;
};

const getChordThirteenth = (
normalizedIntervals: Interval[],
priorAnalysis: ChordAnalysis,
) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.THIRTEENTH)]: thirteenth,
} = normalizedIntervals;
const { extensions: priorExtensions = [] } = priorAnalysis;

switch (thirteenth) {
case Interval.MINOR_THIRTEENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MINOR,
component: ChordComponent.THIRTEENTH,
}],
};
case Interval.MAJOR_THIRTEENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.MAJOR,
component: ChordComponent.THIRTEENTH,
}],
};
case Interval.AUGMENTED_THIRTEENTH:
return {
...priorAnalysis,
extensions: [...priorExtensions, {
type: ChordExtensionType.AUGMENTED,
component: ChordComponent.THIRTEENTH,
}],
};
default:
break;
}

return priorAnalysis;
};

const getBaseChordOmissions = (normalizedIntervals: Interval[]) => {
const [
root,
second,
third,
fourth,
] = normalizedIntervals;

const omissions = [];

if (!second && !third && !fourth) {
omissions.push(ChordComponent.THIRD);
}

if (root !== Interval.UNISON) {
omissions.push(ChordComponent.ROOT);
}

return omissions;
};

const getSeventhChordOmissions = (normalizedIntervals: Interval[]) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.FIFTH)]: fifth,
} = normalizedIntervals;

const omissions = getBaseChordOmissions(normalizedIntervals);

if (!fifth) {
omissions.push(ChordComponent.FIFTH);
}

return omissions;
};

const getNinthChordOmissions = (normalizedIntervals: Interval[]) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh,
} = normalizedIntervals;

const omissions = getSeventhChordOmissions(normalizedIntervals);

if (!seventh && !sixth) {
omissions.push(ChordComponent.SEVENTH);
}

return omissions;
};

const getEleventhChordOmissions = (normalizedIntervals: Interval[]) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth,
} = normalizedIntervals;

const omissions = getNinthChordOmissions(normalizedIntervals);

if (!ninth) {
omissions.push(ChordComponent.NINTH);
}

return omissions;
};

const getThirteenthChordOmissions = (normalizedIntervals: Interval[]) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh,
} = normalizedIntervals;
const omissions = getEleventhChordOmissions(normalizedIntervals);

if (!eleventh) {
omissions.push(ChordComponent.ELEVENTH);
}

return omissions;
};

const getChordOmissions = (
normalizedIntervals: Interval[],
priorAnalysis: ChordAnalysis,
) => {
const {
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh,
[CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.THIRTEENTH)]: thirteenth,
} = normalizedIntervals;

if (thirteenth) {
return {
...priorAnalysis,
omissions: getThirteenthChordOmissions(normalizedIntervals),
};
}
if (eleventh) {
return {
...priorAnalysis,
omissions: getEleventhChordOmissions(normalizedIntervals),
};
}
if (ninth) {
return {
...priorAnalysis,
omissions: getNinthChordOmissions(normalizedIntervals),
};
}
if (sixth || seventh) {
return {
...priorAnalysis,
omissions: getSeventhChordOmissions(normalizedIntervals),
};
}

return {
...priorAnalysis,
omissions: getBaseChordOmissions(normalizedIntervals),
};
};

export const analyzeIntervals = (intervalsFromRoot: Interval[]) => {
const normalizedIntervals = normalizeIntervalsFromRoot(intervalsFromRoot);
let initAnalysis = getChordBase(normalizedIntervals) as ChordAnalysis;
initAnalysis = getChordSixth(normalizedIntervals, initAnalysis);
initAnalysis = getChordSeventh(normalizedIntervals, initAnalysis);
initAnalysis = getChordNinth(normalizedIntervals, initAnalysis);
initAnalysis = getChordEleventh(normalizedIntervals, initAnalysis);
initAnalysis = getChordThirteenth(normalizedIntervals, initAnalysis);
initAnalysis = getChordOmissions(normalizedIntervals, initAnalysis);

const retValue = {} as ChordAnalysis;
if (typeof initAnalysis.base !== 'undefined') {
retValue.base = initAnalysis.base;
}

if ((initAnalysis.extensions?.length ?? 0) > 0) {
retValue.extensions = initAnalysis.extensions;
}

if ((initAnalysis.modifications?.length ?? 0) > 0) {
retValue.modifications = initAnalysis.modifications;
}

if ((initAnalysis.omissions?.length ?? 0) > 0) {
retValue.omissions = initAnalysis.omissions;
}

return retValue;
};

+ 93
- 0
src/common.ts View File

@@ -0,0 +1,93 @@
export enum Interval {
UNISON,
MINOR_SECOND,
MAJOR_SECOND,
MINOR_THIRD,
MAJOR_THIRD,
PERFECT_FOURTH,
DIMINISHED_FIFTH,
PERFECT_FIFTH,
AUGMENTED_FIFTH,
MAJOR_SIXTH,
DIMINISHED_SEVENTH = MAJOR_SIXTH,
MINOR_SEVENTH,
MAJOR_SEVENTH,
OCTAVE,
MINOR_NINTH,
MAJOR_NINTH,
AUGMENTED_NINTH,
TENTH,
MINOR_ELEVENTH,
MAJOR_ELEVENTH,
TWELFTH,
MINOR_THIRTEENTH,
MAJOR_THIRTEENTH,
AUGMENTED_THIRTEENTH,
}

export enum ChordBase {
MAJOR = 'MAJOR',
MINOR = 'MINOR',
DIMINISHED = 'DIMINISHED',
AUGMENTED = 'AUGMENTED',
}

export enum ChordExtensionType {
MAJOR = 'MAJOR',
MINOR = 'MINOR',
DOMINANT = 'DOMINANT',
DIMINISHED = 'DIMINISHED',
AUGMENTED = 'AUGMENTED',
}

export enum ChordModificationType {
SUSPENDED = 'SUSPENDED',
LOWERED = 'LOWERED',
}

export enum ChordComponent {
ROOT = 'ROOT',
SECOND = 'SECOND',
THIRD = 'THIRD',
FOURTH = 'FOURTH',
FIFTH = 'FIFTH',
SIXTH = 'SIXTH',
SEVENTH = 'SEVENTH',
NINTH = 'NINTH',
TENTH = 'TENTH',
ELEVENTH = 'ELEVENTH',
TWELFTH = 'TWELFTH',
THIRTEENTH = 'THIRTEENTH',
}

export const CHORD_COMPONENT_ORDERS = [
ChordComponent.ROOT,
ChordComponent.SECOND,
ChordComponent.THIRD,
ChordComponent.FOURTH,
ChordComponent.FIFTH,
ChordComponent.SIXTH,
ChordComponent.SEVENTH,
ChordComponent.NINTH,
ChordComponent.TENTH,
ChordComponent.ELEVENTH,
ChordComponent.TWELFTH,
ChordComponent.THIRTEENTH,
];

export interface ChordModification {
type: ChordModificationType;
component: ChordComponent;
}

export interface ChordExtension {
type: ChordExtensionType;
component: ChordComponent;
}

export interface ChordAnalysis {
base?: ChordBase;
modifications?: ChordModification[];
extensions?: ChordExtension[];
omissions?: ChordComponent[];
}

+ 274
- 0
src/index.ts View File

@@ -0,0 +1,274 @@
// TODO make this dictionary more computable
// TODO construct/deconstruct chords
// TODO add chord inversion
// TODO add chord quality
// TODO add chord extensions
// TODO add chord alterations
// TODO provide formats for WAV, MIDI, MP3, notation, etc.

export * from './analyze';
export * from './common';
export * from './utils';

const CHORD_DICTIONARY = [
{
name: 'Augmented',
symbol: 'aug',
id: 'aug',
notes: [0, 4, 8],
},
{
name: 'Augmented eleventh',
symbol: '<sup>(♯11)</sup>',
id: '(#11)',
notes: [0, 4, 7, 10, 14, 18],
},
{
name: 'Augmented major seventh',
symbol: 'aug<sup>maj7</sup>',
id: 'augmaj7',
notes: [0, 4, 8, 11],
},
{
name: 'Augmented seventh',
symbol: 'aug<sup>7</sup>',
id: 'aug7',
notes: [0, 4, 8, 10],
},
{
name: 'Diminished',
symbol: 'dim',
id: 'dim',
notes: [0, 3, 6],
},
{
name: 'Diminished major seventh',
symbol: 'dim<sup>maj7</sup>',
id: 'dimmaj7',
notes: [0, 3, 6, 11],
},
{
name: 'Diminished seventh',
symbol: 'dim<sup>7</sup>',
id: 'dim7',
notes: [0, 3, 6, 9],
},
{
name: 'Dominant eleventh',
symbol: '<sup>11</sup>',
id: '11',
notes: [0, 4, 7, 10, 14, 17],
},
{
name: 'Dominant minor ninth',
symbol: '<sup>7♭9</sup>',
notes: [0, 4, 7, 10, 13],
},
{
name: 'Dominant ninth',
symbol: '<sup>9</sup>',
notes: [0, 4, 7, 10, 14],
},
{
name: 'Dominant seventh',
symbol: '<sup>7</sup>',
notes: [0, 4, 7, 10],
},
{
name: 'Dominant seventh flat five',
symbol: '<sup>7♭5</sup>',
notes: [0, 4, 6, 10],
},
{
name: 'Dominant seventh sharp nine',
symbol: '<sup>(♯9)</sup>',
notes: [0, 4, 7, 10, 15],
},
{ name: 'Dominant thirteenth', symbol: '<sup>13</sup>', notes: [0, 4, 7, 10, 14, 17, 21] },
{ name: 'Half-diminished seventh', symbol: 'm<sup>7(♭5)</sup>', notes: [0, 3, 6, 10] },
{
name: 'Major',
symbol: '',
id: '',
notes: [0, 4, 7],
},
{ name: 'Major eleventh', symbol: '<sup>maj11</sup>', notes: [0, 4, 7, 11, 14, 17] },
{
name: 'Major seventh',
symbol: '<sup>maj7</sup>',
notes: [0, 4, 7, 11],
},
{ name: 'Major seventh sharp eleventh', symbol: '<sup>maj7♯11</sup>', notes: [0, 4, 8, 11, 18] },
{
name: 'Major sixth',
symbol: '<sup>6</sup>',
notes: [0, 4, 7, 9],
},
{
name: 'Major sixth ninth',
symbol: '<sup>6(9)</sup>',
notes: [0, 4, 7, 9, 14],
},
{ name: 'Major ninth', symbol: '<sup>maj9</sup>', notes: [0, 4, 7, 11, 14] },
{
name: 'Major thirteenth',
symbol: '<sup>maj13</sup>',
notes: [0, 4, 7, 11, 14, 18, 21],
},
{
name: 'Minor',
symbol: 'm',
notes: [0, 3, 7],
},
{ name: 'Minor eleventh', symbol: 'm<sup>11</sup>', notes: [0, 3, 7, 10, 14, 17] },
{
name: 'Minor major seventh',
symbol: 'm<sup>maj7</sup>',
notes: [0, 3, 7, 11],
},
{ name: 'Minor ninth', symbol: 'm<sup>9</sup>', notes: [0, 3, 7, 10, 14] },
{
name: 'Minor seventh',
symbol: 'm<sup>7</sup>',
notes: [0, 3, 7, 10],
},
{ name: 'Minor sixth', symbol: 'm<sup>6</sup>', notes: [0, 3, 7, 9] },
{
name: 'Minor sixth ninth (6/9)',
symbol: 'm<sup>6(9)</sup>',
notes: [0, 3, 7, 9, 14],
},
{ name: 'Minor thirteenth', symbol: 'm<sup>13</sup>', notes: [0, 3, 7, 10, 14, 17, 21] },
{
name: 'Ninth augmented fifth',
symbol: '<sup>9♯5</sup>',
notes: [0, 4, 8, 10, 14],
},
{ name: 'Ninth flat fifth', notes: [0, 4, 6, 10, 14], symbol: '<sup>9♭5</sup>' },
{
name: 'Seven six',
symbol: '<sup>7(6)</sup>',
notes: [0, 4, 7, 9, 10],
},
{ name: 'Seventh suspension four', symbol: '<sup>7sus4</sup>', notes: [0, 5, 7, 10] },
{
name: 'Suspended fourth',
symbol: '<sup>sus4</sup>',
notes: [0, 5, 7],
},
{
name: 'Suspended second',
symbol: '<sup>sus2</sup>',
notes: [0, 2, 7],
},
{
name: 'Thirteenth flat ninth',
symbol: '<sup>13♭9</sup>',
notes: [0, 4, 7, 10, 13, 21],
},
{
name: 'Thirteenth flat ninth flat fifth',
symbol: '<sup>13♭9(♭5)</sup>',
notes: [0, 4, 6, 10, 13, 21],
},
];

export enum ConstructChordFormat {
PITCH_ARRAY,
NOTATION,
SOUND,
}

enum NotationType {
STANDARD,
TABLATURE,
}

interface ConstructChordOptions {
format?: ConstructChordFormat;
}

enum ConstructChordStyle {
CHORD = 'CHORD',
ARPEGGIO = 'ARPEGGIO',
ARPEGGIO_THEN_CHORD = 'ARPEGGIO_THEN_CHORD',
CHORD_THEN_ARPEGGIO = 'CHORD_THEN_ARPEGGIO',
}

interface ConstructChordSoundOptions extends ConstructChordOptions {
format?: ConstructChordFormat.SOUND;
midiInstrument?: number;
style?: ConstructChordStyle;
mimeType?: string;
}

interface ConstructChordNotationOptions extends ConstructChordOptions {
format?: ConstructChordFormat.NOTATION;
notationType?: NotationType;
mimeType?: string;
}

export enum PitchClass {
C,
B_SHARP = C,
C_SHARP,
D_FLAT = C_SHARP,
D,
D_SHARP,
E_FLAT = D_SHARP,
E,
F_FLAT = E,
F,
E_SHARP = F,
F_SHARP,
G_FLAT = F_SHARP,
G,
G_SHARP,
A_FLAT = G_SHARP,
A,
A_SHARP,
B_FLAT = A_SHARP,
B,
C_FLAT = B,
}

interface ChordParams {
pitchClass: PitchClass;
chord?: string;
inversion?: number;
}

const constructChordPitchArray = (params: ChordParams) => {
const { pitchClass, chord = '', inversion = 0 } = params;
const chordDefinition = CHORD_DICTIONARY.find((c) => c.id === chord);
if (!chordDefinition) {
throw new Error(`Chord ${chord} not found`);
}
const { notes } = chordDefinition;
const pitchArray = notes.map((note) => note + pitchClass);
return pitchArray;
};

const constructChordNotation = (params: ChordParams, options: ConstructChordNotationOptions) => {
return constructChordPitchArray(params);
};

const constructChordSound = (params: ChordParams, options: ConstructChordSoundOptions) => {
return constructChordPitchArray(params);
};

export const constructChord = (params: ChordParams, options = {} as ConstructChordOptions) => {
const { format = ConstructChordFormat.PITCH_ARRAY } = options;
switch (format) {
case ConstructChordFormat.PITCH_ARRAY:
return constructChordPitchArray(params);
case ConstructChordFormat.NOTATION:
return constructChordNotation(params, options as ConstructChordNotationOptions);
case ConstructChordFormat.SOUND:
return constructChordSound(params, options as ConstructChordSoundOptions);
default:
break;
}

throw new TypeError(`Unknown format ${format as string}`);
};

+ 268
- 0
src/utils.ts View File

@@ -0,0 +1,268 @@
import {
ChordAnalysis,
ChordBase,
ChordComponent,
ChordExtensionType,
ChordModificationType,
} from './common';

export const getHtmlChordSymbol = (analysis: ChordAnalysis) => {
let chordSymbol = '';

if (analysis.base === ChordBase.MINOR) {
chordSymbol += 'm';
} else if (analysis.base === ChordBase.DIMINISHED) {
chordSymbol += 'dim';
} else if (analysis.base === ChordBase.AUGMENTED) {
chordSymbol += 'aug';
}

const {
extensions = [],
modifications = [],
omissions = [],
} = analysis;

if (extensions.length > 0) {
extensions.forEach((extension) => {
if (extension.component === ChordComponent.SIXTH) {
if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol += '<sup>6</sup>';
}
}
if (extension.component === ChordComponent.SEVENTH) {
if (extension.type === ChordExtensionType.DOMINANT) {
chordSymbol += '<sup>7</sup>';
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol += '<sup>maj7</sup>';
}

if (extension.type === ChordExtensionType.DIMINISHED) {
chordSymbol += '<sup>7</sup>';
}
}

if (extension.component === ChordComponent.NINTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol += '<sup>(♭9)</sup>';
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol += '<sup>(9)</sup>';
}

if (extension.type === ChordExtensionType.AUGMENTED) {
chordSymbol += '<sup>(♯9)</sup>';
}
}

if (extension.component === ChordComponent.ELEVENTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol += '<sup>(♭11)</sup>';
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol += '<sup>(11)</sup>';
}
}

if (extension.component === ChordComponent.THIRTEENTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol += '<sup>(♭13)</sup>';
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol += '<sup>(13)</sup>';
}

if (extension.type === ChordExtensionType.AUGMENTED) {
chordSymbol += '<sup>(♯13)</sup>';
}
}
});
}

if (modifications.length > 0) {
modifications.forEach((modification) => {
if (modification.component === ChordComponent.FIFTH) {
if (modification.type === ChordModificationType.LOWERED) {
chordSymbol += '<sup>(♭5)</sup>';
}
}

if (modification.component === ChordComponent.FOURTH) {
if (modification.type === ChordModificationType.SUSPENDED) {
chordSymbol += 'sus4';
}
}

if (modification.component === ChordComponent.SECOND) {
if (modification.type === ChordModificationType.SUSPENDED) {
chordSymbol += 'sus2';
}
}
});
}

if (omissions.length > 0) {
omissions.forEach((omission) => {
switch (omission) {
case ChordComponent.THIRD:
chordSymbol += '<sup>(no3)</sup>';
break;
case ChordComponent.FIFTH:
chordSymbol += '<sup>(no5)</sup>';
break;
case ChordComponent.SEVENTH:
chordSymbol += '<sup>(no7)</sup>';
break;
case ChordComponent.NINTH:
chordSymbol += '<sup>(no9)</sup>';
break;
case ChordComponent.ELEVENTH:
chordSymbol += '<sup>(no11)</sup>';
break;
default:
break;
}
});
}

return chordSymbol;
};

export const getChordId = (analysis: ChordAnalysis) => (
getHtmlChordSymbol(analysis)
.replace(/<sup>(.+?)<\/sup>/g, '^$1')
.replace(/♯/g, '#')
.replace(/♭/g, 'b')
);

export const getChordName = (analysis: ChordAnalysis) => {
const chordSymbol = [] as string[];

if (analysis.base === ChordBase.MINOR) {
chordSymbol.push('minor');
} else if (analysis.base === ChordBase.DIMINISHED) {
chordSymbol.push('diminished');
} else if (analysis.base === ChordBase.AUGMENTED) {
chordSymbol.push('augmented');
}

const {
extensions = [],
modifications = [],
omissions = [],
} = analysis;

if (extensions.length > 0) {
extensions.forEach((extension) => {
if (extension.component === ChordComponent.SIXTH) {
if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol.push('sixth');
}
}
if (extension.component === ChordComponent.SEVENTH) {
if (extension.type === ChordExtensionType.DOMINANT) {
chordSymbol.push('seventh');
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol.push('major seventh');
}

if (extension.type === ChordExtensionType.DIMINISHED) {
chordSymbol.push('seventh');
}
}

if (extension.component === ChordComponent.NINTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol.push('minor ninth');
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol.push('ninth');
}

if (extension.type === ChordExtensionType.AUGMENTED) {
chordSymbol.push('augmented ninth');
}
}

if (extension.component === ChordComponent.ELEVENTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol.push('minor eleventh');
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol.push('eleventh');
}
}

if (extension.component === ChordComponent.THIRTEENTH) {
if (extension.type === ChordExtensionType.MINOR) {
chordSymbol.push('minor thirteenth');
}

if (extension.type === ChordExtensionType.MAJOR) {
chordSymbol.push('thirteenth');
}

if (extension.type === ChordExtensionType.AUGMENTED) {
chordSymbol.push('augmented thirteenth');
}
}
});
}

if (modifications.length > 0) {
modifications.forEach((modification) => {
if (modification.component === ChordComponent.FIFTH) {
if (modification.type === ChordModificationType.LOWERED) {
chordSymbol.push('lowered fifth');
}
}

if (modification.component === ChordComponent.FOURTH) {
if (modification.type === ChordModificationType.SUSPENDED) {
chordSymbol.push('suspended fourth');
}
}

if (modification.component === ChordComponent.SECOND) {
if (modification.type === ChordModificationType.SUSPENDED) {
chordSymbol.push('suspended second');
}
}
});
}

if (omissions.length > 0) {
omissions.forEach((omission) => {
switch (omission) {
case ChordComponent.THIRD:
chordSymbol.push('no third');
break;
case ChordComponent.FIFTH:
chordSymbol.push('no fifth');
break;
case ChordComponent.SEVENTH:
chordSymbol.push('no seventh');
break;
case ChordComponent.NINTH:
chordSymbol.push('no ninth');
break;
case ChordComponent.ELEVENTH:
chordSymbol.push('no eleventh');
break;
default:
break;
}
});
}

return chordSymbol.join(' ');
};

+ 427
- 0
test/index.test.ts View File

@@ -0,0 +1,427 @@
import {
describe,
it,
expect,
beforeEach,
} from 'vitest';
import {
ChordBase,
ChordComponent,
ChordExtensionType,
ChordModificationType,
constructChord,
analyzeIntervals, getChordName,
} from '../src';

describe('constructChord', () => {
it('works', () => {
const chord = constructChord({ pitchClass: 0 });
expect(chord).toEqual([0, 4, 7]);
});
});

describe('analyzeChord', () => {
describe('building', () => {
let chord: number[];

beforeEach(() => {
chord = [];
});

describe('root', () => {
beforeEach(() => {
chord.push(0);
});

describe('major third', () => {
beforeEach(() => {
chord.push(4);
});

describe('perfect fifth', () => {
beforeEach(() => {
chord.push(7);
});

it('determines major chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
});
});

describe('major sixth', () => {
beforeEach(() => {
chord.push(9);
});

it('describes major sixth chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SIXTH,
}],
});
});
});

describe('minor seventh', () => {
beforeEach(() => {
chord.push(10);
});

it('describes dominant seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
extensions: [{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('major seventh', () => {
beforeEach(() => {
chord.push(11);
});

it('describes major seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
});
});
});
});

describe('diminished fifth', () => {
beforeEach(() => {
chord.push(6);
});

describe('minor seventh', () => {
beforeEach(() => {
chord.push(10);
});

it('describes dominant seventh lowered fifth chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
modifications: [{
type: ChordModificationType.LOWERED,
component: ChordComponent.FIFTH,
}],
extensions: [{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('major seventh', () => {
beforeEach(() => {
chord.push(11);
});

it('describes major seventh lowered fifth chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MAJOR,
modifications: [{
type: ChordModificationType.LOWERED,
component: ChordComponent.FIFTH,
}],
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
});
});
});
});

describe('augmented fifth', () => {
beforeEach(() => {
chord.push(8);
});

it('determines augmented chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.AUGMENTED,
});
});

describe('minor seventh', () => {
beforeEach(() => {
chord.push(10);
});

it('describes augmented dominant seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.AUGMENTED,
extensions: [{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('major seventh', () => {
beforeEach(() => {
chord.push(11);
});

it('describes augmented major seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.AUGMENTED,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
});
});
});
});
});

describe('minor third', () => {
beforeEach(() => {
chord.push(3);
});

describe('perfect fifth', () => {
beforeEach(() => {
chord.push(7);
});

it('determines minor chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MINOR,
});
});

describe('major sixth', () => {
beforeEach(() => {
chord.push(9);
});

it('describes minor sixth chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MINOR,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SIXTH,
}],
});
});
});

describe('minor seventh', () => {
beforeEach(() => {
chord.push(10);
});

it('describes minor seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MINOR,
extensions: [{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('major seventh', () => {
beforeEach(() => {
chord.push(11);
});

it('describes minor major seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.MINOR,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
});
});
});
});

describe('diminished fifth', () => {
beforeEach(() => {
chord.push(6);
});

it('determines diminished chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.DIMINISHED,
});
});

describe('diminished seventh', () => {
beforeEach(() => {
chord.push(9);
});

it('describes diminished seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.DIMINISHED,
extensions: [{
type: ChordExtensionType.DIMINISHED,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('minor seventh', () => {
beforeEach(() => {
chord.push(10);
});

it('describes diminished dominant seventh (half-diminished) chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.DIMINISHED,
extensions: [{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
}],
});
});
});

describe('major seventh', () => {
beforeEach(() => {
chord.push(11);
});

it('describes diminished major seventh chord', () => {
expect(analyzeIntervals(chord)).toEqual({
base: ChordBase.DIMINISHED,
extensions: [{
type: ChordExtensionType.MAJOR,
component: ChordComponent.SEVENTH,
}],
});
});
});
});
});
});
});

describe('extensions', () => {
it('analyzes complete ninth chords', () => {
expect(analyzeIntervals([0, 4, 7, 10, 14])).toEqual({
base: ChordBase.MAJOR,
extensions: [
{
type: ChordExtensionType.DOMINANT,
component: ChordComponent.SEVENTH,
},
{
type: ChordExtensionType.MAJOR,
component: ChordComponent.NINTH,
},
],
});
});

it('analyzes incomplete ninth chords', () => {
expect(analyzeIntervals([0, 4, 7, 14])).toEqual({
base: ChordBase.MAJOR,
extensions: [
{
type: ChordExtensionType.MAJOR,
component: ChordComponent.NINTH,
},
],
omissions: [ChordComponent.SEVENTH],
});

expect(analyzeIntervals([0, 4, 14])).toEqual({
base: ChordBase.MAJOR,
extensions: [
{
type: ChordExtensionType.MAJOR,
component: ChordComponent.NINTH,
},
],
omissions: [ChordComponent.FIFTH, ChordComponent.SEVENTH],
});
});
});

describe('naming', () => {
it('names major chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7]))).toBe('');
});

it('names minor chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 7]))).toBe('minor');
});

it('names augmented chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 8]))).toBe('augmented');
});

it('names diminished chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 6]))).toBe('diminished');
});

it('names major seventh chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 11]))).toBe('major seventh');
});

it('names minor seventh chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 7, 10]))).toBe('minor seventh');
});

it('names dominant seventh chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 10]))).toBe('seventh');
});

it('names major ninth chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 11, 14]))).toBe('major seventh ninth');
});

it('names minor ninth chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 7, 10, 14]))).toBe('minor seventh ninth');
});

it('names dominant ninth chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 10, 14]))).toBe('seventh ninth');
});

it('names major eleventh chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 11, 14, 18]))).toBe('major seventh ninth eleventh');
});

it('names minor eleventh chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 7, 10, 14, 18]))).toBe('minor seventh ninth eleventh');
});

it('names dominant eleventh chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 10, 14, 18]))).toBe('seventh ninth eleventh');
});

it('names major thirteenth chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 11, 14, 18, 21]))).toBe('major seventh ninth eleventh thirteenth');
});

it('names minor thirteenth chords', () => {
expect(getChordName(analyzeIntervals([0, 3, 7, 10, 14, 18, 21]))).toBe('minor seventh ninth eleventh thirteenth');
});

it('names dominant thirteenth chords', () => {
expect(getChordName(analyzeIntervals([0, 4, 7, 10, 14, 18, 21]))).toBe('seventh ninth eleventh thirteenth');
});
});
});

+ 21
- 0
tsconfig.eslint.json View File

@@ -0,0 +1,21 @@
{
"exclude": ["node_modules"],
"include": ["src", "types", "test"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018"
}
}

+ 21
- 0
tsconfig.json View File

@@ -0,0 +1,21 @@
{
"exclude": ["node_modules"],
"include": ["src", "types"],
"compilerOptions": {
"module": "ESNext",
"lib": ["ESNext"],
"importHelpers": true,
"declaration": true,
"sourceMap": true,
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"jsx": "react",
"esModuleInterop": true,
"target": "es2018"
}
}

+ 3569
- 0
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save