From b968b15f41d26de14043911e262320a0457c40c0 Mon Sep 17 00:00:00 2001 From: Kilian McMahon Date: Tue, 19 Sep 2023 01:40:11 +0200 Subject: [PATCH 1/5] feat: a massive reworking of the system --- src/chords/determineChordInfo.ts | 92 ++-- src/chords/getChord.test.ts | 125 +++--- src/chords/getChord.ts | 224 +++++----- src/chords/getScaleChords.ts | 17 +- src/chords/helpers.ts | 41 +- src/consts.ts | 126 +++++- src/db/scales/allScales.ts | 567 +++++++++++++++++++++++++ src/db/scales/major.ts | 62 +++ src/helper.ts | 10 +- src/keys/getKey.test.ts | 44 +- src/keys/getKey.ts | 45 +- src/keys/helpers.ts | 2 +- src/modes/getName.ts | 12 - src/modes/guessMode.test.ts | 7 + src/modes/guessMode.ts | 1 + src/modes/helpers.ts | 10 +- src/notes/determineNoteType.test.ts | 14 + src/notes/determineNoteType.ts | 12 + src/notes/getEquivalentNote.test.ts | 38 +- src/notes/getEquivalentNote.ts | 38 +- src/notes/getInterval.test.ts | 8 +- src/notes/getInterval.ts | 28 +- src/notes/getScaleDegreeOfNote.test.ts | 2 +- src/notes/getScaleDegreeOfNote.ts | 17 +- src/notes/helpers.ts | 30 +- src/notes/intervalToSemitone.test.ts | 20 +- src/notes/intervalToSemitone.ts | 19 +- src/notes/noteToInteger.test.ts | 14 + src/notes/noteToInteger.ts | 5 + src/notes/noteToMidi.test.ts | 21 + src/notes/noteToMidi.ts | 29 ++ src/notes/transpose.test.ts | 32 +- src/notes/transpose.ts | 115 +++-- src/scale/extractName.ts | 35 +- src/scale/getMajorFromMode.ts | 10 +- src/scale/getScale.test.ts | 166 ++++++-- src/scale/getScale.ts | 63 ++- src/scale/getScaleDegrees.test.ts | 18 + src/scale/getScaleDegrees.ts | 19 + src/scale/guessScale.test.ts | 30 +- src/scale/guessScale.ts | 74 ++-- src/scale/helpers.ts | 26 ++ src/scale/isValidScale.test.ts | 10 +- src/scale/isValidScale.ts | 10 +- src/scale/scaleToMidiKeys.test.ts | 206 +++++++++ src/scale/scaleToMidiKeys.ts | 11 + src/scale/scaleToMidiNumbers.test.ts | 10 + src/scale/scaleToMidiNumbers.ts | 15 + src/types.ts | 183 ++++---- 49 files changed, 2066 insertions(+), 647 deletions(-) create mode 100644 src/db/scales/allScales.ts create mode 100644 src/db/scales/major.ts delete mode 100644 src/modes/getName.ts create mode 100644 src/modes/guessMode.test.ts create mode 100644 src/modes/guessMode.ts create mode 100644 src/notes/determineNoteType.test.ts create mode 100644 src/notes/determineNoteType.ts create mode 100644 src/notes/noteToInteger.test.ts create mode 100644 src/notes/noteToInteger.ts create mode 100644 src/notes/noteToMidi.test.ts create mode 100644 src/notes/noteToMidi.ts create mode 100644 src/scale/getScaleDegrees.test.ts create mode 100644 src/scale/getScaleDegrees.ts create mode 100644 src/scale/helpers.ts create mode 100644 src/scale/scaleToMidiKeys.test.ts create mode 100644 src/scale/scaleToMidiKeys.ts create mode 100644 src/scale/scaleToMidiNumbers.test.ts create mode 100644 src/scale/scaleToMidiNumbers.ts diff --git a/src/chords/determineChordInfo.ts b/src/chords/determineChordInfo.ts index 27dbc6d..41d81c1 100644 --- a/src/chords/determineChordInfo.ts +++ b/src/chords/determineChordInfo.ts @@ -1,68 +1,76 @@ -import { numberTypeChordMap } from '../consts'; -import type { IChordInfo, TChordQuality, TChordType, TSusType } from '../types'; -import { chordRegexp, isAddType } from './helpers'; +import { numberTypeChordMap } from '../consts.js'; +import { determineNoteType } from '../notes/determineNoteType.js'; +import type { + IChordInfo, + TAddType, + TChordQuality, + TChordType, + TSusType, +} from '../types.js'; +import { chordRegexp, isAddType } from './helpers.js'; const determineAlteredNotes = (alteredNotes?: string) => { - return alteredNotes === undefined - ? undefined - : (alteredNotes.match(/(#|b)(?:5|7|9|11|13)/g) as string[]); + return alteredNotes === undefined + ? undefined + : (alteredNotes.match(/(#|b)(?:5|7|9|11|13)/g) as string[]); }; const determineChordQuality = (quality?: string): TChordQuality => { - if (quality === undefined || ['Δ', 'M', 'maj'].includes(quality)) { - return 'major'; - } + if (quality === undefined || ['Δ', 'M', 'maj'].includes(quality)) { + return 'major'; + } - if (quality === 'm') { - return 'minor'; - } + if (quality === 'm') { + return 'minor'; + } - if (['dim', 'o'].includes(quality)) { - return 'diminished'; - } + if (['dim', 'o'].includes(quality)) { + return 'diminished'; + } - if (['ø'].includes(quality)) { - return 'half-diminished'; - } + if (['ø'].includes(quality)) { + return 'half-diminished'; + } - if (['aug', '+'].includes(quality)) { - return 'augmented'; - } + if (['aug', '+'].includes(quality)) { + return 'augmented'; + } - return 'major'; + return 'major'; }; const determindChordType = (type?: string): TChordType => { - return type === undefined ? 'triad' : numberTypeChordMap[type]; + return type === undefined ? 'triad' : numberTypeChordMap[type]; }; -const determineAdd = (add?: string) => { - return add && isAddType(add) ? add : undefined; +const determineAdd = (add?: string): TAddType | undefined => { + return add && isAddType(add) ? add : undefined; }; const determineSus = (sus?: string): TSusType | undefined => { - if (sus === undefined) return undefined; - if (['sus', 'sus4', 'sus9'].includes(sus)) return 'sus4'; - if (sus === 'sus2') return 'sus2'; - return undefined; + if (sus === undefined) return undefined; + if (['sus', 'sus4', 'sus9'].includes(sus)) return 'sus4'; + if (sus === 'sus2') return 'sus2'; + return undefined; }; const determineSlashChord = (slashNote?: string) => { - return slashNote?.replace('/', ''); + return slashNote?.replace('/', ''); }; export const determineChord = (name: string): IChordInfo => { - const [note, quality, type, altered, add, sus, slashNote] = - name.match(chordRegexp)?.slice(1) || []; + const [note, quality, type, altered, add, sus, slashNote] = + name.match(chordRegexp)?.slice(1) || []; - return { - name, - note, - quality: determineChordQuality(quality), - type: determindChordType(type), - alteredNotes: determineAlteredNotes(altered), - addType: determineAdd(add), - susType: determineSus(sus), - slashNote: determineSlashChord(slashNote) - }; + return { + name, + note, + quality: determineChordQuality(quality), + type: determindChordType(type), + alteredNotes: determineAlteredNotes(altered), + addType: determineAdd(add), + susType: determineSus(sus), + slashNote: determineSlashChord(slashNote), + pitchClassType: determineNoteType(name), + }; }; diff --git a/src/chords/getChord.test.ts b/src/chords/getChord.test.ts index 6b26b7a..4e20b5b 100644 --- a/src/chords/getChord.test.ts +++ b/src/chords/getChord.test.ts @@ -1,72 +1,75 @@ import { describe, expect, it } from 'vitest'; -import { getChord } from './getChord'; +import { getChord } from './getChord.js'; describe('getChord', () => { - it('supports major chords', () => { - expect(getChord('C').notes).toEqual(['C', 'E', 'G']); - expect(getChord('Eb').notes).toEqual(['Eb', 'G', 'Bb']); - }); + it('supports major chords', () => { + expect(getChord('C').notes).toEqual(['C', 'E', 'G']); + expect(getChord('Eb').notes).toEqual(['Eb', 'G', 'Bb']); + }); - it('supports minor chords', () => { - expect(getChord('Am').notes).toEqual(['A', 'C', 'E']); - expect(getChord('D#m').notes).toEqual(['D#', 'F#', 'A#']); - }); + it.only('supports minor chords', () => { + expect(getChord('Am').notes).toEqual(['A', 'C', 'E']); + expect(getChord('D#m').notes).toEqual(['D#', 'F#', 'A#']); + }); - it('supports augmented chords', () => { - expect(getChord('Caug').notes).toEqual(['C', 'E', 'G#']); - expect(getChord('C#aug').notes).toEqual(['C#', 'F', 'A']); - expect(getChord('Bbaug').notes).toEqual(['Bb', 'D', 'F#']); - expect(getChord('Aaug7').notes).toEqual(['A', 'Db', 'F', 'G']); - }); + it('supports augmented chords', () => { + expect(getChord('Caug').notes).toEqual(['C', 'E', 'G#']); + expect(getChord('C#aug').notes).toEqual(['C#', 'F', 'A']); + expect(getChord('Bbaug').notes).toEqual(['Bb', 'D', 'F#']); + expect(getChord('Aaug7').notes).toEqual(['A', 'Db', 'F', 'G']); + }); - it('supports diminished chords', () => { - expect(getChord('Cdim').notes).toEqual(['C', 'Eb', 'Gb']); - expect(getChord('C#dim').notes).toEqual(['C#', 'E', 'G']); - expect(getChord('Dbdim').notes).toEqual(['Db', 'E', 'G']); - expect(getChord('Ddim').notes).toEqual(['D', 'F', 'Ab']); - expect(getChord('D#dim').notes).toEqual(['D#', 'F#', 'A']); - expect(getChord('Ebdim').notes).toEqual(['Eb', 'Gb', 'A']); - expect(getChord('Edim').notes).toEqual(['E', 'G', 'Bb']); - expect(getChord('Fdim').notes).toEqual(['F', 'Ab', 'B']); - expect(getChord('F#dim').notes).toEqual(['F#', 'A', 'C']); - expect(getChord('Gbdim').notes).toEqual(['Gb', 'A', 'C']); - expect(getChord('Gdim').notes).toEqual(['G', 'Bb', 'Db']); - expect(getChord('G#dim').notes).toEqual(['G#', 'B', 'D']); - expect(getChord('Abdim').notes).toEqual(['Ab', 'B', 'D']); - expect(getChord('Adim').notes).toEqual(['A', 'C', 'Eb']); - expect(getChord('A#dim').notes).toEqual(['A#', 'C#', 'E']); - expect(getChord('Bbdim').notes).toEqual(['Bb', 'Db', 'E']); - expect(getChord('Bdim').notes).toEqual(['B', 'D', 'F']); - }); + it('supports diminished chords', () => { + expect(getChord('Cdim').notes).toEqual(['C', 'Eb', 'Gb']); + expect(getChord('C#dim').notes).toEqual(['C#', 'E', 'G']); + expect(getChord('Dbdim').notes).toEqual(['Db', 'E', 'G']); + expect(getChord('Ddim').notes).toEqual(['D', 'F', 'Ab']); + expect(getChord('D#dim').notes).toEqual(['D#', 'F#', 'A']); + expect(getChord('Ebdim').notes).toEqual(['Eb', 'Gb', 'A']); + expect(getChord('Edim').notes).toEqual(['E', 'G', 'Bb']); + expect(getChord('Fdim').notes).toEqual(['F', 'Ab', 'B']); + expect(getChord('F#dim').notes).toEqual(['F#', 'A', 'C']); + expect(getChord('Gbdim').notes).toEqual(['Gb', 'A', 'C']); + expect(getChord('Gdim').notes).toEqual(['G', 'Bb', 'Db']); + expect(getChord('G#dim').notes).toEqual(['G#', 'B', 'D']); + expect(getChord('Abdim').notes).toEqual(['Ab', 'B', 'D']); + expect(getChord('Adim').notes).toEqual(['A', 'C', 'Eb']); + expect(getChord('A#dim').notes).toEqual(['A#', 'C#', 'E']); + expect(getChord('Bbdim').notes).toEqual(['Bb', 'Db', 'E']); + expect(getChord('Bdim').notes).toEqual(['B', 'D', 'F']); + }); - it('supports modified notes', () => { - expect(getChord('C7b5').notes).toEqual(['C', 'E', 'Gb', 'Bb']); - expect(getChord('C7#5').notes).toEqual(['C', 'E', 'G#', 'Bb']); - expect(getChord('Cm7#5').notes).toEqual(['C', 'Eb', 'G#', 'Bb']); - expect(getChord('C7b9').notes).toEqual(['C', 'E', 'G', 'Bb', 'Db']); - expect(getChord('C7#9').notes).toEqual(['C', 'E', 'G', 'Bb', 'D#']); - expect(getChord('C7b5b9').notes).toEqual(['C', 'E', 'Gb', 'Bb', 'Db']); - expect(getChord('C7#5#9').notes).toEqual(['C', 'E', 'G#', 'Bb', 'D#']); - expect(getChord('C7b5#9').notes).toEqual(['C', 'E', 'Gb', 'Bb', 'D#']); - expect(getChord('C7#5b9').notes).toEqual(['C', 'E', 'G#', 'Bb', 'Db']); - }); + it('supports modified notes', () => { + expect(getChord('C7b5').notes).toEqual(['C', 'E', 'Gb', 'Bb']); + expect(getChord('C7#5').notes).toEqual(['C', 'E', 'G#', 'Bb']); + expect(getChord('Cm7#5').notes).toEqual(['C', 'Eb', 'G#', 'Bb']); + expect(getChord('C7b9').notes).toEqual(['C', 'E', 'G', 'Bb', 'Db']); + expect(getChord('C7#9').notes).toEqual(['C', 'E', 'G', 'Bb', 'D#']); + expect(getChord('C7b5b9').notes).toEqual(['C', 'E', 'Gb', 'Bb', 'Db']); + expect(getChord('C7#5#9').notes).toEqual(['C', 'E', 'G#', 'Bb', 'D#']); + expect(getChord('C7b5#9').notes).toEqual(['C', 'E', 'Gb', 'Bb', 'D#']); + expect(getChord('C7#5b9').notes).toEqual(['C', 'E', 'G#', 'Bb', 'Db']); + }); - it('supports add chords', () => { - expect(getChord('Cadd2').notes).toEqual(['C', 'D', 'E', 'G']); - expect(getChord('Cadd4').notes).toEqual(['C', 'E', 'F', 'G']); - expect(getChord('Cadd9').notes).toEqual(['C', 'E', 'G', 'D']); - expect(getChord('Cadd11').notes).toEqual(['C', 'E', 'G', 'F']); - expect(getChord('Cadd13').notes).toEqual(['C', 'E', 'G', 'A']); - }); + it('supports add chords', () => { + expect(getChord('Cadd2').notes).toEqual(['C', 'D', 'E', 'G']); + expect(getChord('Cadd4').notes).toEqual(['C', 'E', 'F', 'G']); + expect(getChord('Cadd9').notes).toEqual(['C', 'E', 'G', 'D']); + expect(getChord('Cadd11').notes).toEqual(['C', 'E', 'G', 'F']); + expect(getChord('Cadd13').notes).toEqual(['C', 'E', 'G', 'A']); + }); - it('supports slash chords (chord inversions)', () => { - expect(getChord('C/E').notes).toEqual(['E', 'G', 'C']); - expect(getChord('C/G').notes).toEqual(['G', 'C', 'E']); - expect(getChord('Eb/G').notes).toEqual(['G', 'Bb', 'Eb']); - expect(getChord('Eb/Bb').notes).toEqual(['Bb', 'Eb', 'G']); - }); + it('supports slash chords (chord inversions)', () => { + expect(getChord('C/E').notes).toEqual(['E', 'G', 'C']); + expect(getChord('C/G').notes).toEqual(['G', 'C', 'E']); + expect(getChord('Eb/G').notes).toEqual(['G', 'Bb', 'Eb']); + expect(getChord('Eb/Bb').notes).toEqual(['Bb', 'Eb', 'G']); + }); - // it('supports weird chords', () => { - // expect(getChord('Cø7').notes).toEqual(['C', 'Eb', 'Gb', 'Bb']); - // }); + // it('supports weird chords', () => { + // expect(getChord('Cø7').notes).toEqual(['C', 'Eb', 'Gb', 'Bb']); + // }); + // it('supports constraining to the notes in a scale', () => { + + // }) }); diff --git a/src/chords/getChord.ts b/src/chords/getChord.ts index a85b64d..e91e817 100644 --- a/src/chords/getChord.ts +++ b/src/chords/getChord.ts @@ -1,114 +1,124 @@ -import { chordQualityIntervalsMap } from '../consts'; -import { offsetArr } from '../helper'; -import { transposeNote } from '../notes/transpose'; -import type { IChord, IChordInfo, TIntervalShorthand } from '../types'; -import { determineChord } from './determineChordInfo'; +import { chordQualityIntervalsMap } from '../consts.js'; +import { offsetArr } from '../helper.js'; +import { determineNoteType } from '../notes/determineNoteType.js'; +import { transposeNote } from '../notes/transpose.js'; +import type { IChord, IChordInfo, TIntervalShorthand } from '../types.js'; +import { determineChord } from './determineChordInfo.js'; const chordInfoToIntervalMap = ({ - addType, - alteredNotes, - name, - type, - quality, - susType + addType, + alteredNotes, + name, + type, + quality, + susType, }: IChordInfo): TIntervalShorthand[] => { - let intervals: TIntervalShorthand[] = chordQualityIntervalsMap[quality]; - - if (type === 'fifth') { - intervals = intervals.filter((interval) => !interval.includes('3')); - intervals = intervals.concat('P8'); - } - - if (type === 'seventh') { - intervals = intervals.concat('m7'); - } - - if (susType !== undefined) { - intervals = intervals.map((interval) => - interval.includes('3') ? (susType === 'sus2' ? 'M2' : 'P4') : interval - ); - } - - if (type === 'ninth' || name.includes('sus9')) { - intervals = intervals.concat('m7', 'M9'); - } - - if (type === 'eleventh') { - intervals = intervals.concat('m7', 'M9', 'P11'); - } - - if (type === 'thirteenth') { - intervals = intervals.concat('m7', 'M9', 'P11', 'M13'); - } - - if (alteredNotes) { - if (alteredNotes.includes('b5')) { - intervals = intervals.map((interval) => (interval.includes('P5') ? 'd5' : interval)); - } - if (alteredNotes.includes('#5')) { - intervals = intervals.map((interval) => (interval.includes('P5') ? 'A5' : interval)); - } - - if (alteredNotes.includes('b9')) { - intervals = intervals.concat('m9'); - } - if (alteredNotes.includes('#9')) { - intervals = intervals.concat('A9'); - } - } - - if (addType) { - if (addType.includes('2')) { - intervals = [...intervals.slice(0, 1), 'M2', ...intervals.slice(1)]; - } - if (addType.includes('4')) { - intervals = [...intervals.slice(0, 2), 'P4', ...intervals.slice(2)]; - } - if (addType.includes('9')) { - intervals = intervals.concat('M9'); - } - if (addType.includes('11')) { - intervals = intervals.concat('P11'); - } - if (addType.includes('13')) { - intervals = intervals.concat('M13'); - } - } - - return intervals; + let intervals: TIntervalShorthand[] = chordQualityIntervalsMap[quality]; + + if (type === 'fifth') { + intervals = intervals.filter((interval) => !interval.includes('3')); + intervals = intervals.concat('P8'); + } + + if (type === 'seventh') { + intervals = intervals.concat('m7'); + } + + if (susType !== undefined) { + intervals = intervals.map((interval) => + interval.includes('3') ? (susType === 'sus2' ? 'M2' : 'P4') : interval + ); + } + + if (type === 'ninth' || name.includes('sus9')) { + intervals = intervals.concat('m7', 'M9'); + } + + if (type === 'eleventh') { + intervals = intervals.concat('m7', 'M9', 'P11'); + } + + if (type === 'thirteenth') { + intervals = intervals.concat('m7', 'M9', 'P11', 'M13'); + } + + if (alteredNotes) { + if (alteredNotes.includes('b5')) { + intervals = intervals.map((interval) => + interval.includes('P5') ? 'd5' : interval + ); + } + if (alteredNotes.includes('#5')) { + intervals = intervals.map((interval) => + interval.includes('P5') ? 'A5' : interval + ); + } + + if (alteredNotes.includes('b9')) { + intervals = intervals.concat('m9'); + } + if (alteredNotes.includes('#9')) { + intervals = intervals.concat('A9'); + } + } + + if (addType) { + if (addType.includes('2')) { + intervals = [...intervals.slice(0, 1), 'M2', ...intervals.slice(1)]; + } + if (addType.includes('4')) { + intervals = [...intervals.slice(0, 2), 'P4', ...intervals.slice(2)]; + } + if (addType.includes('9')) { + intervals = intervals.concat('M9'); + } + if (addType.includes('11')) { + intervals = intervals.concat('P11'); + } + if (addType.includes('13')) { + intervals = intervals.concat('M13'); + } + } + + return intervals; }; export const getChord = (name: string): IChord => { - if (name === '') { - return { - name: `Type a chord`, - notes: [] - }; - } - - const chordInfo = determineChord(name); - const finalIntervals = chordInfoToIntervalMap(chordInfo); - - if (finalIntervals === undefined) { - return { - name: `${name} is not supported yet`, - notes: [] - }; - } - - let notes = finalIntervals.map((interval) => transposeNote(chordInfo.note, interval)); - const { slashNote } = chordInfo; - - if (slashNote) { - const triad = notes.slice(0, 3); - if (triad.includes(slashNote)) { - const rootNoteIndex = triad.indexOf(slashNote); - notes = offsetArr(notes, rootNoteIndex); - } - } - - return { - name, - notes - }; + if (name === '') { + return { + name: `Type a chord`, + notes: [], + }; + } + + const chordInfo = determineChord(name); + const finalIntervals = chordInfoToIntervalMap(chordInfo); + + if (finalIntervals === undefined) { + return { + name: `${name} is not a supported chord`, + notes: [], + }; + } + + let notes = finalIntervals.map((interval) => { + return transposeNote(chordInfo.note, interval, { + forceFlat: chordInfo.pitchClassType === 'flat', + forceSharp: chordInfo.pitchClassType === 'sharp', + }); + }); + const { slashNote } = chordInfo; + + if (slashNote) { + const triad = notes.slice(0, 3); + if (triad.includes(slashNote)) { + const rootNoteIndex = triad.indexOf(slashNote); + notes = offsetArr(notes, rootNoteIndex); + } + } + + return { + name, + notes, + }; }; diff --git a/src/chords/getScaleChords.ts b/src/chords/getScaleChords.ts index 21094a1..ab41fff 100644 --- a/src/chords/getScaleChords.ts +++ b/src/chords/getScaleChords.ts @@ -1,11 +1,14 @@ -import { majorScaleQualities, modes } from '../consts'; -import { offsetArr } from '../helper'; -import type { TMode } from '../types'; -import { getChord } from './getChord'; -import { scaleQualitiesToChordSymbol } from './helpers'; +import { majorScaleQualities, modes, scaleTypes } from '../consts.js'; +import { offsetArr } from '../helper.js'; +import type { TMode, TScaleType } from '../types.js'; +import { getChord } from './getChord.js'; +import { scaleQualitiesToChordSymbol } from './helpers.js'; -export const getScaleChords = (scale: string[], mode: TMode) => { - const scaleQualities = offsetArr(majorScaleQualities, modes.indexOf(mode)); +export const getScaleChords = (scale: string[], mode: TScaleType) => { + const scaleQualities = offsetArr( + majorScaleQualities, + scaleTypes.indexOf(mode) + ); return scale.map((note, i) => { const quality = scaleQualitiesToChordSymbol(scaleQualities[i]); diff --git a/src/chords/helpers.ts b/src/chords/helpers.ts index df3278a..044a634 100644 --- a/src/chords/helpers.ts +++ b/src/chords/helpers.ts @@ -1,27 +1,38 @@ -import { addTypes, susTypes } from '../consts'; -import type { TAddType, TChordQuality, TSusType } from '../types'; +import { addTypes, susTypes } from '../consts.js'; +import type { + ScaleQualities, + TAddType, + TChordQuality, + TSusType, +} from '../types.js'; export function isAddType(value: string): value is TAddType { - return addTypes.includes(value as TAddType); + return addTypes.includes(value as TAddType); } export function isSusType(value: string): value is TSusType { - return susTypes.includes(value as TSusType); + return susTypes.includes(value as TSusType); } export const chordRegexp = new RegExp( - /((?:^[A-G])(?:#|b)?)(aug|dim|maj|m|M|o|\+)?(2|4|5|6|7|9|11|13)?((?:(?:#|b)(?:5|7|9|11|13))+)?(add(?:2|4|9|11|13)?)?(sus(?:2|4|9)?)?(\/[A-G](?:#|b)?)?/ + /((?:^[A-G])(?:#|b)?)(aug|dim|maj|m|M|o|\+)?(2|4|5|6|7|9|11|13)?((?:(?:#|b)(?:5|7|9|11|13))+)?(add(?:2|4|9|11|13)?)?(sus(?:2|4|9)?)?(\/[A-G](?:#|b)?)?/ ); export const scaleQualitiesToChordSymbol = (quality: TChordQuality) => { - switch (quality) { - case 'major': - return ''; - case 'minor': - return 'm'; - case 'diminished': - return 'dim'; - default: - return 'major'; - } + switch (quality) { + case 'major': + return ''; + case 'minor': + return 'm'; + case 'diminished': + return 'dim'; + case 'augmented': + return 'aug'; + default: + return 'major'; + } +}; + +export const romanNumeralCase = (numeral: string, quality: ScaleQualities) => { + return quality === 'major' ? numeral : numeral.toLowerCase(); }; diff --git a/src/consts.ts b/src/consts.ts index 780e82e..744742b 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -3,7 +3,7 @@ import type { TChordType, TIntervalShorthand, TMode, -} from './types'; +} from './types.js'; export const intervalsMap: Record = { d2: 0, @@ -95,35 +95,62 @@ export const chordQualityIntervalsMap: Record = { diminished: ['P1', 'm3', 'd5'], }; -export const scaleIntervals: Record = { - ionian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'M7', 'P8'], +export const scaleIntervals = { + major: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'M7', 'P8'], dorian: ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'm7', 'P8'], phrygian: ['P1', 'm2', 'm3', 'P4', 'P5', 'm6', 'm7', 'P8'], lydian: ['P1', 'M2', 'M3', 'A4', 'P5', 'M6', 'M7', 'P8'], - myxolydian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'm7', 'P8'], - aeolian: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'm7', 'P8'], + mixolydian: ['P1', 'M2', 'M3', 'P4', 'P5', 'M6', 'm7', 'P8'], + minor: ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'm7', 'P8'], + locrian: ['P1', 'm2', 'm3', 'P4', 'd5', 'm6', 'm7', 'P8'], +} as const; + +type ScaleType = keyof typeof scaleIntervals; +export const scaleTypes = Object.keys(scaleIntervals) as ScaleType[]; + +const futureScaleIntervals = { 'harmonic-minor': ['P1', 'M2', 'm3', 'P4', 'P5', 'm6', 'M7', 'P8'], 'melodic-minor': ['P1', 'M2', 'm3', 'P4', 'P5', 'M6', 'M7', 'P8'], - locrian: ['P1', 'm2', 'm3', 'P4', 'd5', 'm6', 'm7', 'P8'], 'dorian-b2': ['P1', 'm2', 'm3', 'P4', 'P5', 'M6', 'm7', 'P8'], }; +export type NotePosition = { + natural?: string; + sharp?: string; + flat?: string; +}; + +export const notePositions2: NotePosition[] = [ + { natural: 'A' }, + { sharp: 'A#', flat: 'Bb' }, + { natural: 'B', flat: 'Cb' }, + { natural: 'C', sharp: 'B#' }, + { sharp: 'C#', flat: 'Db' }, + { natural: 'D' }, + { sharp: 'D#', flat: 'Eb' }, + { natural: 'E', flat: 'Fb' }, + { natural: 'F', sharp: 'E#' }, + { sharp: 'F#', flat: 'Gb' }, + { natural: 'G' }, + { sharp: 'G#', flat: 'Ab' }, +]; + export const notePositions = [ ['A'], ['A#', 'Bb'], - ['B'], - ['C'], + ['B', 'Cb'], + ['C', 'B#'], ['C#', 'Db'], ['D'], ['D#', 'Eb'], - ['E'], - ['F'], + ['E', 'Fb'], + ['F', 'E#'], ['F#', 'Gb'], ['G'], ['G#', 'Ab'], ]; -export const majorScales: Record = { +export const oldMajorScales: Record = { C: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], F: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], Bb: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], @@ -146,7 +173,7 @@ export const modes: TMode[] = [ 'dorian', 'phrygian', 'lydian', - 'myxolydian', + 'mixolydian', 'aeolian', 'locrian', ]; @@ -160,15 +187,15 @@ export const majorScaleQualities: TChordQuality[] = [ 'diminished', ]; -export const majorScaleRomanNumerals = [ +export const romanNumerals = [ 'I', - 'ii', - 'iii', + 'II', + 'III', 'IV', 'V', - 'vi', - 'vii', -]; + 'VI', + 'VII', +] as const; export const addTypes = ['add2', 'add4', 'add9', 'add11', 'add13']; export const susTypes = ['sus2', 'sus4']; @@ -183,3 +210,66 @@ export const numberTypeChordMap: Record = { 11: 'eleventh', 13: 'thirteenth', }; + +export const majorScales = { + C: { + midiNumbers: [0, 2, 4, 5, 7, 9, 11], + notes: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], + }, + F: { + midiNumbers: [5, 7, 9, 10, 12, 14, 16], + notes: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], + }, + Bb: { + midiNumbers: [10, 12, 14, 15, 17, 19, 21], + notes: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], + }, + Eb: { + midiNumbers: [3, 5, 7, 8, 10, 12, 14], + notes: ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D'], + }, + Ab: { + midiNumbers: [8, 10, 12, 13, 15, 17, 19], + notes: ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G'], + }, + Db: { + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + notes: ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'], + }, + Gb: { + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + notes: ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F'], + }, + Cb: { + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + notes: ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'], + }, + G: { + midiNumbers: [7, 9, 11, 12, 14, 16, 18], + notes: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'], + }, + D: { + midiNumbers: [2, 4, 6, 7, 9, 11, 13], + notes: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'], + }, + A: { + midiNumbers: [9, 11, 13, 14, 16, 18, 20], + notes: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'], + }, + E: { + midiNumbers: [4, 6, 8, 9, 11, 13, 15], + notes: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'], + }, + B: { + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + notes: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'], + }, + 'F#': { + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + notes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E#'], + }, + 'C#': { + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + notes: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], + }, +}; diff --git a/src/db/scales/allScales.ts b/src/db/scales/allScales.ts new file mode 100644 index 0000000..784b0be --- /dev/null +++ b/src/db/scales/allScales.ts @@ -0,0 +1,567 @@ +export type ScaleInfo = { + notes: string[]; + midiNumbers: number[]; + integers: number[]; +}; + +export const major: Record = { + C: { + notes: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], + midiNumbers: [0, 2, 4, 5, 7, 9, 11], + integers: [0, 2, 4, 5, 7, 9, 11], + }, + F: { + notes: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], + midiNumbers: [5, 7, 9, 10, 12, 14, 16], + integers: [5, 7, 9, 10, 0, 2, 4], + }, + Bb: { + notes: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], + midiNumbers: [10, 12, 14, 15, 17, 19, 21], + integers: [10, 0, 2, 3, 5, 7, 9], + }, + Eb: { + notes: ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D'], + midiNumbers: [3, 5, 7, 8, 10, 12, 14], + integers: [3, 5, 7, 8, 10, 0, 2], + }, + Ab: { + notes: ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G'], + midiNumbers: [8, 10, 12, 13, 15, 17, 19], + integers: [8, 10, 0, 1, 3, 5, 7], + }, + Db: { + notes: ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'], + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + integers: [1, 3, 5, 6, 8, 10, 0], + }, + Gb: { + notes: ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F'], + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + integers: [6, 8, 10, 11, 1, 3, 5], + }, + Cb: { + notes: ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'], + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + integers: [11, 1, 3, 4, 6, 8, 10], + }, + G: { + notes: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'], + midiNumbers: [7, 9, 11, 12, 14, 16, 18], + integers: [7, 9, 11, 0, 2, 4, 6], + }, + D: { + notes: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'], + midiNumbers: [2, 4, 6, 7, 9, 11, 13], + integers: [2, 4, 6, 7, 9, 11, 1], + }, + A: { + notes: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'], + midiNumbers: [9, 11, 13, 14, 16, 18, 20], + integers: [9, 11, 1, 2, 4, 6, 8], + }, + E: { + notes: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'], + midiNumbers: [4, 6, 8, 9, 11, 13, 15], + integers: [4, 6, 8, 9, 11, 1, 3], + }, + B: { + notes: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'], + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + integers: [11, 1, 3, 4, 6, 8, 10], + }, + 'F#': { + notes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E#'], + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + integers: [6, 8, 10, 11, 1, 3, 5], + }, + 'C#': { + notes: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + integers: [1, 3, 5, 6, 8, 10, 0], + }, +}; + +export const dorian: Record = { + D: { + notes: ['D', 'E', 'F', 'G', 'A', 'B', 'C'], + midiNumbers: [2, 4, 5, 7, 9, 11, 12], + integers: [2, 4, 5, 7, 9, 11, 0], + }, + G: { + notes: ['G', 'A', 'Bb', 'C', 'D', 'E', 'F'], + midiNumbers: [7, 9, 10, 12, 14, 16, 17], + integers: [7, 9, 10, 0, 2, 4, 5], + }, + C: { + notes: ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb'], + midiNumbers: [0, 2, 3, 5, 7, 9, 10], + integers: [0, 2, 3, 5, 7, 9, 10], + }, + F: { + notes: ['F', 'G', 'Ab', 'Bb', 'C', 'D', 'Eb'], + midiNumbers: [5, 7, 8, 10, 12, 14, 15], + integers: [5, 7, 8, 10, 0, 2, 3], + }, + Bb: { + notes: ['Bb', 'C', 'Db', 'Eb', 'F', 'G', 'Ab'], + midiNumbers: [10, 12, 13, 15, 17, 19, 20], + integers: [10, 0, 1, 3, 5, 7, 8], + }, + Eb: { + notes: ['Eb', 'F', 'Gb', 'Ab', 'Bb', 'C', 'Db'], + midiNumbers: [3, 5, 6, 8, 10, 12, 13], + integers: [3, 5, 6, 8, 10, 0, 1], + }, + Ab: { + notes: ['Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F', 'Gb'], + midiNumbers: [8, 10, 11, 13, 15, 17, 18], + integers: [8, 10, 11, 1, 3, 5, 6], + }, + Db: { + notes: ['Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb', 'Cb'], + midiNumbers: [1, 3, 4, 6, 8, 10, 11], + integers: [1, 3, 4, 6, 8, 10, 11], + }, + A: { + notes: ['A', 'B', 'C', 'D', 'E', 'F#', 'G'], + midiNumbers: [9, 11, 12, 14, 16, 18, 19], + integers: [9, 11, 0, 2, 4, 6, 7], + }, + E: { + notes: ['E', 'F#', 'G', 'A', 'B', 'C#', 'D'], + midiNumbers: [4, 6, 7, 9, 11, 13, 14], + integers: [4, 6, 7, 9, 11, 1, 2], + }, + B: { + notes: ['B', 'C#', 'D', 'E', 'F#', 'G#', 'A'], + midiNumbers: [11, 13, 14, 16, 18, 20, 21], + integers: [11, 1, 2, 4, 6, 8, 9], + }, + 'F#': { + notes: ['F#', 'G#', 'A', 'B', 'C#', 'D#', 'E'], + midiNumbers: [6, 8, 9, 11, 13, 15, 16], + integers: [6, 8, 9, 11, 1, 3, 4], + }, + 'C#': { + notes: ['C#', 'D#', 'E', 'F#', 'G#', 'A#', 'B'], + midiNumbers: [1, 3, 4, 6, 8, 10, 11], + integers: [1, 3, 4, 6, 8, 10, 11], + }, + 'G#': { + notes: ['G#', 'A#', 'B', 'C#', 'D#', 'E#', 'F#'], + midiNumbers: [8, 10, 11, 13, 15, 17, 18], + integers: [8, 10, 11, 1, 3, 5, 6], + }, + 'D#': { + notes: ['D#', 'E#', 'F#', 'G#', 'A#', 'B#', 'C#'], + midiNumbers: [3, 5, 6, 8, 10, 12, 13], + integers: [3, 5, 6, 8, 10, 0, 1], + }, +}; + +export const phrygian: Record = { + E: { + notes: ['E', 'F', 'G', 'A', 'B', 'C', 'D'], + midiNumbers: [4, 5, 7, 9, 11, 12, 14], + integers: [4, 5, 7, 9, 11, 0, 2], + }, + A: { + notes: ['A', 'Bb', 'C', 'D', 'E', 'F', 'G'], + midiNumbers: [9, 10, 12, 14, 16, 17, 19], + integers: [9, 10, 0, 2, 4, 5, 7], + }, + D: { + notes: ['D', 'Eb', 'F', 'G', 'A', 'Bb', 'C'], + midiNumbers: [2, 3, 5, 7, 9, 10, 12], + integers: [2, 3, 5, 7, 9, 10, 0], + }, + G: { + notes: ['G', 'Ab', 'Bb', 'C', 'D', 'Eb', 'F'], + midiNumbers: [7, 8, 10, 12, 14, 15, 17], + integers: [7, 8, 10, 0, 2, 3, 5], + }, + C: { + notes: ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb'], + midiNumbers: [0, 1, 3, 5, 7, 8, 10], + integers: [0, 1, 3, 5, 7, 8, 10], + }, + F: { + notes: ['F', 'Gb', 'Ab', 'Bb', 'C', 'Db', 'Eb'], + midiNumbers: [5, 6, 8, 10, 12, 13, 15], + integers: [5, 6, 8, 10, 0, 1, 3], + }, + Bb: { + notes: ['Bb', 'Cb', 'Db', 'Eb', 'F', 'Gb', 'Ab'], + midiNumbers: [10, 11, 13, 15, 17, 18, 20], + integers: [10, 11, 1, 3, 5, 6, 8], + }, + Eb: { + notes: ['Eb', 'Fb', 'Gb', 'Ab', 'Bb', 'Cb', 'Db'], + midiNumbers: [3, 4, 6, 8, 10, 11, 13], + integers: [3, 4, 6, 8, 10, 11, 1], + }, + B: { + notes: ['B', 'C', 'D', 'E', 'F#', 'G', 'A'], + midiNumbers: [11, 12, 14, 16, 18, 19, 21], + integers: [11, 0, 2, 4, 6, 7, 9], + }, + 'F#': { + notes: ['F#', 'G', 'A', 'B', 'C#', 'D', 'E'], + midiNumbers: [6, 7, 9, 11, 13, 14, 16], + integers: [6, 7, 9, 11, 1, 2, 4], + }, + 'C#': { + notes: ['C#', 'D', 'E', 'F#', 'G#', 'A', 'B'], + midiNumbers: [1, 2, 4, 6, 8, 9, 11], + integers: [1, 2, 4, 6, 8, 9, 11], + }, + 'G#': { + notes: ['G#', 'A', 'B', 'C#', 'D#', 'E', 'F#'], + midiNumbers: [8, 9, 11, 13, 15, 16, 18], + integers: [8, 9, 11, 1, 3, 4, 6], + }, + 'D#': { + notes: ['D#', 'E', 'F#', 'G#', 'A#', 'B', 'C#'], + midiNumbers: [3, 4, 6, 8, 10, 11, 13], + integers: [3, 4, 6, 8, 10, 11, 1], + }, + 'A#': { + notes: ['A#', 'B', 'C#', 'D#', 'E#', 'F#', 'G#'], + midiNumbers: [10, 11, 13, 15, 17, 18, 20], + integers: [10, 11, 1, 3, 5, 6, 8], + }, + 'E#': { + notes: ['E#', 'F#', 'G#', 'A#', 'B#', 'C#', 'D#'], + midiNumbers: [5, 6, 8, 10, 12, 13, 15], + integers: [5, 6, 8, 10, 0, 1, 3], + }, +}; + +export const lydian: Record = { + F: { + notes: ['F', 'G', 'A', 'B', 'C', 'D', 'E'], + midiNumbers: [5, 7, 9, 11, 12, 14, 16], + integers: [5, 7, 9, 11, 0, 2, 4], + }, + Bb: { + notes: ['Bb', 'C', 'D', 'E', 'F', 'G', 'A'], + midiNumbers: [10, 12, 14, 16, 17, 19, 21], + integers: [10, 0, 2, 4, 5, 7, 9], + }, + Eb: { + notes: ['Eb', 'F', 'G', 'A', 'Bb', 'C', 'D'], + midiNumbers: [3, 5, 7, 9, 10, 12, 14], + integers: [3, 5, 7, 9, 10, 0, 2], + }, + Ab: { + notes: ['Ab', 'Bb', 'C', 'D', 'Eb', 'F', 'G'], + midiNumbers: [8, 10, 12, 14, 15, 17, 19], + integers: [8, 10, 0, 2, 3, 5, 7], + }, + Db: { + notes: ['Db', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C'], + midiNumbers: [1, 3, 5, 7, 8, 10, 12], + integers: [1, 3, 5, 7, 8, 10, 0], + }, + Gb: { + notes: ['Gb', 'Ab', 'Bb', 'C', 'Db', 'Eb', 'F'], + midiNumbers: [6, 8, 10, 12, 13, 15, 17], + integers: [6, 8, 10, 0, 1, 3, 5], + }, + Cb: { + notes: ['Cb', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb'], + midiNumbers: [11, 13, 15, 17, 18, 20, 22], + integers: [11, 1, 3, 5, 6, 8, 10], + }, + Fb: { + notes: ['Fb', 'Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb'], + midiNumbers: [4, 6, 8, 10, 11, 13, 15], + integers: [4, 6, 8, 10, 11, 1, 3], + }, + C: { + notes: ['C', 'D', 'E', 'F#', 'G', 'A', 'B'], + midiNumbers: [0, 2, 4, 6, 7, 9, 11], + integers: [0, 2, 4, 6, 7, 9, 11], + }, + G: { + notes: ['G', 'A', 'B', 'C#', 'D', 'E', 'F#'], + midiNumbers: [7, 9, 11, 13, 14, 16, 18], + integers: [7, 9, 11, 1, 2, 4, 6], + }, + D: { + notes: ['D', 'E', 'F#', 'G#', 'A', 'B', 'C#'], + midiNumbers: [2, 4, 6, 8, 9, 11, 13], + integers: [2, 4, 6, 8, 9, 11, 1], + }, + A: { + notes: ['A', 'B', 'C#', 'D#', 'E', 'F#', 'G#'], + midiNumbers: [9, 11, 13, 15, 16, 18, 20], + integers: [9, 11, 1, 3, 4, 6, 8], + }, + E: { + notes: ['E', 'F#', 'G#', 'A#', 'B', 'C#', 'D#'], + midiNumbers: [4, 6, 8, 10, 11, 13, 15], + integers: [4, 6, 8, 10, 11, 1, 3], + }, + B: { + notes: ['B', 'C#', 'D#', 'E#', 'F#', 'G#', 'A#'], + midiNumbers: [11, 13, 15, 17, 18, 20, 22], + integers: [11, 1, 3, 5, 6, 8, 10], + }, + 'F#': { + notes: ['F#', 'G#', 'A#', 'B#', 'C#', 'D#', 'E#'], + midiNumbers: [6, 8, 10, 12, 13, 15, 17], + integers: [6, 8, 10, 0, 1, 3, 5], + }, +}; + +export const mixolydian: Record = { + G: { + notes: ['G', 'A', 'B', 'C', 'D', 'E', 'F'], + midiNumbers: [7, 9, 11, 12, 14, 16, 17], + integers: [7, 9, 11, 0, 2, 4, 5], + }, + C: { + notes: ['C', 'D', 'E', 'F', 'G', 'A', 'Bb'], + midiNumbers: [0, 2, 4, 5, 7, 9, 10], + integers: [0, 2, 4, 5, 7, 9, 10], + }, + F: { + notes: ['F', 'G', 'A', 'Bb', 'C', 'D', 'Eb'], + midiNumbers: [5, 7, 9, 10, 12, 14, 15], + integers: [5, 7, 9, 10, 0, 2, 3], + }, + Bb: { + notes: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'Ab'], + midiNumbers: [10, 12, 14, 15, 17, 19, 20], + integers: [10, 0, 2, 3, 5, 7, 8], + }, + Eb: { + notes: ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'Db'], + midiNumbers: [3, 5, 7, 8, 10, 12, 13], + integers: [3, 5, 7, 8, 10, 0, 1], + }, + Ab: { + notes: ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'Gb'], + midiNumbers: [8, 10, 12, 13, 15, 17, 18], + integers: [8, 10, 0, 1, 3, 5, 6], + }, + Db: { + notes: ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'Cb'], + midiNumbers: [1, 3, 5, 6, 8, 10, 11], + integers: [1, 3, 5, 6, 8, 10, 11], + }, + Gb: { + notes: ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'Fb'], + midiNumbers: [6, 8, 10, 11, 13, 15, 16], + integers: [6, 8, 10, 11, 1, 3, 4], + }, + D: { + notes: ['D', 'E', 'F#', 'G', 'A', 'B', 'C'], + midiNumbers: [2, 4, 6, 7, 9, 11, 12], + integers: [2, 4, 6, 7, 9, 11, 0], + }, + A: { + notes: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G'], + midiNumbers: [9, 11, 13, 14, 16, 18, 19], + integers: [9, 11, 1, 2, 4, 6, 7], + }, + E: { + notes: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D'], + midiNumbers: [4, 6, 8, 9, 11, 13, 14], + integers: [4, 6, 8, 9, 11, 1, 2], + }, + B: { + notes: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A'], + midiNumbers: [11, 13, 15, 16, 18, 20, 21], + integers: [11, 1, 3, 4, 6, 8, 9], + }, + 'F#': { + notes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E'], + midiNumbers: [6, 8, 10, 11, 13, 15, 16], + integers: [6, 8, 10, 11, 1, 3, 4], + }, + 'C#': { + notes: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B'], + midiNumbers: [1, 3, 5, 6, 8, 10, 11], + integers: [1, 3, 5, 6, 8, 10, 11], + }, + 'G#': { + notes: ['G#', 'A#', 'B#', 'C#', 'D#', 'E#', 'F#'], + midiNumbers: [8, 10, 12, 13, 15, 17, 18], + integers: [8, 10, 0, 1, 3, 5, 6], + }, +}; + +export const minor: Record = { + A: { + notes: ['A', 'B', 'C', 'D', 'E', 'F', 'G'], + midiNumbers: [9, 11, 12, 14, 16, 17, 19], + integers: [9, 11, 0, 2, 4, 5, 7], + }, + D: { + notes: ['D', 'E', 'F', 'G', 'A', 'Bb', 'C'], + midiNumbers: [2, 4, 5, 7, 9, 10, 12], + integers: [2, 4, 5, 7, 9, 10, 0], + }, + G: { + notes: ['G', 'A', 'Bb', 'C', 'D', 'Eb', 'F'], + midiNumbers: [7, 9, 10, 12, 14, 15, 17], + integers: [7, 9, 10, 0, 2, 3, 5], + }, + C: { + notes: ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb'], + midiNumbers: [0, 2, 3, 5, 7, 8, 10], + integers: [0, 2, 3, 5, 7, 8, 10], + }, + F: { + notes: ['F', 'G', 'Ab', 'Bb', 'C', 'Db', 'Eb'], + midiNumbers: [5, 7, 8, 10, 12, 13, 15], + integers: [5, 7, 8, 10, 0, 1, 3], + }, + Bb: { + notes: ['Bb', 'C', 'Db', 'Eb', 'F', 'Gb', 'Ab'], + midiNumbers: [10, 12, 13, 15, 17, 18, 20], + integers: [10, 0, 1, 3, 5, 6, 8], + }, + Eb: { + notes: ['Eb', 'F', 'Gb', 'Ab', 'Bb', 'Cb', 'Db'], + midiNumbers: [3, 5, 6, 8, 10, 11, 13], + integers: [3, 5, 6, 8, 10, 11, 1], + }, + Ab: { + notes: ['Ab', 'Bb', 'Cb', 'Db', 'Eb', 'Fb', 'Gb'], + midiNumbers: [8, 10, 11, 13, 15, 16, 18], + integers: [8, 10, 11, 1, 3, 4, 6], + }, + E: { + notes: ['E', 'F#', 'G', 'A', 'B', 'C', 'D'], + midiNumbers: [4, 6, 7, 9, 11, 12, 14], + integers: [4, 6, 7, 9, 11, 0, 2], + }, + B: { + notes: ['B', 'C#', 'D', 'E', 'F#', 'G', 'A'], + midiNumbers: [11, 13, 14, 16, 18, 19, 21], + integers: [11, 1, 2, 4, 6, 7, 9], + }, + 'F#': { + notes: ['F#', 'G#', 'A', 'B', 'C#', 'D', 'E'], + midiNumbers: [6, 8, 9, 11, 13, 14, 16], + integers: [6, 8, 9, 11, 1, 2, 4], + }, + 'C#': { + notes: ['C#', 'D#', 'E', 'F#', 'G#', 'A', 'B'], + midiNumbers: [1, 3, 4, 6, 8, 9, 11], + integers: [1, 3, 4, 6, 8, 9, 11], + }, + 'G#': { + notes: ['G#', 'A#', 'B', 'C#', 'D#', 'E', 'F#'], + midiNumbers: [8, 10, 11, 13, 15, 16, 18], + integers: [8, 10, 11, 1, 3, 4, 6], + }, + 'D#': { + notes: ['D#', 'E#', 'F#', 'G#', 'A#', 'B', 'C#'], + midiNumbers: [3, 5, 6, 8, 10, 11, 13], + integers: [3, 5, 6, 8, 10, 11, 1], + }, + 'A#': { + notes: ['A#', 'B#', 'C#', 'D#', 'E#', 'F#', 'G#'], + midiNumbers: [10, 12, 13, 15, 17, 18, 20], + integers: [10, 0, 1, 3, 5, 6, 8], + }, +}; + +export const locrian: Record = { + B: { + notes: ['B', 'C', 'D', 'E', 'F', 'G', 'A'], + midiNumbers: [11, 12, 14, 16, 17, 19, 21], + integers: [11, 0, 2, 4, 5, 7, 9], + }, + E: { + notes: ['E', 'F', 'G', 'A', 'Bb', 'C', 'D'], + midiNumbers: [4, 5, 7, 9, 10, 12, 14], + integers: [4, 5, 7, 9, 10, 0, 2], + }, + A: { + notes: ['A', 'Bb', 'C', 'D', 'Eb', 'F', 'G'], + midiNumbers: [9, 10, 12, 14, 15, 17, 19], + integers: [9, 10, 0, 2, 3, 5, 7], + }, + D: { + notes: ['D', 'Eb', 'F', 'G', 'Ab', 'Bb', 'C'], + midiNumbers: [2, 3, 5, 7, 8, 10, 12], + integers: [2, 3, 5, 7, 8, 10, 0], + }, + G: { + notes: ['G', 'Ab', 'Bb', 'C', 'Db', 'Eb', 'F'], + midiNumbers: [7, 8, 10, 12, 13, 15, 17], + integers: [7, 8, 10, 0, 1, 3, 5], + }, + C: { + notes: ['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb'], + midiNumbers: [0, 1, 3, 5, 6, 8, 10], + integers: [0, 1, 3, 5, 6, 8, 10], + }, + F: { + notes: ['F', 'Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb'], + midiNumbers: [5, 6, 8, 10, 11, 13, 15], + integers: [5, 6, 8, 10, 11, 1, 3], + }, + Bb: { + notes: ['Bb', 'Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab'], + midiNumbers: [10, 11, 13, 15, 16, 18, 20], + integers: [10, 11, 1, 3, 4, 6, 8], + }, + 'F#': { + notes: ['F#', 'G', 'A', 'B', 'C', 'D', 'E'], + midiNumbers: [6, 7, 9, 11, 12, 14, 16], + integers: [6, 7, 9, 11, 0, 2, 4], + }, + 'C#': { + notes: ['C#', 'D', 'E', 'F#', 'G', 'A', 'B'], + midiNumbers: [1, 2, 4, 6, 7, 9, 11], + integers: [1, 2, 4, 6, 7, 9, 11], + }, + 'G#': { + notes: ['G#', 'A', 'B', 'C#', 'D', 'E', 'F#'], + midiNumbers: [8, 9, 11, 13, 14, 16, 18], + integers: [8, 9, 11, 1, 2, 4, 6], + }, + 'D#': { + notes: ['D#', 'E', 'F#', 'G#', 'A', 'B', 'C#'], + midiNumbers: [3, 4, 6, 8, 9, 11, 13], + integers: [3, 4, 6, 8, 9, 11, 1], + }, + 'A#': { + notes: ['A#', 'B', 'C#', 'D#', 'E', 'F#', 'G#'], + midiNumbers: [10, 11, 13, 15, 16, 18, 20], + integers: [10, 11, 1, 3, 4, 6, 8], + }, + 'E#': { + notes: ['E#', 'F#', 'G#', 'A#', 'B', 'C#', 'D#'], + midiNumbers: [5, 6, 8, 10, 11, 13, 15], + integers: [5, 6, 8, 10, 11, 1, 3], + }, + 'B#': { + notes: ['B#', 'C#', 'D#', 'E#', 'F#', 'G#', 'A#'], + midiNumbers: [0, 1, 3, 5, 6, 8, 10], + integers: [0, 1, 3, 5, 6, 8, 10], + }, +}; + +type LocrianNames = keyof typeof locrian; + +export const melodicMinor = {}; + +export const scales = { + major, + dorian, + phrygian, + lydian, + mixolydian, + minor, + locrian, +} as const; + +export type ScaleGroups = keyof typeof scales; diff --git a/src/db/scales/major.ts b/src/db/scales/major.ts new file mode 100644 index 0000000..1070fa7 --- /dev/null +++ b/src/db/scales/major.ts @@ -0,0 +1,62 @@ +export const majorScales = { + C: { + midiNumbers: [0, 2, 4, 5, 7, 9, 11], + notes: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], + }, + F: { + midiNumbers: [5, 7, 9, 10, 12, 14, 16], + notes: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], + }, + Bb: { + midiNumbers: [10, 12, 14, 15, 17, 19, 21], + notes: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], + }, + Eb: { + midiNumbers: [3, 5, 7, 8, 10, 12, 14], + notes: ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D'], + }, + Ab: { + midiNumbers: [8, 10, 12, 13, 15, 17, 19], + notes: ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G'], + }, + Db: { + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + notes: ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'], + }, + Gb: { + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + notes: ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F'], + }, + Cb: { + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + notes: ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'], + }, + G: { + midiNumbers: [7, 9, 11, 12, 14, 16, 18], + notes: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'], + }, + D: { + midiNumbers: [2, 4, 6, 7, 9, 11, 13], + notes: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'], + }, + A: { + midiNumbers: [9, 11, 13, 14, 16, 18, 20], + notes: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'], + }, + E: { + midiNumbers: [4, 6, 8, 9, 11, 13, 15], + notes: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'], + }, + B: { + midiNumbers: [11, 13, 15, 16, 18, 20, 22], + notes: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'], + }, + 'F#': { + midiNumbers: [6, 8, 10, 11, 13, 15, 17], + notes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E#'], + }, + 'C#': { + midiNumbers: [1, 3, 5, 6, 8, 10, 12], + notes: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], + }, +}; diff --git a/src/helper.ts b/src/helper.ts index 6441f9f..4a251f5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,14 +1,16 @@ -import type { TMode } from './types'; +import type { TMode, TScaleType } from './types.js'; export const isFlat = (str: string) => str.includes('b'); export const isSharp = (str: string) => str.includes('#'); -export const isMajor = (mode: TMode) => mode === 'ionian'; -export const isMinor = (mode: TMode) => mode === 'aeolian'; +export const isMajor = (mode: TMode | TScaleType) => + mode === 'ionian' || mode === 'major'; +export const isMinor = (mode: TMode | TScaleType) => + mode === 'aeolian' || mode === 'minor'; export const isMode = (mode: TMode) => !['aeolian', 'ionian'].includes(mode); // Arr export const offsetArr = (arr: Type[], amount: number) => { - return [...arr.slice(amount), ...arr.slice(0, amount)]; + return [...arr.slice(amount), ...arr.slice(0, amount)]; }; diff --git a/src/keys/getKey.test.ts b/src/keys/getKey.test.ts index e09d82d..f2fd46e 100644 --- a/src/keys/getKey.test.ts +++ b/src/keys/getKey.test.ts @@ -1,18 +1,34 @@ import { describe, it, expect } from 'vitest'; -import { getKey } from './getKey'; +import { getKey } from './getKey.js'; describe('getKey', () => { - it('gets Major keys', () => { - expect(getKey('C major')?.major.notes).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); - expect(getKey('C major')?.minor.notes).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']); - expect(getKey('C major')?.chords.map((chord) => chord.name)).toEqual([ - 'C', - 'Dm', - 'Em', - 'F', - 'G', - 'Am', - 'Bdim' - ]); - }); + it('gets Major keys', () => { + expect(getKey('C major')?.major.notes).toEqual([ + 'C', + 'D', + 'E', + 'F', + 'G', + 'A', + 'B', + ]); + expect(getKey('C major')?.minor.notes).toEqual([ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + ]); + expect(getKey('C major')?.chords.map((chord) => chord.name)).toEqual([ + 'C', + 'Dm', + 'Em', + 'F', + 'G', + 'Am', + 'Bdim', + ]); + }); }); diff --git a/src/keys/getKey.ts b/src/keys/getKey.ts index 4000d32..2ac5e06 100644 --- a/src/keys/getKey.ts +++ b/src/keys/getKey.ts @@ -1,12 +1,13 @@ -import { getScaleChords } from '../chords/getScaleChords'; -import { majorScales, modes } from '../consts'; -import { isMajor, offsetArr } from '../helper'; -import { getModeName } from '../modes/getName'; -import { isModeName } from '../modes/helpers'; -import { extractScaleName } from '../scale/extractName'; -import { getMajorFromMode } from '../scale/getMajorFromMode'; -import type { IChord, TMode } from '../types'; -import { getRelativeMinorName } from './helpers'; +import { getScaleChords } from '../chords/getScaleChords.js'; +import { modes, scaleTypes } from '../consts.js'; +import { scales } from '../db/scales/allScales.js'; +import { isMajor, offsetArr } from '../helper.js'; +import { isScaleType } from '../modes/helpers.js'; +import { extractScaleName } from '../scale/extractName.js'; +import { getMajorFromMode } from '../scale/getMajorFromMode.js'; +import { getFriendlyModeName } from '../scale/helpers.js'; +import type { IChord, TMode } from '../types.js'; +import { getRelativeMinorName } from './helpers.js'; type KeyInfo = { name: string; @@ -25,34 +26,40 @@ type KeyInfo = { export const getKey = (key: string): KeyInfo | undefined => { const [pitch, mode] = extractScaleName(key) || []; - + const friendlyModeName = getFriendlyModeName(mode); if (mode === undefined || pitch === undefined) { return; } - if (!isModeName(mode)) { + if (!isScaleType(friendlyModeName)) { return; } - const keyQuality = getModeName(mode); - const majorPitch = isMajor(mode) ? pitch : getMajorFromMode(pitch, mode); - const majorScale = majorScales[majorPitch]; - const scale = offsetArr(majorScale, modes.indexOf(mode)); + const keyQuality = friendlyModeName; + const majorPitch = isMajor(mode) + ? pitch + : getMajorFromMode(pitch, friendlyModeName); + + const majorScale = scales.major[majorPitch]; + const scale = offsetArr( + majorScale.notes, + scaleTypes.indexOf(friendlyModeName) + ); return { name: `${pitch} ${keyQuality}`, notes: scale, major: { name: `${majorPitch} major`, - notes: majorScale, + notes: majorScale.notes, }, minor: { name: getRelativeMinorName(pitch), - notes: offsetArr(majorScale, 5), + notes: offsetArr(majorScale.notes, 5), }, modes(name: TMode) { - return offsetArr(majorScale, modes.indexOf(name)); + return offsetArr(majorScale.notes, modes.indexOf(name)); }, - chords: getScaleChords(scale, mode), + chords: getScaleChords(scale, friendlyModeName), }; }; diff --git a/src/keys/helpers.ts b/src/keys/helpers.ts index 08551cc..e9501b5 100644 --- a/src/keys/helpers.ts +++ b/src/keys/helpers.ts @@ -1,4 +1,4 @@ -import { majorScales } from '../consts'; +import { majorScales } from '../consts.js'; export const getRelativeMinorName = (pitchClass: string) => { return `${majorScales[pitchClass][5]} minor`; diff --git a/src/modes/getName.ts b/src/modes/getName.ts deleted file mode 100644 index 0b0a6a5..0000000 --- a/src/modes/getName.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { TMode } from '../types'; - -export const getModeName = (mode: TMode) => { - switch (mode) { - case 'ionian': - return 'major'; - case 'aeolian': - return 'minor'; - default: - return mode; - } -}; diff --git a/src/modes/guessMode.test.ts b/src/modes/guessMode.test.ts new file mode 100644 index 0000000..4b9bdd1 --- /dev/null +++ b/src/modes/guessMode.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('guessMode', () => { + it('guesses mode bases on scale and chords', () => { + // Mad World + }); +}); diff --git a/src/modes/guessMode.ts b/src/modes/guessMode.ts new file mode 100644 index 0000000..4633542 --- /dev/null +++ b/src/modes/guessMode.ts @@ -0,0 +1 @@ +export const guessMode = () => {}; diff --git a/src/modes/helpers.ts b/src/modes/helpers.ts index e6af48b..6b93496 100644 --- a/src/modes/helpers.ts +++ b/src/modes/helpers.ts @@ -1,10 +1,10 @@ -import { modes, scaleIntervals } from '../consts'; -import type { TMode, TScaleType } from '../types'; +import { modes, scaleIntervals } from '../consts.js'; +import type { TMode, TScaleType } from '../types.js'; -export function isModeName(value: string): value is TMode { - return modes.includes(value as TMode); +export function isModeName(value: string | undefined): value is TMode { + return modes.includes(value as TMode); } export function isScaleType(value: string): value is TScaleType { - return Object.keys(scaleIntervals).includes(value as TScaleType); + return Object.keys(scaleIntervals).includes(value as TScaleType); } diff --git a/src/notes/determineNoteType.test.ts b/src/notes/determineNoteType.test.ts new file mode 100644 index 0000000..f487c9d --- /dev/null +++ b/src/notes/determineNoteType.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { determineNoteType } from './determineNoteType.js'; + +describe('determindNoteType', () => { + it('can determine flat notes', () => { + expect(determineNoteType('Bb')).toEqual('flat'); + }); + it('can determine sharp notes', () => { + expect(determineNoteType('D#')).toEqual('sharp'); + }); + it('can determine natural notes', () => { + expect(determineNoteType('D')).toEqual('natural'); + }); +}); diff --git a/src/notes/determineNoteType.ts b/src/notes/determineNoteType.ts new file mode 100644 index 0000000..e9a7f9e --- /dev/null +++ b/src/notes/determineNoteType.ts @@ -0,0 +1,12 @@ +import { isFlat, isSharp } from '../helper.js'; +import { NoteType } from '../types.js'; + +export const determineNoteType = (note: string): NoteType => { + if (isSharp(note)) { + return 'sharp'; + } else if (isFlat(note)) { + return 'flat'; + } else { + return 'natural'; + } +}; diff --git a/src/notes/getEquivalentNote.test.ts b/src/notes/getEquivalentNote.test.ts index b4a6c87..52f62cb 100644 --- a/src/notes/getEquivalentNote.test.ts +++ b/src/notes/getEquivalentNote.test.ts @@ -1,13 +1,35 @@ import { describe, expect, it } from 'vitest'; -import { getEquivalentNote } from './getEquivalentNote'; +import { + getEquivalentNote, + getWhiteNoteEquivalent, +} from './getEquivalentNote.js'; describe('getEquivalentNote', () => { - it('returns the equivalent note', () => { - expect(getEquivalentNote('Gb')).toEqual('F#'); - expect(getEquivalentNote('Ab')).toEqual('G#'); - }); + it('returns the equivalent note', () => { + expect(getEquivalentNote('Gb')).toEqual('F#'); + expect(getEquivalentNote('Ab')).toEqual('G#'); + expect(getEquivalentNote('B')).toEqual('Cb'); + expect(getEquivalentNote('C')).toEqual('B#'); + expect(getEquivalentNote('E')).toEqual('Fb'); + expect(getEquivalentNote('F')).toEqual('E#'); + }); - it('returns the original note if no equivalent is found', () => { - expect(getEquivalentNote('E')).toEqual('E'); - }); + it('returns the original note if no equivalent is found', () => { + expect(getEquivalentNote('A')).toEqual('A'); + expect(getEquivalentNote('D')).toEqual('D'); + expect(getEquivalentNote('G')).toEqual('G'); + }); +}); + +describe('getWhiteNoteEquivalent', () => { + it('returns an equivalent note for white notes', () => { + expect(getWhiteNoteEquivalent('Cb')).toEqual('B'); + expect(getWhiteNoteEquivalent('Fb')).toEqual('E'); + expect(getWhiteNoteEquivalent('E#')).toEqual('F'); + expect(getWhiteNoteEquivalent('B#')).toEqual('C'); + expect(getWhiteNoteEquivalent('B')).toEqual('Cb'); + expect(getWhiteNoteEquivalent('E')).toEqual('Fb'); + expect(getWhiteNoteEquivalent('F')).toEqual('E#'); + expect(getWhiteNoteEquivalent('C')).toEqual('B#'); + }); }); diff --git a/src/notes/getEquivalentNote.ts b/src/notes/getEquivalentNote.ts index cbfc348..5c16871 100644 --- a/src/notes/getEquivalentNote.ts +++ b/src/notes/getEquivalentNote.ts @@ -1,12 +1,36 @@ -import { notePositions } from '../consts'; +import { notePositions2 } from '../consts.js'; +import { determineNoteType } from './determineNoteType.js'; export const getEquivalentNote = (note: string) => { - const positionIndex = notePositions.findIndex((notes) => notes.includes(note)); - const noteGroup = notePositions[positionIndex]; + const positionIndex = notePositions2.findIndex((notes) => + Object.values(notes).includes(note) + ); + const noteGroup = notePositions2[positionIndex]; + const hasEquivalents = Object.values(noteGroup).length > 1; - if (positionIndex !== -1 && noteGroup.length > 1) { - return noteGroup[0] === note ? noteGroup[1] : noteGroup[0]; - } + if (positionIndex !== -1 && hasEquivalents) { + const currentNoteType = determineNoteType(note); + return Object.entries(noteGroup) + .filter(([type, noteValue]) => type !== currentNoteType) + .flatMap((x) => x)[1]; + } - return note; + return note; +}; + +export const getWhiteNoteEquivalent = (note: string) => { + const exceptions = [ + ['B', 'Cb'], + ['C', 'B#'], + ['E', 'Fb'], + ['F', 'E#'], + ]; + + const positionIndex = exceptions.findIndex((notes) => notes.includes(note)); + const noteGroup = exceptions[positionIndex]; + if (positionIndex !== -1 && noteGroup.length > 1) { + return noteGroup[0] === note ? noteGroup[1] : noteGroup[0]; + } else { + return ''; + } }; diff --git a/src/notes/getInterval.test.ts b/src/notes/getInterval.test.ts index 9ca509c..0e74bc2 100644 --- a/src/notes/getInterval.test.ts +++ b/src/notes/getInterval.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { getInterval } from './getInterval'; +import { getInterval } from './getInterval.js'; describe('getInterval', () => { - it('returns the most likely interval name', () => { - expect(getInterval('C', 'E')).toEqual('M3'); - }); + it('returns the most likely interval name', () => { + expect(getInterval('C', 'E')).toEqual('M3'); + }); }); diff --git a/src/notes/getInterval.ts b/src/notes/getInterval.ts index b285486..4d89538 100644 --- a/src/notes/getInterval.ts +++ b/src/notes/getInterval.ts @@ -1,18 +1,20 @@ -import { intervalsBySemitone } from '../consts'; -import type { TIntervalShorthand } from '../types'; -import { resetNotePositions } from './helpers'; +import { intervalsBySemitone } from '../consts.js'; +import type { TIntervalShorthand } from '../types.js'; +import { resetNotePositions } from './helpers.js'; export const getInterval = ( - noteX: string, - noteY: string, - showAlternatives = false + noteX: string, + noteY: string, + showAlternatives = false ): TIntervalShorthand | TIntervalShorthand[] => { - const allNotes = resetNotePositions(noteX); - const noteYIndex = allNotes.findIndex((note) => note.includes(noteY)); + const allNotes = resetNotePositions(noteX); + const noteYIndex = allNotes.findIndex((note) => + Object.values(note).includes(noteY) + ); - if (showAlternatives) { - return intervalsBySemitone[noteYIndex]; - } else { - return intervalsBySemitone[noteYIndex][0]; - } + if (showAlternatives) { + return intervalsBySemitone[noteYIndex]; + } else { + return intervalsBySemitone[noteYIndex][0]; + } }; diff --git a/src/notes/getScaleDegreeOfNote.test.ts b/src/notes/getScaleDegreeOfNote.test.ts index 2ec6115..8515d07 100644 --- a/src/notes/getScaleDegreeOfNote.test.ts +++ b/src/notes/getScaleDegreeOfNote.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getScaleDegreeOfNote } from './getScaleDegreeOfNote'; +import { getScaleDegreeOfNote } from './getScaleDegreeOfNote.js'; describe('getScaleDegreeOfNote', () => { it('converts notes to degrees based on a scale', () => { diff --git a/src/notes/getScaleDegreeOfNote.ts b/src/notes/getScaleDegreeOfNote.ts index 63a1014..5dd019b 100644 --- a/src/notes/getScaleDegreeOfNote.ts +++ b/src/notes/getScaleDegreeOfNote.ts @@ -1,4 +1,15 @@ -import { majorScaleRomanNumerals } from '../consts'; +import { romanNumeralCase } from '../chords/helpers.js'; +import { majorScaleQualities, modes, romanNumerals } from '../consts.js'; +import { offsetArr } from '../helper.js'; +import { TMode } from '../types.js'; -export const getScaleDegreeOfNote = (note: string, scale: string[]) => - majorScaleRomanNumerals[scale.indexOf(note)]; +export const getScaleDegreeOfNote = ( + note: string, + scale: string[], + mode: TMode = 'ionian' +) => { + const reOrdered = offsetArr(majorScaleQualities, modes.indexOf(mode)); + return romanNumerals.map((numeral, i) => { + return romanNumeralCase(numeral, reOrdered[i]); + })[scale.indexOf(note)]; +}; diff --git a/src/notes/helpers.ts b/src/notes/helpers.ts index 7de2c0e..58228ec 100644 --- a/src/notes/helpers.ts +++ b/src/notes/helpers.ts @@ -1,8 +1,30 @@ -import { notePositions } from '../consts'; -import { offsetArr } from '../helper'; +import { notePositions2 } from '../consts.js'; +import { offsetArr } from '../helper.js'; +import { MIDINumber } from '../types.js'; export const resetNotePositions = (note: string) => { - const noteIndex = notePositions.findIndex((notes) => notes.includes(note)); + const noteIndex = notePositions2.findIndex((notes) => + Object.values(notes).includes(note) + ); - return offsetArr(notePositions, noteIndex); + return offsetArr(notePositions2, noteIndex); }; + +export function isMidiNumber(value: any): value is MIDINumber { + return (value as MIDINumber) >= 0 && value <= 127; +} + +export const keyboardNotePositions = [ + ['C', 'B#'], + ['C#', 'Db'], + ['D'], + ['D#', 'Eb'], + ['E', 'Fb'], + ['F', 'E#'], + ['F#', 'Gb'], + ['G'], + ['G#', 'Ab'], + ['A'], + ['A#', 'Bb'], + ['B', 'Cb'], +]; diff --git a/src/notes/intervalToSemitone.test.ts b/src/notes/intervalToSemitone.test.ts index a3490d4..b76c180 100644 --- a/src/notes/intervalToSemitone.test.ts +++ b/src/notes/intervalToSemitone.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from 'vitest'; -import { intervalToSemitone } from './intervalToSemitone'; +import { intervalToSemitone } from './intervalToSemitone.js'; describe('getSemitones', () => { - it('returns a semitone value given an interval shorthand', () => { - expect(intervalToSemitone('P8')).toEqual(12); - }); + it('returns a semitone value given an interval shorthand', () => { + expect(intervalToSemitone('P8')).toEqual(12); + }); - it('returns a semitone value between 0 and 12', () => { - // This makes it easier to get the correct note value - // without have to deal with octaves - expect(intervalToSemitone('A15', true)).toEqual(1); - expect(intervalToSemitone('P12', true)).toEqual(7); - }); + it('returns a semitone value between 0 and 12', () => { + // This makes it easier to get the correct note value + // without have to deal with octaves + expect(intervalToSemitone('A15', true)).toEqual(1); + expect(intervalToSemitone('P12', true)).toEqual(7); + }); }); diff --git a/src/notes/intervalToSemitone.ts b/src/notes/intervalToSemitone.ts index 728819a..1b809e8 100644 --- a/src/notes/intervalToSemitone.ts +++ b/src/notes/intervalToSemitone.ts @@ -1,12 +1,15 @@ -import { intervalsMap } from '../consts'; -import type { TIntervalShorthand } from '../types'; +import { intervalsMap } from '../consts.js'; +import type { TIntervalShorthand } from '../types.js'; -export const intervalToSemitone = (interval: TIntervalShorthand, normalized = false) => { - const semitones = intervalsMap[interval]; +export const intervalToSemitone = ( + interval: TIntervalShorthand, + normalized = false +) => { + const semitones = intervalsMap[interval]; - if (normalized) { - return semitones >= 12 ? semitones % 12 : semitones; - } + if (normalized) { + return semitones >= 12 ? semitones % 12 : semitones; + } - return semitones; + return semitones; }; diff --git a/src/notes/noteToInteger.test.ts b/src/notes/noteToInteger.test.ts new file mode 100644 index 0000000..6cf12f5 --- /dev/null +++ b/src/notes/noteToInteger.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { noteToInteger } from './noteToInteger.js'; + +describe('noteToMidi', () => { + it('returns a midi note number when given a pitch', () => { + expect(noteToInteger('C')).toEqual(0); + expect(noteToInteger('D')).toEqual(2); + expect(noteToInteger('E')).toEqual(4); + expect(noteToInteger('F')).toEqual(5); + expect(noteToInteger('G')).toEqual(7); + expect(noteToInteger('A')).toEqual(9); + expect(noteToInteger('B')).toEqual(11); + }); +}); diff --git a/src/notes/noteToInteger.ts b/src/notes/noteToInteger.ts new file mode 100644 index 0000000..d63b3ac --- /dev/null +++ b/src/notes/noteToInteger.ts @@ -0,0 +1,5 @@ +import { keyboardNotePositions } from './helpers.js'; + +export const noteToInteger = (note: string) => { + return keyboardNotePositions.findIndex((key) => key.includes(note)); +}; diff --git a/src/notes/noteToMidi.test.ts b/src/notes/noteToMidi.test.ts new file mode 100644 index 0000000..f232189 --- /dev/null +++ b/src/notes/noteToMidi.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { midiToNote, noteToMidi } from './noteToMidi.js'; + +describe('noteToMidi', () => { + it('returns a midi note number when given a pitch', () => { + expect(noteToMidi('C-1')).toEqual(0); + expect(noteToMidi('C4')).toEqual(60); + expect(noteToMidi('G9')).toEqual(127); + expect(noteToMidi('Ab2')).toEqual(44); + expect(noteToMidi('G#2')).toEqual(44); + }); +}); + +describe('midiToNote', () => { + it('returns a pitch when given a midi note number', () => { + expect(midiToNote(0)).toEqual(['C', 'B#']); + expect(midiToNote(60)).toEqual(['C', 'B#']); + expect(midiToNote(127)).toEqual(['G']); + expect(midiToNote(44)).toEqual(['G#', 'Ab']); + }); +}); diff --git a/src/notes/noteToMidi.ts b/src/notes/noteToMidi.ts new file mode 100644 index 0000000..3e3cc1e --- /dev/null +++ b/src/notes/noteToMidi.ts @@ -0,0 +1,29 @@ +import { MIDINumber } from '../types.js'; +import { isMidiNumber, keyboardNotePositions } from './helpers.js'; + +export const noteToMidi = (note: string): number | undefined => { + let midiNote = note; + if (!/([0-9]|-1)$/.test(note)) { + midiNote = `${note}-1`; + } + + const regex = /^([A-G][b#]?)([0-9]|-1)$/; + const match = midiNote.match(regex)?.slice(1, 3); + + if (!match) return undefined; + + const [pitchClass, octave] = match; + + const noteNumber = keyboardNotePositions.findIndex((notes) => + notes.includes(pitchClass) + ); + const result = (parseInt(octave) + 1) * 12 + noteNumber; + + if (isMidiNumber(result)) { + return result; + } +}; + +export const midiToNote = (num: number) => { + return keyboardNotePositions[num % 12]; +}; diff --git a/src/notes/transpose.test.ts b/src/notes/transpose.test.ts index 38ec194..7e4fc25 100644 --- a/src/notes/transpose.test.ts +++ b/src/notes/transpose.test.ts @@ -1,16 +1,28 @@ import { describe, expect, it } from 'vitest'; -import { transposeNote } from './transpose'; +import { transposeNote } from './transpose.js'; describe('transposeNote', () => { - it('supports transposing notes by interval shorthand', () => { - expect(transposeNote('C', 'P4')).toEqual('F'); - }); + it('supports transposing notes by interval shorthand', () => { + expect(transposeNote('C', 'P4')).toEqual('F'); + }); - it('chooses sharps for augmented intervals', () => { - expect(transposeNote('C', 'A4')).toEqual('F#'); - }); + it('chooses sharps for augmented intervals', () => { + expect(transposeNote('C', 'A4')).toEqual('F#'); + }); - it('chooses flats for diminished intervals', () => { - expect(transposeNote('C', 'd5')).toEqual('Gb'); - }); + it('chooses flats for diminished intervals', () => { + expect(transposeNote('C', 'd5')).toEqual('Gb'); + }); + + it('chooses a flat variant when passed the forceFlats options is true', () => { + expect(transposeNote('C', 'm3', { forceFlat: true })).toEqual('Eb'); + }); + + it('chooses a sharp variant when passed the forceSharps options is true', () => { + expect(transposeNote('C', 'm3', { forceSharp: true })).toEqual('D#'); + }); + + it('uses a scale of notes to constrain the results', () => { + expect(transposeNote('C', 'P1', { keyName: 'C# Major' })).toEqual('B#'); + }); }); diff --git a/src/notes/transpose.ts b/src/notes/transpose.ts index 1185ce2..622d8f9 100644 --- a/src/notes/transpose.ts +++ b/src/notes/transpose.ts @@ -1,66 +1,91 @@ -import { isFlat, isSharp } from '../helper'; -import { getScale } from '../scale/getScale'; -import type { TIntervalShorthand } from '../types'; -import { intervalToSemitone } from './intervalToSemitone'; -import { resetNotePositions } from './helpers'; - -interface IForceVariant { - forceFlat?: boolean; - forceSharp?: boolean; -} +import { isFlat, isSharp } from '../helper.js'; +import { findScaleByName, getScale } from '../scale/getScale.js'; +import type { TIntervalShorthand } from '../types.js'; +import { intervalToSemitone } from './intervalToSemitone.js'; +import { resetNotePositions } from './helpers.js'; +import { NotePosition } from '../consts.js'; -const pickNoteVariant = (notes: string[], srcNote: string, options?: IForceVariant) => { - const [hasFlat, hasSharp] = [isFlat(srcNote), isSharp(srcNote)]; +interface PickNoteVariantOptions { + forceFlat?: boolean; + forceSharp?: boolean; + forceScale?: string[]; +} - if (options?.forceSharp || hasSharp) { - return notes[0]; - } +const pickNoteVariant = ( + notes: NotePosition, + srcNote: string, + options?: PickNoteVariantOptions +) => { + const [hasFlat, hasSharp] = [isFlat(srcNote), isSharp(srcNote)]; + if (options?.forceSharp || hasSharp) { + return notes.sharp; + } - if (options?.forceFlat || hasFlat) { - return notes[1]; - } + if (options?.forceFlat || hasFlat) { + return notes.flat; + } - return notes[1]; + return notes.natural ? notes.natural : notes.flat; }; -const pickNoteFromScale = (notes: string[], scale: string[] | undefined) => { - if (scale === undefined) { - return 'X'; - } +const pickNoteFromScale = (noteGroup: NotePosition, scale: string[]) => { + if (scale === undefined) return 'X'; - const correctIndex = notes.findIndex((note) => scale.includes(note)); + if (noteGroup.flat && scale.includes(noteGroup.flat)) { + return noteGroup.flat; + } else if (noteGroup.sharp && scale.includes(noteGroup.sharp)) { + return noteGroup.sharp; + } else if (noteGroup.natural && scale.includes(noteGroup.natural)) { + return noteGroup.natural; + } - return correctIndex >= 0 ? notes[correctIndex] : 'X'; + return 'X'; }; interface INoteTransposeOptions { - forceFlat?: boolean; - forceSharp?: boolean; - keyName?: string; + forceFlat?: boolean; + forceSharp?: boolean; + forceAliasAccidentals?: boolean; + keyName?: string; } export const transposeNote = ( - note: string, - interval: TIntervalShorthand, - options?: INoteTransposeOptions + note: string, + interval: TIntervalShorthand, + options?: INoteTransposeOptions ): string => { - const transposedNotes = resetNotePositions(note)[intervalToSemitone(interval, true)]; + const transposedNotes = + resetNotePositions(note)[intervalToSemitone(interval, true)]; + + const chooseSharp = /a/i.test(interval) || options?.forceSharp; + const chooseFlat = /d/i.test(interval) || options?.forceFlat; + + if (Object.values(transposedNotes).length === 1 && transposedNotes.natural) { + return transposedNotes.natural; + } - if (transposedNotes.length === 1) { - return transposedNotes[0]; - } + if (chooseSharp && transposedNotes.sharp) { + return transposedNotes.sharp; + } - if (/a/i.test(interval)) { - return pickNoteVariant(transposedNotes, note, { forceSharp: true }); - } + if (chooseFlat && transposedNotes.flat) { + return transposedNotes.flat; + } - if (/d/i.test(interval)) { - return pickNoteVariant(transposedNotes, note, { forceFlat: true }); - } + if (options?.forceFlat && options.forceSharp) { + console.error( + 'You cannot pass both forceFlat and ForceSharp options at the same time' + ); + } - if (options?.keyName !== undefined) { - return pickNoteFromScale(transposedNotes, getScale(options.keyName)); - } + if (options?.keyName !== undefined) { + return pickNoteFromScale( + transposedNotes, + findScaleByName(options.keyName)?.notes + ); + } - return pickNoteVariant(transposedNotes, note); + return transposedNotes.natural + ? transposedNotes.natural + : transposedNotes.flat || ''; }; diff --git a/src/scale/extractName.ts b/src/scale/extractName.ts index 0994834..593d297 100644 --- a/src/scale/extractName.ts +++ b/src/scale/extractName.ts @@ -1,22 +1,15 @@ -import { isScaleType } from '../modes/helpers'; -import type { TScaleType } from '../types'; - -export const extractScaleName = (name: string): [string, TScaleType] | undefined => { - const regex = new RegExp(/([A-G](?:b|#)?) (.*)/); - const [pitchClass, scale] = name.match(regex)?.slice(1, 3) || []; - const normalScale = scale.toLowerCase().replaceAll(' ', '-'); - - if ((pitchClass || scale) === undefined) { - console.log('Not a scale'); - } - - if (normalScale === 'major') { - return [pitchClass, 'ionian']; - } - - if (normalScale === 'minor') { - return [pitchClass, 'aeolian']; - } - - if (isScaleType(normalScale)) return [pitchClass, normalScale]; +import { isScaleType } from '../modes/helpers.js'; +import type { TMode, TScaleType } from '../types.js'; + +export const extractScaleName = ( + name: string +): [string, TScaleType | TMode] | undefined => { + const regex = new RegExp(/([A-G](?:b|#)?) (.*)/); + const [pitchClass, scale] = name.match(regex)?.slice(1, 3) || []; + const normalScale = scale.toLowerCase().replaceAll(' ', '-'); + + if ((pitchClass || scale) === undefined) { + console.log('Not a scale'); + } + if (isScaleType(normalScale)) return [pitchClass, normalScale]; }; diff --git a/src/scale/getMajorFromMode.ts b/src/scale/getMajorFromMode.ts index 1ced390..b7365f5 100644 --- a/src/scale/getMajorFromMode.ts +++ b/src/scale/getMajorFromMode.ts @@ -1,10 +1,10 @@ -import { majorScales, modes } from '../consts'; -import type { TMode } from '../types'; +import { majorScales, scaleTypes } from '../consts.js'; +import type { TScaleType } from '../types.js'; -export const getMajorFromMode = (tonic: string, mode: TMode) => { - const modeIndex = modes.indexOf(mode); +export const getMajorFromMode = (tonic: string, mode: TScaleType) => { + const modeIndex = scaleTypes.indexOf(mode); const scaleIndex = Object.values(majorScales).findIndex((notes) => { - return notes[modeIndex] === tonic; + return notes.notes[modeIndex] === tonic; }); return Object.keys(majorScales)[scaleIndex]; diff --git a/src/scale/getScale.test.ts b/src/scale/getScale.test.ts index 719380c..2b14c30 100644 --- a/src/scale/getScale.test.ts +++ b/src/scale/getScale.test.ts @@ -1,46 +1,134 @@ import { describe, expect, it } from 'vitest'; -import { getScale } from './getScale'; +import { getScale } from './getScale.js'; describe('getScale', () => { - it('supports major scales', () => { - expect(getScale('C major')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); - expect(getScale('E major')).toEqual(['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#']); - }); + it.only('supports major scales', () => { + expect(getScale('C major')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); + expect(getScale('E major')).toEqual([ + 'E', + 'F#', + 'G#', + 'A', + 'B', + 'C#', + 'D#', + ]); + }); - it('supports minor scales', () => { - expect(getScale('A minor')).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']); - expect(getScale('C# minor')).toEqual(['C#', 'D#', 'E', 'F#', 'G#', 'A', 'B']); - }); + it('supports minor scales', () => { + expect(getScale('A minor')).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G']); + expect(getScale('C# minor')).toEqual([ + 'C#', + 'D#', + 'E', + 'F#', + 'G#', + 'A', + 'B', + ]); + }); - it('supports melodic minor scales', () => { - expect(getScale('A melodic minor')).toEqual(['A', 'B', 'C', 'D', 'E', 'F#', 'G#']); - expect(getScale('F melodic minor')).toEqual(['F', 'G', 'Ab', 'Bb', 'C', 'D', 'E']); - }); - it('supports harmonic minor scales', () => { - expect(getScale('B harmonic minor')).toEqual(['B', 'C#', 'D', 'E', 'F#', 'G', 'A#']); - }); + it('supports melodic minor scales', () => { + expect(getScale('A melodic minor')).toEqual([ + 'A', + 'B', + 'C', + 'D', + 'E', + 'F#', + 'G#', + ]); + expect(getScale('F melodic minor')).toEqual([ + 'F', + 'G', + 'Ab', + 'Bb', + 'C', + 'D', + 'E', + ]); + }); + it('supports harmonic minor scales', () => { + expect(getScale('B harmonic minor')).toEqual([ + 'B', + 'C#', + 'D', + 'E', + 'F#', + 'G', + 'A#', + ]); + }); - describe('diatonic modes', () => { - it('supports ionian mode', () => { - expect(getScale('C ionian')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); - }); - it('supports dorian mode', () => { - expect(getScale('C dorian')).toEqual(['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb']); - }); - it('supports phrygian mode', () => { - expect(getScale('C phrygian')).toEqual(['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb']); - }); - it('supports lydian mode', () => { - expect(getScale('C lydian')).toEqual(['C', 'D', 'E', 'F#', 'G', 'A', 'B']); - }); - it('supports myxolydian mode', () => { - expect(getScale('C myxolydian')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'Bb']); - }); - it('supports aeolian mode', () => { - expect(getScale('C aeolian')).toEqual(['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']); - }); - it('supports locrian mode', () => { - expect(getScale('C locrian')).toEqual(['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb']); - }); - }); + describe('diatonic modes', () => { + it('supports ionian mode', () => { + expect(getScale('C ionian')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); + }); + it('supports dorian mode', () => { + expect(getScale('C dorian')).toEqual([ + 'C', + 'D', + 'Eb', + 'F', + 'G', + 'A', + 'Bb', + ]); + }); + it('supports phrygian mode', () => { + expect(getScale('C phrygian')).toEqual([ + 'C', + 'Db', + 'Eb', + 'F', + 'G', + 'Ab', + 'Bb', + ]); + }); + it('supports lydian mode', () => { + expect(getScale('C lydian')).toEqual([ + 'C', + 'D', + 'E', + 'F#', + 'G', + 'A', + 'B', + ]); + }); + it('supports mixolydian mode', () => { + expect(getScale('C mixolydian')).toEqual([ + 'C', + 'D', + 'E', + 'F', + 'G', + 'A', + 'Bb', + ]); + }); + it('supports aeolian mode', () => { + expect(getScale('C aeolian')).toEqual([ + 'C', + 'D', + 'Eb', + 'F', + 'G', + 'Ab', + 'Bb', + ]); + }); + it('supports locrian mode', () => { + expect(getScale('C locrian')).toEqual([ + 'C', + 'Db', + 'Eb', + 'F', + 'Gb', + 'Ab', + 'Bb', + ]); + }); + }); }); diff --git a/src/scale/getScale.ts b/src/scale/getScale.ts index cc1058a..9fa776f 100644 --- a/src/scale/getScale.ts +++ b/src/scale/getScale.ts @@ -1,28 +1,55 @@ -import { scaleIntervals } from '../consts'; -import { transposeNote } from '../notes/transpose'; -import { getEquivalentNote } from '../notes/getEquivalentNote'; -import { extractScaleName } from './extractName'; -import { isValidScale } from './isValidScale'; +import { scaleIntervals } from '../consts.js'; +import { transposeNote } from '../notes/transpose.js'; +import { getEquivalentNote } from '../notes/getEquivalentNote.js'; +import { extractScaleName } from './extractName.js'; +import { isValidScale } from './isValidScale.js'; +import { ScaleInfo, scales } from '../db/scales/allScales.js'; +import { isScaleType } from '../modes/helpers.js'; +import { getFriendlyModeName } from './helpers.js'; const hasAccidental = (note: string) => { - return /[#b]/.test(note); + return /[#b]/.test(note); }; const getValidScale = (scale: string[]) => { - if (isValidScale(scale)) { - return scale; - } else { - return scale.map((note) => (hasAccidental(note) ? getEquivalentNote(note) : note)); - } + if (isValidScale(scale)) { + return scale; + } else { + return scale.map((note) => + hasAccidental(note) ? getEquivalentNote(note) : note + ); + } +}; + +export const findScaleByName = (name: string): ScaleInfo => { + const [pitchClass, mode] = extractScaleName(name) || []; + const friendlyModeName = getFriendlyModeName(mode); + + if ( + isScaleType(friendlyModeName) && + scales[friendlyModeName] && + pitchClass !== undefined + ) { + return scales[friendlyModeName][pitchClass]; + } + + return { + notes: [], + midiNumbers: [], + integers: [], + }; }; export const getScale = (scaleName: string) => { - const [pitchClass, mode] = extractScaleName(scaleName) || []; - const generatedScale = Array(7).fill(pitchClass); + const [pitchClass, mode] = extractScaleName(scaleName) || []; + const friendlyModeName = getFriendlyModeName(mode); + const generatedScale = Array(7).fill(pitchClass); - if (pitchClass && mode !== undefined) { - return getValidScale( - generatedScale.map((note, i) => transposeNote(note, scaleIntervals[mode][i])) - ); - } + if (pitchClass && mode !== undefined) { + return getValidScale( + generatedScale.map((note, i) => + transposeNote(note, scaleIntervals[friendlyModeName][i]) + ) + ); + } }; diff --git a/src/scale/getScaleDegrees.test.ts b/src/scale/getScaleDegrees.test.ts new file mode 100644 index 0000000..5b7485e --- /dev/null +++ b/src/scale/getScaleDegrees.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest'; +import { getScaleDegrees } from './getScaleDegrees.js'; + +const testSet = [ + ['C major', ['I', 'ii', 'iii', 'IV', 'V', 'vi', 'vii']], + ['C dorian', ['i', 'ii', 'III', 'IV', 'v', 'vi', 'VII']], + ['C phrygian', ['i', 'II', 'III', 'iv', 'v', 'VI', 'vii']], + ['C lydian', ['I', 'II', 'iii', 'iv', 'V', 'vi', 'vii']], + ['C mixolydian', ['I', 'ii', 'iii', 'IV', 'v', 'vi', 'VII']], + ['C minor', ['i', 'ii', 'III', 'iv', 'v', 'VI', 'VII']], + ['C locrian', ['i', 'II', 'iii', 'iv', 'V', 'VI', 'vii']], +] as const; + +describe('getScaleDegrees', () => { + it.each(testSet)('gets degrees for %s', (modeName, result) => { + expect(getScaleDegrees(modeName)).toStrictEqual(result); + }); +}); diff --git a/src/scale/getScaleDegrees.ts b/src/scale/getScaleDegrees.ts new file mode 100644 index 0000000..bd17884 --- /dev/null +++ b/src/scale/getScaleDegrees.ts @@ -0,0 +1,19 @@ +import { romanNumeralCase } from '../chords/helpers.js'; +import { majorScaleQualities, romanNumerals, scaleTypes } from '../consts.js'; +import { offsetArr } from '../helper.js'; +import { isScaleType } from '../modes/helpers.js'; +import { extractScaleName } from './extractName.js'; + +export const getScaleDegrees = (scaleName: string) => { + const [_, scaleType] = extractScaleName(scaleName) || []; + if (scaleType === undefined || !isScaleType(scaleType)) return []; + + const reOrdered = offsetArr( + majorScaleQualities, + scaleTypes.indexOf(scaleType) + ); + + return romanNumerals.map((numeral, i) => { + return romanNumeralCase(numeral, reOrdered[i]); + }); +}; diff --git a/src/scale/guessScale.test.ts b/src/scale/guessScale.test.ts index 9fedd18..96a64ee 100644 --- a/src/scale/guessScale.test.ts +++ b/src/scale/guessScale.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { guessScale } from './guessScale'; +import { guessScale } from './guessScale.js'; describe('guessScale', () => { const testSongs = [ @@ -7,43 +7,55 @@ describe('guessScale', () => { title: 'Dancing Queen', includedNotes: ['D', 'E', 'F#', 'G#', 'A', 'B'], excludedNotes: [], - key: 'A major', + key: ['A major'], }, { title: "Say It Ain't So", includedNotes: ['D#', 'F', 'G', 'G#', 'A#', 'C', 'D'], excludedNotes: [], - key: 'Eb major', + key: ['Eb major'], }, { title: 'Conspiracy', includedNotes: ['C', 'D', 'E', 'F', 'G', 'A#'], excludedNotes: [], - key: 'F major', + key: ['F major'], }, { title: 'Last Nite', includedNotes: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], excludedNotes: [], - key: 'C major', + key: ['C major'], }, { title: 'The Dress', includedNotes: ['C', 'C#', 'D#', 'F', 'G', 'G#', 'A#'], excludedNotes: [], - key: 'Ab major', + key: ['Ab major'], }, { title: 'Fell in Love Without You', includedNotes: ['C#', 'D#', 'E'], excludedNotes: ['C', 'D', 'F', 'G', 'A'], - key: 'B major', + key: ['Cb major', 'B major'], + }, + { + title: 'Some cool song', + includedNotes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'F'], + excludedNotes: [], + key: ['Gb major', 'F# major'], + }, + { + title: 'Some cool other song', + includedNotes: ['C#', 'D#', 'F', 'F#', 'G#', 'A#', 'C'], + excludedNotes: [], + key: ['Db major', 'C# major'], }, ]; it.each(testSongs)('$title', ({ includedNotes, excludedNotes, key }) => { expect( - guessScale(includedNotes, excludedNotes).map((x) => x.name) - ).toStrictEqual([key]); + guessScale(includedNotes, excludedNotes).map(([name]) => name) + ).toStrictEqual(key); }); }); diff --git a/src/scale/guessScale.ts b/src/scale/guessScale.ts index c224115..ab0041e 100644 --- a/src/scale/guessScale.ts +++ b/src/scale/guessScale.ts @@ -1,58 +1,36 @@ -import { majorScales } from '../consts'; -import { getEquivalentNote } from '../notes/getEquivalentNote'; +import { ScaleGroups, scales } from '../db/scales/allScales.js'; +import { noteToInteger } from '../notes/noteToInteger.js'; -// export const determineTonicFromNotes = (arr: string[]) => { -// const noteFrequency = Object.entries( -// arr.reduce>((accum, note) => { -// if (accum[note]) { -// accum[note] += accum[note]; -// } else { -// accum[note] = 1; -// } - -// return accum; -// }, {}) -// ).sort((a, b) => { -// return b[1] - a[1]; -// }); - -// return noteFrequency[0]; -// }; +type KeyGuess = { name?: string; scale: string[] }; -const filterScalesFromNotes = ( - notes: string[], - scales: string[][], - exclusive = false -) => { - return scales.filter((scale) => { - if (exclusive) { - return !notes.some( - (note) => - scale.includes(note) || scale.includes(getEquivalentNote(note)) - ); - } else { - return notes.every( - (note) => - scale.includes(note) || scale.includes(getEquivalentNote(note)) - ); - } - }); +const getScaleIntegersObj = (name: ScaleGroups) => { + return Object.entries(scales[name]).reduce>( + (accum, [scaleName, scaleInfo]) => { + accum[`${scaleName} ${name}`] = scaleInfo.integers; + return accum; + }, + {} + ); }; -type KeyGuess = { name?: string; scale: string[] }; - export const guessScale = ( includedNotes: string[], excludedNotes: string[] -): KeyGuess[] => { - const allMajors = Object.values(majorScales); - const possibleScales = filterScalesFromNotes(excludedNotes, allMajors, true); - const filteredScales = filterScalesFromNotes(includedNotes, possibleScales); +) => { + const includedIntegers = includedNotes.map(noteToInteger); + const excludedIntegers = excludedNotes.map(noteToInteger); + const majorScales = getScaleIntegersObj('major'); + const possibleScales = Object.entries(majorScales).filter( + ([scaleName, scale]) => { + return !scale.some((note) => excludedIntegers.includes(note)); + } + ); - return filteredScales.map((scale) => { - return { - name: `${scale?.at(0)} major`, - scale, - }; + const result = possibleScales.filter(([name, scale]) => { + return includedIntegers.every((note) => { + return scale.includes(note); + }); }); + + return result; }; diff --git a/src/scale/helpers.ts b/src/scale/helpers.ts new file mode 100644 index 0000000..802448c --- /dev/null +++ b/src/scale/helpers.ts @@ -0,0 +1,26 @@ +import { ScaleGroups } from '../db/scales/allScales.js'; + +export const getFriendlyModeName = ( + modeName: string | undefined +): ScaleGroups => { + switch (modeName) { + case 'major': + case 'ionian': + return 'major'; + case 'dorian': + return 'dorian'; + case 'phrygian': + return 'phrygian'; + case 'lydian': + return 'lydian'; + case 'mixolydian': + return 'mixolydian'; + case 'minor': + case 'aeolian': + return 'minor'; + case 'locrian': + return 'locrian'; + default: + return 'major'; + } +}; diff --git a/src/scale/isValidScale.test.ts b/src/scale/isValidScale.test.ts index 5aff4bb..568e559 100644 --- a/src/scale/isValidScale.test.ts +++ b/src/scale/isValidScale.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from 'vitest'; -import { isValidScale } from './isValidScale'; +import { isValidScale } from './isValidScale.js'; describe('isValidScale', () => { - it('should return false in these cases', () => { - expect(isValidScale(['E', 'Gb', 'Ab', 'A', 'B', 'Db', 'Eb'])).toEqual(false); - }); + it('should return false in these cases', () => { + expect(isValidScale(['E', 'Gb', 'Ab', 'A', 'B', 'Db', 'Eb'])).toEqual( + false + ); + }); }); diff --git a/src/scale/isValidScale.ts b/src/scale/isValidScale.ts index 93b30ec..500ca41 100644 --- a/src/scale/isValidScale.ts +++ b/src/scale/isValidScale.ts @@ -1,6 +1,10 @@ export const isValidScale = (notes: string[]) => { - const duplicatesRegexp = new RegExp(/(A{2}|B{2}|C{2}|D{2}|E{2}|F{2}|G{2})/); - const filteredNotes = [...notes].sort().join('').replaceAll('b', '').replaceAll('#', ''); + const duplicatesRegexp = new RegExp(/(A{2}|B{2}|C{2}|D{2}|E{2}|F{2}|G{2})/); + const filteredNotes = [...notes] + .sort() + .join('') + .replaceAll('b', '') + .replaceAll('#', ''); - return !duplicatesRegexp.test(filteredNotes); + return !duplicatesRegexp.test(filteredNotes); }; diff --git a/src/scale/scaleToMidiKeys.test.ts b/src/scale/scaleToMidiKeys.test.ts new file mode 100644 index 0000000..b74d5d4 --- /dev/null +++ b/src/scale/scaleToMidiKeys.test.ts @@ -0,0 +1,206 @@ +import { describe, expect, it } from 'vitest'; +import { ScaleKey, scaleToPianoKeys } from './scaleToMidiKeys.js'; + +export const majorScales: [string, string[], ScaleKey[]][] = [ + [ + 'C', + ['C', 'D', 'E', 'F', 'G', 'A', 'B'], + [ + { midiNumber: 0, note: 'C' }, + { midiNumber: 2, note: 'D' }, + { midiNumber: 4, note: 'E' }, + { midiNumber: 5, note: 'F' }, + { midiNumber: 7, note: 'G' }, + { midiNumber: 9, note: 'A' }, + { midiNumber: 11, note: 'B' }, + ], + ], + [ + 'F', + ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], + [ + { midiNumber: 5, note: 'F' }, + { midiNumber: 7, note: 'G' }, + { midiNumber: 9, note: 'A' }, + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 12, note: 'C' }, + { midiNumber: 14, note: 'D' }, + { midiNumber: 16, note: 'E' }, + ], + ], + [ + 'Bb', + ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], + [ + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 12, note: 'C' }, + { midiNumber: 14, note: 'D' }, + { midiNumber: 15, note: 'Eb' }, + { midiNumber: 17, note: 'F' }, + { midiNumber: 19, note: 'G' }, + { midiNumber: 21, note: 'A' }, + ], + ], + [ + 'Eb', + ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D'], + [ + { midiNumber: 3, note: 'Eb' }, + { midiNumber: 5, note: 'F' }, + { midiNumber: 7, note: 'G' }, + { midiNumber: 8, note: 'Ab' }, + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 12, note: 'C' }, + { midiNumber: 14, note: 'D' }, + ], + ], + [ + 'Ab', + ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G'], + [ + { midiNumber: 8, note: 'Ab' }, + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 12, note: 'C' }, + { midiNumber: 13, note: 'Db' }, + { midiNumber: 15, note: 'Eb' }, + { midiNumber: 17, note: 'F' }, + { midiNumber: 19, note: 'G' }, + ], + ], + [ + 'Db', + ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'], + [ + { midiNumber: 1, note: 'Db' }, + { midiNumber: 3, note: 'Eb' }, + { midiNumber: 5, note: 'F' }, + { midiNumber: 6, note: 'Gb' }, + { midiNumber: 8, note: 'Ab' }, + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 12, note: 'C' }, + ], + ], + [ + 'Gb', + ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F'], + [ + { midiNumber: 6, note: 'Gb' }, + { midiNumber: 8, note: 'Ab' }, + { midiNumber: 10, note: 'Bb' }, + { midiNumber: 11, note: 'Cb' }, + { midiNumber: 13, note: 'Db' }, + { midiNumber: 15, note: 'Eb' }, + { midiNumber: 17, note: 'F' }, + ], + ], + [ + 'Cb', + ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'], + [ + { midiNumber: 11, note: 'Cb' }, + { midiNumber: 13, note: 'Db' }, + { midiNumber: 15, note: 'Eb' }, + { midiNumber: 16, note: 'Fb' }, + { midiNumber: 18, note: 'Gb' }, + { midiNumber: 20, note: 'Ab' }, + { midiNumber: 22, note: 'Bb' }, + ], + ], + [ + 'G', + ['G', 'A', 'B', 'C', 'D', 'E', 'F#'], + [ + { midiNumber: 7, note: 'G' }, + { midiNumber: 9, note: 'A' }, + { midiNumber: 11, note: 'B' }, + { midiNumber: 12, note: 'C' }, + { midiNumber: 14, note: 'D' }, + { midiNumber: 16, note: 'E' }, + { midiNumber: 18, note: 'F#' }, + ], + ], + [ + 'D', + ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'], + [ + { midiNumber: 2, note: 'D' }, + { midiNumber: 4, note: 'E' }, + { midiNumber: 6, note: 'F#' }, + { midiNumber: 7, note: 'G' }, + { midiNumber: 9, note: 'A' }, + { midiNumber: 11, note: 'B' }, + { midiNumber: 13, note: 'C#' }, + ], + ], + [ + 'A', + ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'], + [ + { midiNumber: 9, note: 'A' }, + { midiNumber: 11, note: 'B' }, + { midiNumber: 13, note: 'C#' }, + { midiNumber: 14, note: 'D' }, + { midiNumber: 16, note: 'E' }, + { midiNumber: 18, note: 'F#' }, + { midiNumber: 20, note: 'G#' }, + ], + ], + [ + 'E', + ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'], + [ + { midiNumber: 4, note: 'E' }, + { midiNumber: 6, note: 'F#' }, + { midiNumber: 8, note: 'G#' }, + { midiNumber: 9, note: 'A' }, + { midiNumber: 11, note: 'B' }, + { midiNumber: 13, note: 'C#' }, + { midiNumber: 15, note: 'D#' }, + ], + ], + [ + 'B', + ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'], + [ + { midiNumber: 11, note: 'B' }, + { midiNumber: 13, note: 'C#' }, + { midiNumber: 15, note: 'D#' }, + { midiNumber: 16, note: 'E' }, + { midiNumber: 18, note: 'F#' }, + { midiNumber: 20, note: 'G#' }, + { midiNumber: 22, note: 'A#' }, + ], + ], + [ + "'F#'", + ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E#'], + [ + { midiNumber: 6, note: 'F#' }, + { midiNumber: 8, note: 'G#' }, + { midiNumber: 10, note: 'A#' }, + { midiNumber: 11, note: 'B' }, + { midiNumber: 13, note: 'C#' }, + { midiNumber: 15, note: 'D#' }, + { midiNumber: 17, note: 'E#' }, + ], + ], + [ + "'C#'", + ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], + [ + { midiNumber: 1, note: 'C#' }, + { midiNumber: 3, note: 'D#' }, + { midiNumber: 5, note: 'E#' }, + { midiNumber: 6, note: 'F#' }, + { midiNumber: 8, note: 'G#' }, + { midiNumber: 10, note: 'A#' }, + { midiNumber: 12, note: 'B#' }, + ], + ], +]; + +describe('scaleToMidiKeys', () => { + it.each(majorScales)('gets degrees for %s', (_, scale, result) => { + expect(scaleToPianoKeys(scale)).toStrictEqual(result); + }); +}); diff --git a/src/scale/scaleToMidiKeys.ts b/src/scale/scaleToMidiKeys.ts new file mode 100644 index 0000000..273374d --- /dev/null +++ b/src/scale/scaleToMidiKeys.ts @@ -0,0 +1,11 @@ +import { scaleToSequentialKeys } from './scaleToMidiNumbers.js'; + +export const scaleToPianoKeys = (scale: string[]) => { + const integers = scaleToSequentialKeys(scale); + + return scale.map((note, i) => { + return { midiNumber: integers[i], note }; + }); +}; + +export type ScaleKey = ReturnType[number]; diff --git a/src/scale/scaleToMidiNumbers.test.ts b/src/scale/scaleToMidiNumbers.test.ts new file mode 100644 index 0000000..ceaa7e2 --- /dev/null +++ b/src/scale/scaleToMidiNumbers.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import { scaleToSequentialKeys } from './scaleToMidiNumbers.js'; + +describe('scaleToIntegers', () => { + it('converts an array of notes to an array of integers', () => { + expect( + scaleToSequentialKeys(['C', 'D', 'E', 'F', 'G', 'A', 'B']) + ).toStrictEqual([0, 2, 4, 5, 7, 9, 11]); + }); +}); diff --git a/src/scale/scaleToMidiNumbers.ts b/src/scale/scaleToMidiNumbers.ts new file mode 100644 index 0000000..122908b --- /dev/null +++ b/src/scale/scaleToMidiNumbers.ts @@ -0,0 +1,15 @@ +import { noteToMidi } from '../notes/noteToMidi.js'; + +export const scaleToSequentialKeys = (scale: string[], octave = -1) => { + const startingNote = noteToMidi(`${scale[0]}${octave}`); + + return scale.map((note) => { + const midiNumber = noteToMidi(`${note}${octave}`); + + if (midiNumber !== undefined && startingNote !== undefined) { + return midiNumber >= startingNote ? midiNumber : midiNumber + 12; + } else { + return 120; + } + }); +}; diff --git a/src/types.ts b/src/types.ts index 3a99c50..b7ced1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,97 +1,109 @@ -import type { scaleIntervals } from "./consts"; +import type { majorScaleQualities, scaleIntervals } from './consts.js'; + +export type Enumerate< + N extends number, + Acc extends number[] = [] +> = Acc['length'] extends N + ? Acc[number] + : Enumerate; + +export type Range = Exclude< + Enumerate, + Enumerate +>; export type TIntervalQuality = - | "perfect" - | "major" - | "minor" - | "augmented" - | "diminished"; + | 'perfect' + | 'major' + | 'minor' + | 'augmented' + | 'diminished'; export type TChordQuality = - | "major" - | "minor" - | "augmented" - | "diminished" - | "half-diminished" - | "dominant"; + | 'major' + | 'minor' + | 'augmented' + | 'diminished' + | 'half-diminished' + | 'dominant'; export type TMode = - | "ionian" - | "dorian" - | "phrygian" - | "lydian" - | "myxolydian" - | "aeolian" - | "locrian"; + | 'ionian' + | 'dorian' + | 'phrygian' + | 'lydian' + | 'mixolydian' + | 'aeolian' + | 'locrian'; export type TChordType = - | "second" - | "triad" - | "fourth" - | "fifth" - | "sixth" - | "seventh" - | "ninth" - | "eleventh" - | "thirteenth"; + | 'second' + | 'triad' + | 'fourth' + | 'fifth' + | 'sixth' + | 'seventh' + | 'ninth' + | 'eleventh' + | 'thirteenth'; export type TIntervalShorthand = - | "d2" - | "P1" - | "A1" - | "d3" - | "m2" - | "M2" - | "A2" - | "m3" - | "d4" - | "M3" - | "A3" - | "P4" - | "A4" - | "d5" - | "d6" - | "P5" - | "A5" - | "m6" - | "d7" - | "M6" - | "A6" - | "m7" - | "d8" - | "M7" - | "A7" - | "P8" - | "d9" - | "m9" - | "A8" - | "d10" - | "M9" - | "A9" - | "m10" - | "d11" - | "M10" - | "A10" - | "P11" - | "A11" - | "d12" - | "P12" - | "d13" - | "m13" - | "A12" - | "M13" - | "d14" - | "m14" - | "A13" - | "M14" - | "d15" - | "P15" - | "A14" - | "A15"; + | 'd2' + | 'P1' + | 'A1' + | 'd3' + | 'm2' + | 'M2' + | 'A2' + | 'm3' + | 'd4' + | 'M3' + | 'A3' + | 'P4' + | 'A4' + | 'd5' + | 'd6' + | 'P5' + | 'A5' + | 'm6' + | 'd7' + | 'M6' + | 'A6' + | 'm7' + | 'd8' + | 'M7' + | 'A7' + | 'P8' + | 'd9' + | 'm9' + | 'A8' + | 'd10' + | 'M9' + | 'A9' + | 'm10' + | 'd11' + | 'M10' + | 'A10' + | 'P11' + | 'A11' + | 'd12' + | 'P12' + | 'd13' + | 'm13' + | 'A12' + | 'M13' + | 'd14' + | 'm14' + | 'A13' + | 'M14' + | 'd15' + | 'P15' + | 'A14' + | 'A15'; export type TScaleType = keyof typeof scaleIntervals; -export type TAddType = "add2" | "add4" | "add9" | "add11" | "add13"; -export type TSusType = "sus2" | "sus4"; +export type TAddType = 'add2' | 'add4' | 'add9' | 'add11' | 'add13'; +export type TSusType = 'sus2' | 'sus4'; export interface IChordInfo { name: string; @@ -102,9 +114,16 @@ export interface IChordInfo { addType?: TAddType; susType?: TSusType; slashNote?: string; + pitchClassType: NoteType; } export interface IChord { name: string; notes: string[]; } + +export type ScaleQualities = (typeof majorScaleQualities)[number]; + +export type MIDINumber = Range<0, 127>; + +export type NoteType = 'flat' | 'sharp' | 'natural'; From e96f9510dad44ac4f4d0beb5b904a0ac28477aff Mon Sep 17 00:00:00 2001 From: Kilian McMahon Date: Thu, 21 Sep 2023 21:32:16 +0200 Subject: [PATCH 2/5] chore: tidy things up --- src/chords/determineChordInfo.ts | 20 +-- src/chords/getChord.test.ts | 2 +- src/chords/getChord.ts | 10 +- src/chords/getScaleChords.test.ts | 116 +++++++++++++ src/chords/getScaleChords.ts | 26 ++- src/chords/helpers.ts | 16 +- src/consts.ts | 22 +-- src/db/scales/allScales.ts | 4 - src/db/scales/major.ts | 62 ------- src/helper.ts | 8 +- src/keys/getKey.test.ts | 162 +++++++++++++++--- src/keys/getKey.ts | 37 ++-- src/keys/helpers.ts | 4 +- src/main.ts | 37 ++-- src/modes/helpers.ts | 6 +- src/notes/determineNoteType.ts | 5 +- src/notes/getInterval.ts | 4 +- src/notes/getScaleDegreeOfNote.ts | 4 +- src/notes/intervalToSemitone.ts | 4 +- src/notes/noteToMidi.ts | 1 - src/notes/transpose.test.ts | 6 +- src/notes/transpose.ts | 66 +++---- src/scale/extractName.ts | 13 +- src/scale/getMajorFromMode.ts | 7 +- src/scale/getScale.test.ts | 40 +---- ...sScale.test.ts => guessMajorScale.test.ts} | 12 +- .../{guessScale.ts => guessMajorScale.ts} | 4 +- ...iKeys.test.ts => scaleToPianoKeys.test.ts} | 4 +- ...scaleToMidiKeys.ts => scaleToPianoKeys.ts} | 2 +- ....test.ts => scaleToSequentialKeys.test.ts} | 4 +- ...idiNumbers.ts => scaleToSequentialKeys.ts} | 0 src/types.ts | 27 +-- 32 files changed, 450 insertions(+), 285 deletions(-) create mode 100644 src/chords/getScaleChords.test.ts delete mode 100644 src/db/scales/major.ts rename src/scale/{guessScale.test.ts => guessMajorScale.test.ts} (81%) rename src/scale/{guessScale.ts => guessMajorScale.ts} (92%) rename src/scale/{scaleToMidiKeys.test.ts => scaleToPianoKeys.test.ts} (98%) rename src/scale/{scaleToMidiKeys.ts => scaleToPianoKeys.ts} (79%) rename src/scale/{scaleToMidiNumbers.test.ts => scaleToSequentialKeys.test.ts} (69%) rename src/scale/{scaleToMidiNumbers.ts => scaleToSequentialKeys.ts} (100%) diff --git a/src/chords/determineChordInfo.ts b/src/chords/determineChordInfo.ts index 41d81c1..9d54469 100644 --- a/src/chords/determineChordInfo.ts +++ b/src/chords/determineChordInfo.ts @@ -1,11 +1,11 @@ import { numberTypeChordMap } from '../consts.js'; import { determineNoteType } from '../notes/determineNoteType.js'; import type { - IChordInfo, - TAddType, - TChordQuality, - TChordType, - TSusType, + ChordInfo, + AddType, + ChordQuality, + ChordType, + SusType, } from '../types.js'; import { chordRegexp, isAddType } from './helpers.js'; @@ -15,7 +15,7 @@ const determineAlteredNotes = (alteredNotes?: string) => { : (alteredNotes.match(/(#|b)(?:5|7|9|11|13)/g) as string[]); }; -const determineChordQuality = (quality?: string): TChordQuality => { +const determineChordQuality = (quality?: string): ChordQuality => { if (quality === undefined || ['Δ', 'M', 'maj'].includes(quality)) { return 'major'; } @@ -39,15 +39,15 @@ const determineChordQuality = (quality?: string): TChordQuality => { return 'major'; }; -const determindChordType = (type?: string): TChordType => { +const determindChordType = (type?: string): ChordType => { return type === undefined ? 'triad' : numberTypeChordMap[type]; }; -const determineAdd = (add?: string): TAddType | undefined => { +const determineAdd = (add?: string): AddType | undefined => { return add && isAddType(add) ? add : undefined; }; -const determineSus = (sus?: string): TSusType | undefined => { +const determineSus = (sus?: string): SusType | undefined => { if (sus === undefined) return undefined; if (['sus', 'sus4', 'sus9'].includes(sus)) return 'sus4'; if (sus === 'sus2') return 'sus2'; @@ -58,7 +58,7 @@ const determineSlashChord = (slashNote?: string) => { return slashNote?.replace('/', ''); }; -export const determineChord = (name: string): IChordInfo => { +export const determineChord = (name: string): ChordInfo => { const [note, quality, type, altered, add, sus, slashNote] = name.match(chordRegexp)?.slice(1) || []; diff --git a/src/chords/getChord.test.ts b/src/chords/getChord.test.ts index 4e20b5b..6af0b7d 100644 --- a/src/chords/getChord.test.ts +++ b/src/chords/getChord.test.ts @@ -7,7 +7,7 @@ describe('getChord', () => { expect(getChord('Eb').notes).toEqual(['Eb', 'G', 'Bb']); }); - it.only('supports minor chords', () => { + it('supports minor chords', () => { expect(getChord('Am').notes).toEqual(['A', 'C', 'E']); expect(getChord('D#m').notes).toEqual(['D#', 'F#', 'A#']); }); diff --git a/src/chords/getChord.ts b/src/chords/getChord.ts index e91e817..f23443a 100644 --- a/src/chords/getChord.ts +++ b/src/chords/getChord.ts @@ -1,8 +1,7 @@ import { chordQualityIntervalsMap } from '../consts.js'; import { offsetArr } from '../helper.js'; -import { determineNoteType } from '../notes/determineNoteType.js'; import { transposeNote } from '../notes/transpose.js'; -import type { IChord, IChordInfo, TIntervalShorthand } from '../types.js'; +import type { Chord, ChordInfo, IntervalShorthand } from '../types.js'; import { determineChord } from './determineChordInfo.js'; const chordInfoToIntervalMap = ({ @@ -12,8 +11,8 @@ const chordInfoToIntervalMap = ({ type, quality, susType, -}: IChordInfo): TIntervalShorthand[] => { - let intervals: TIntervalShorthand[] = chordQualityIntervalsMap[quality]; +}: ChordInfo): IntervalShorthand[] => { + let intervals: IntervalShorthand[] = chordQualityIntervalsMap[quality]; if (type === 'fifth') { intervals = intervals.filter((interval) => !interval.includes('3')); @@ -83,7 +82,7 @@ const chordInfoToIntervalMap = ({ return intervals; }; -export const getChord = (name: string): IChord => { +export const getChord = (name: string): Chord => { if (name === '') { return { name: `Type a chord`, @@ -105,6 +104,7 @@ export const getChord = (name: string): IChord => { return transposeNote(chordInfo.note, interval, { forceFlat: chordInfo.pitchClassType === 'flat', forceSharp: chordInfo.pitchClassType === 'sharp', + forceSimple: true, }); }); const { slashNote } = chordInfo; diff --git a/src/chords/getScaleChords.test.ts b/src/chords/getScaleChords.test.ts new file mode 100644 index 0000000..66af2ae --- /dev/null +++ b/src/chords/getScaleChords.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from 'vitest'; +import { getScaleChords } from './getScaleChords.js'; + +describe('getScaleChords', () => { + it('supports major scales', () => { + expect(getScaleChords(['C', 'D', 'E', 'F', 'G', 'A', 'B'])).toEqual([ + { name: 'C', notes: ['C', 'E', 'G'], romanNumeral: 'I' }, + { name: 'Dm', notes: ['D', 'F', 'A'], romanNumeral: 'ii' }, + { name: 'Em', notes: ['E', 'G', 'B'], romanNumeral: 'iii' }, + { name: 'F', notes: ['F', 'A', 'C'], romanNumeral: 'IV' }, + { name: 'G', notes: ['G', 'B', 'D'], romanNumeral: 'V' }, + { name: 'Am', notes: ['A', 'C', 'E'], romanNumeral: 'vi' }, + { name: 'Bdim', notes: ['B', 'D', 'F'], romanNumeral: 'vii' }, + ]); + + expect(getScaleChords(['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'])).toEqual([ + { name: 'Db', notes: ['Db', 'F', 'Ab'], romanNumeral: 'I' }, + { name: 'Ebm', notes: ['Eb', 'Gb', 'Bb'], romanNumeral: 'ii' }, + { name: 'Fm', notes: ['F', 'Ab', 'C'], romanNumeral: 'iii' }, + { name: 'Gb', notes: ['Gb', 'Bb', 'Db'], romanNumeral: 'IV' }, + { name: 'Ab', notes: ['Ab', 'C', 'Eb'], romanNumeral: 'V' }, + { name: 'Bbm', notes: ['Bb', 'Db', 'F'], romanNumeral: 'vi' }, + { name: 'Cdim', notes: ['C', 'Eb', 'Gb'], romanNumeral: 'vii' }, + ]); + }); + + it('supports dorian scales', () => { + const result = [ + { name: 'Bm', notes: ['B', 'D#', 'F#'], romanNumeral: 'i' }, + { name: 'C#m', notes: ['C#', 'E', 'G#'], romanNumeral: 'ii' }, + { name: 'D#', notes: ['D#', 'F#', 'A#'], romanNumeral: 'III' }, + { name: 'E', notes: ['E', 'G#', 'B'], romanNumeral: 'IV' }, + { name: 'F#m', notes: ['F#', 'A#', 'C#'], romanNumeral: 'v' }, + { name: 'G#dim', notes: ['G#', 'B', 'D#'], romanNumeral: 'vi' }, + { name: 'A#', notes: ['A#', 'C#', 'E'], romanNumeral: 'VII' }, + ]; + const scale = ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#']; + + expect(getScaleChords(scale, 'dorian')).toEqual(result); + }); + + it('supports phrygian scales', () => { + const result = [ + { name: 'Bbm', notes: ['Bb', 'D', 'F'], romanNumeral: 'i' }, + { name: 'C', notes: ['C', 'Eb', 'G'], romanNumeral: 'II' }, + { name: 'D', notes: ['D', 'F', 'A'], romanNumeral: 'III' }, + { name: 'Ebm', notes: ['Eb', 'G', 'Bb'], romanNumeral: 'iv' }, + { name: 'Fdim', notes: ['F', 'A', 'C'], romanNumeral: 'v' }, + { name: 'G', notes: ['G', 'Bb', 'D'], romanNumeral: 'VI' }, + { name: 'Am', notes: ['A', 'C', 'Eb'], romanNumeral: 'vii' }, + ]; + const scale = ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A']; + + expect(getScaleChords(scale, 'phrygian')).toEqual(result); + }); + + it('supports lydian scales', () => { + const result = [ + { name: 'D', notes: ['D', 'F#', 'A'], romanNumeral: 'I' }, + { name: 'E', notes: ['E', 'G#', 'B'], romanNumeral: 'II' }, + { name: 'F#m', notes: ['F#', 'A', 'C#'], romanNumeral: 'iii' }, + { name: 'G#dim', notes: ['G#', 'B', 'D'], romanNumeral: 'iv' }, + { name: 'A', notes: ['A', 'C#', 'E'], romanNumeral: 'V' }, + { name: 'Bm', notes: ['B', 'D', 'F#'], romanNumeral: 'vi' }, + { name: 'C#m', notes: ['C#', 'E', 'G#'], romanNumeral: 'vii' }, + ]; + const scale = ['D', 'E', 'F#', 'G#', 'A', 'B', 'C#']; + + expect(getScaleChords(scale, 'lydian')).toEqual(result); + }); + + it('supports mixolydian scales', () => { + const result = [ + { name: 'B', notes: ['B', 'D#', 'F#'], romanNumeral: 'I' }, + { name: 'C#m', notes: ['C#', 'E', 'G#'], romanNumeral: 'ii' }, + { name: 'D#dim', notes: ['D#', 'F#', 'A'], romanNumeral: 'iii' }, + { name: 'E', notes: ['E', 'G#', 'B'], romanNumeral: 'IV' }, + { name: 'F#m', notes: ['F#', 'A', 'C#'], romanNumeral: 'v' }, + { name: 'G#m', notes: ['G#', 'B', 'D#'], romanNumeral: 'vi' }, + { name: 'A', notes: ['A', 'C#', 'E'], romanNumeral: 'VII' }, + ]; + const scale = ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A']; + + expect(getScaleChords(scale, 'mixolydian')).toEqual(result); + }); + + it('supports minor scales', () => { + const result = [ + { name: 'Am', notes: ['A', 'C', 'E'], romanNumeral: 'i' }, + { name: 'Bdim', notes: ['B', 'D', 'F'], romanNumeral: 'ii' }, + { name: 'C', notes: ['C', 'E', 'G'], romanNumeral: 'III' }, + { name: 'Dm', notes: ['D', 'F', 'A'], romanNumeral: 'iv' }, + { name: 'Em', notes: ['E', 'G', 'B'], romanNumeral: 'v' }, + { name: 'F', notes: ['F', 'A', 'C'], romanNumeral: 'VI' }, + { name: 'G', notes: ['G', 'B', 'D'], romanNumeral: 'VII' }, + ]; + const scale = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + + expect(getScaleChords(scale, 'minor')).toEqual(result); + }); + + it('supports locrian scales', () => { + const result = [ + { name: 'Edim', notes: ['E', 'G', 'Bb'], romanNumeral: 'i' }, + { name: 'F', notes: ['F', 'A', 'C'], romanNumeral: 'II' }, + { name: 'Gm', notes: ['G', 'Bb', 'D'], romanNumeral: 'iii' }, + { name: 'Am', notes: ['A', 'C', 'E'], romanNumeral: 'iv' }, + { name: 'Bb', notes: ['Bb', 'D', 'F'], romanNumeral: 'V' }, + { name: 'C', notes: ['C', 'E', 'G'], romanNumeral: 'VI' }, + { name: 'Dm', notes: ['D', 'F', 'A'], romanNumeral: 'vii' }, + ]; + const scale = ['E', 'F', 'G', 'A', 'Bb', 'C', 'D']; + + expect(getScaleChords(scale, 'locrian')).toEqual(result); + }); +}); diff --git a/src/chords/getScaleChords.ts b/src/chords/getScaleChords.ts index ab41fff..1d0c7a7 100644 --- a/src/chords/getScaleChords.ts +++ b/src/chords/getScaleChords.ts @@ -1,17 +1,27 @@ -import { majorScaleQualities, modes, scaleTypes } from '../consts.js'; +import { majorScaleQualities, romanNumerals, scaleTypes } from '../consts.js'; import { offsetArr } from '../helper.js'; -import type { TMode, TScaleType } from '../types.js'; -import { getChord } from './getChord.js'; -import { scaleQualitiesToChordSymbol } from './helpers.js'; +import { Chord, TScaleType } from '../types.js'; +import { romanNumeralCase, scaleQualitiesToChordSymbol } from './helpers.js'; -export const getScaleChords = (scale: string[], mode: TScaleType) => { +export const getScaleChords = ( + scale: string[], + scaleType: TScaleType = 'major' +): Chord[] => { + const thirds = offsetArr(scale, 2); + const fifths = offsetArr(scale, 4); const scaleQualities = offsetArr( majorScaleQualities, - scaleTypes.indexOf(mode) + scaleTypes.indexOf(scaleType) ); return scale.map((note, i) => { - const quality = scaleQualitiesToChordSymbol(scaleQualities[i]); - return getChord(`${note}${quality}`); + const chordName = `${note}${scaleQualitiesToChordSymbol( + scaleQualities[i] + )}`; + return { + name: chordName, + notes: [note, thirds[i], fifths[i]], + romanNumeral: romanNumeralCase(romanNumerals[i], scaleQualities[i]), + }; }); }; diff --git a/src/chords/helpers.ts b/src/chords/helpers.ts index 044a634..6f422e2 100644 --- a/src/chords/helpers.ts +++ b/src/chords/helpers.ts @@ -1,24 +1,24 @@ import { addTypes, susTypes } from '../consts.js'; import type { ScaleQualities, - TAddType, - TChordQuality, - TSusType, + AddType, + ChordQuality, + SusType, } from '../types.js'; -export function isAddType(value: string): value is TAddType { - return addTypes.includes(value as TAddType); +export function isAddType(value: string): value is AddType { + return addTypes.includes(value as AddType); } -export function isSusType(value: string): value is TSusType { - return susTypes.includes(value as TSusType); +export function isSusType(value: string): value is SusType { + return susTypes.includes(value as SusType); } export const chordRegexp = new RegExp( /((?:^[A-G])(?:#|b)?)(aug|dim|maj|m|M|o|\+)?(2|4|5|6|7|9|11|13)?((?:(?:#|b)(?:5|7|9|11|13))+)?(add(?:2|4|9|11|13)?)?(sus(?:2|4|9)?)?(\/[A-G](?:#|b)?)?/ ); -export const scaleQualitiesToChordSymbol = (quality: TChordQuality) => { +export const scaleQualitiesToChordSymbol = (quality: ChordQuality) => { switch (quality) { case 'major': return ''; diff --git a/src/consts.ts b/src/consts.ts index 744742b..c79edf0 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,11 +1,11 @@ import type { - TChordQuality, - TChordType, - TIntervalShorthand, - TMode, + ChordQuality, + ChordType, + IntervalShorthand, + Mode, } from './types.js'; -export const intervalsMap: Record = { +export const intervalsMap: Record = { d2: 0, P1: 0, A1: 1, @@ -59,7 +59,7 @@ export const intervalsMap: Record = { A14: 24, A15: 25, }; -export const intervalsBySemitone: Record = { +export const intervalsBySemitone: Record = { '0': ['P1', 'd2'], '1': ['m2', 'A1'], '2': ['M2', 'd3'], @@ -88,7 +88,7 @@ export const intervalsBySemitone: Record = { '25': ['A15'], }; -export const chordQualityIntervalsMap: Record = { +export const chordQualityIntervalsMap: Record = { major: ['P1', 'M3', 'P5'], augmented: ['P1', 'M3', 'A5'], minor: ['P1', 'm3', 'P5'], @@ -105,7 +105,7 @@ export const scaleIntervals = { locrian: ['P1', 'm2', 'm3', 'P4', 'd5', 'm6', 'm7', 'P8'], } as const; -type ScaleType = keyof typeof scaleIntervals; +export type ScaleType = keyof typeof scaleIntervals; export const scaleTypes = Object.keys(scaleIntervals) as ScaleType[]; const futureScaleIntervals = { @@ -168,7 +168,7 @@ export const oldMajorScales: Record = { 'C#': ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], }; -export const modes: TMode[] = [ +export const modes: Mode[] = [ 'ionian', 'dorian', 'phrygian', @@ -177,7 +177,7 @@ export const modes: TMode[] = [ 'aeolian', 'locrian', ]; -export const majorScaleQualities: TChordQuality[] = [ +export const majorScaleQualities: ChordQuality[] = [ 'major', 'minor', 'minor', @@ -200,7 +200,7 @@ export const romanNumerals = [ export const addTypes = ['add2', 'add4', 'add9', 'add11', 'add13']; export const susTypes = ['sus2', 'sus4']; -export const numberTypeChordMap: Record = { +export const numberTypeChordMap: Record = { 2: 'second', 4: 'fourth', 5: 'fifth', diff --git a/src/db/scales/allScales.ts b/src/db/scales/allScales.ts index 784b0be..fde2b4f 100644 --- a/src/db/scales/allScales.ts +++ b/src/db/scales/allScales.ts @@ -550,10 +550,6 @@ export const locrian: Record = { }, }; -type LocrianNames = keyof typeof locrian; - -export const melodicMinor = {}; - export const scales = { major, dorian, diff --git a/src/db/scales/major.ts b/src/db/scales/major.ts deleted file mode 100644 index 1070fa7..0000000 --- a/src/db/scales/major.ts +++ /dev/null @@ -1,62 +0,0 @@ -export const majorScales = { - C: { - midiNumbers: [0, 2, 4, 5, 7, 9, 11], - notes: ['C', 'D', 'E', 'F', 'G', 'A', 'B'], - }, - F: { - midiNumbers: [5, 7, 9, 10, 12, 14, 16], - notes: ['F', 'G', 'A', 'Bb', 'C', 'D', 'E'], - }, - Bb: { - midiNumbers: [10, 12, 14, 15, 17, 19, 21], - notes: ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A'], - }, - Eb: { - midiNumbers: [3, 5, 7, 8, 10, 12, 14], - notes: ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D'], - }, - Ab: { - midiNumbers: [8, 10, 12, 13, 15, 17, 19], - notes: ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G'], - }, - Db: { - midiNumbers: [1, 3, 5, 6, 8, 10, 12], - notes: ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C'], - }, - Gb: { - midiNumbers: [6, 8, 10, 11, 13, 15, 17], - notes: ['Gb', 'Ab', 'Bb', 'Cb', 'Db', 'Eb', 'F'], - }, - Cb: { - midiNumbers: [11, 13, 15, 16, 18, 20, 22], - notes: ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'], - }, - G: { - midiNumbers: [7, 9, 11, 12, 14, 16, 18], - notes: ['G', 'A', 'B', 'C', 'D', 'E', 'F#'], - }, - D: { - midiNumbers: [2, 4, 6, 7, 9, 11, 13], - notes: ['D', 'E', 'F#', 'G', 'A', 'B', 'C#'], - }, - A: { - midiNumbers: [9, 11, 13, 14, 16, 18, 20], - notes: ['A', 'B', 'C#', 'D', 'E', 'F#', 'G#'], - }, - E: { - midiNumbers: [4, 6, 8, 9, 11, 13, 15], - notes: ['E', 'F#', 'G#', 'A', 'B', 'C#', 'D#'], - }, - B: { - midiNumbers: [11, 13, 15, 16, 18, 20, 22], - notes: ['B', 'C#', 'D#', 'E', 'F#', 'G#', 'A#'], - }, - 'F#': { - midiNumbers: [6, 8, 10, 11, 13, 15, 17], - notes: ['F#', 'G#', 'A#', 'B', 'C#', 'D#', 'E#'], - }, - 'C#': { - midiNumbers: [1, 3, 5, 6, 8, 10, 12], - notes: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], - }, -}; diff --git a/src/helper.ts b/src/helper.ts index 4a251f5..e4bc33a 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,13 +1,13 @@ -import type { TMode, TScaleType } from './types.js'; +import type { Mode, TScaleType } from './types.js'; export const isFlat = (str: string) => str.includes('b'); export const isSharp = (str: string) => str.includes('#'); -export const isMajor = (mode: TMode | TScaleType) => +export const isMajor = (mode: Mode | TScaleType) => mode === 'ionian' || mode === 'major'; -export const isMinor = (mode: TMode | TScaleType) => +export const isMinor = (mode: Mode | TScaleType) => mode === 'aeolian' || mode === 'minor'; -export const isMode = (mode: TMode) => !['aeolian', 'ionian'].includes(mode); +export const isMode = (mode: Mode) => !['aeolian', 'ionian'].includes(mode); // Arr diff --git a/src/keys/getKey.test.ts b/src/keys/getKey.test.ts index f2fd46e..7ded722 100644 --- a/src/keys/getKey.test.ts +++ b/src/keys/getKey.test.ts @@ -3,32 +3,140 @@ import { getKey } from './getKey.js'; describe('getKey', () => { it('gets Major keys', () => { - expect(getKey('C major')?.major.notes).toEqual([ - 'C', - 'D', - 'E', - 'F', - 'G', - 'A', - 'B', - ]); - expect(getKey('C major')?.minor.notes).toEqual([ - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - ]); - expect(getKey('C major')?.chords.map((chord) => chord.name)).toEqual([ - 'C', - 'Dm', - 'Em', - 'F', - 'G', - 'Am', - 'Bdim', - ]); + const result = getKey('C major'); + const majorNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + const minorNotes = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + const chordNames = ['C', 'Dm', 'Em', 'F', 'G', 'Am', 'Bdim']; + + expect(result?.name).toEqual('C major'); + expect(result?.notes.names).toEqual(majorNotes); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Ionian modes', () => { + const result = getKey('C ionian'); + const majorNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; + const minorNotes = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; + const chordNames = ['C', 'Dm', 'Em', 'F', 'G', 'Am', 'Bdim']; + + expect(result?.name).toEqual('C major'); + expect(result?.notes.names).toEqual(majorNotes); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Dorian modes', () => { + const result = getKey('C dorian'); + const scaleNotes = ['C', 'D', 'Eb', 'F', 'G', 'A', 'Bb']; + const majorNotes = ['Bb', 'C', 'D', 'Eb', 'F', 'G', 'A']; + const minorNotes = ['G', 'A', 'Bb', 'C', 'D', 'Eb', 'F']; + const chordNames = ['Cm', 'Dm', 'Eb', 'F', 'Gm', 'Adim', 'Bb']; + + expect(result?.name).toEqual('C dorian'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('Bb major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('G minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Phrygian modes', () => { + const result = getKey('C phrygian'); + const scaleNotes = ['C', 'Db', 'Eb', 'F', 'G', 'Ab', 'Bb']; + const majorNotes = ['Ab', 'Bb', 'C', 'Db', 'Eb', 'F', 'G']; + const minorNotes = ['F', 'G', 'Ab', 'Bb', 'C', 'Db', 'Eb']; + const chordNames = ['Cm', 'Db', 'Eb', 'Fm', 'Gdim', 'Ab', 'Bbm']; + + expect(result?.name).toEqual('C phrygian'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('Ab major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('F minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Lydian modes', () => { + const result = getKey('C lydian'); + const scaleNotes = ['C', 'D', 'E', 'F#', 'G', 'A', 'B']; + const majorNotes = ['G', 'A', 'B', 'C', 'D', 'E', 'F#']; + const minorNotes = ['E', 'F#', 'G', 'A', 'B', 'C', 'D']; + const chordNames = ['C', 'D', 'Em', 'F#dim', 'G', 'Am', 'Bm']; + + expect(result?.name).toEqual('C lydian'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('G major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('E minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Mixolydian modes', () => { + const result = getKey('C mixolydian'); + const scaleNotes = ['C', 'D', 'E', 'F', 'G', 'A', 'Bb']; + const majorNotes = ['F', 'G', 'A', 'Bb', 'C', 'D', 'E']; + const minorNotes = ['D', 'E', 'F', 'G', 'A', 'Bb', 'C']; + const chordNames = ['C', 'Dm', 'Edim', 'F', 'Gm', 'Am', 'Bb']; + + expect(result?.name).toEqual('C mixolydian'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('F major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('D minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Minor modes', () => { + const result = getKey('C minor'); + const scaleNotes = ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']; + const majorNotes = ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D']; + const minorNotes = ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']; + const chordNames = ['Cm', 'Ddim', 'Eb', 'Fm', 'Gm', 'Ab', 'Bb']; + + expect(result?.name).toEqual('C minor'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('Eb major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('C minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Aeolian modes', () => { + const result = getKey('C aeolian'); + const scaleNotes = ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']; + const majorNotes = ['Eb', 'F', 'G', 'Ab', 'Bb', 'C', 'D']; + const minorNotes = ['C', 'D', 'Eb', 'F', 'G', 'Ab', 'Bb']; + const chordNames = ['Cm', 'Ddim', 'Eb', 'Fm', 'Gm', 'Ab', 'Bb']; + + expect(result?.name).toEqual('C minor'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('Eb major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('C minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); + }); + + it('gets Locrian modes', () => { + const result = getKey('C locrian'); + const scaleNotes = ['C', 'Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb']; + const majorNotes = ['Db', 'Eb', 'F', 'Gb', 'Ab', 'Bb', 'C']; + const minorNotes = ['Bb', 'C', 'Db', 'Eb', 'F', 'Gb', 'Ab']; + const chordNames = ['Cdim', 'Db', 'Ebm', 'Fm', 'Gb', 'Ab', 'Bbm']; + + expect(result?.name).toEqual('C locrian'); + expect(result?.notes.names).toEqual(scaleNotes); + expect(result?.major.name).toEqual('Db major'); + expect(result?.major.notes).toEqual(majorNotes); + expect(result?.minor.name).toEqual('Bb minor'); + expect(result?.minor.notes).toEqual(minorNotes); + expect(result?.chords.map((chord) => chord.name)).toEqual(chordNames); }); }); diff --git a/src/keys/getKey.ts b/src/keys/getKey.ts index 2ac5e06..4d12382 100644 --- a/src/keys/getKey.ts +++ b/src/keys/getKey.ts @@ -6,12 +6,18 @@ import { isScaleType } from '../modes/helpers.js'; import { extractScaleName } from '../scale/extractName.js'; import { getMajorFromMode } from '../scale/getMajorFromMode.js'; import { getFriendlyModeName } from '../scale/helpers.js'; -import type { IChord, TMode } from '../types.js'; +import { scaleToPianoKeys } from '../scale/scaleToPianoKeys.js'; +import type { Chord, Mode } from '../types.js'; import { getRelativeMinorName } from './helpers.js'; +type ScaleNotes = { + names: string[]; + midiKeys: number[]; +}; + type KeyInfo = { name: string; - notes: string[]; + notes: ScaleNotes; major: { name: string; notes: string[]; @@ -20,20 +26,16 @@ type KeyInfo = { name: string; notes: string[]; }; - modes(name: TMode): string[]; - chords: IChord[]; + modes(name: Mode): string[]; + chords: Chord[]; }; export const getKey = (key: string): KeyInfo | undefined => { const [pitch, mode] = extractScaleName(key) || []; const friendlyModeName = getFriendlyModeName(mode); - if (mode === undefined || pitch === undefined) { - return; - } + if (mode === undefined || pitch === undefined) return undefined; - if (!isScaleType(friendlyModeName)) { - return; - } + if (!isScaleType(friendlyModeName)) return undefined; const keyQuality = friendlyModeName; const majorPitch = isMajor(mode) @@ -46,18 +48,27 @@ export const getKey = (key: string): KeyInfo | undefined => { scaleTypes.indexOf(friendlyModeName) ); + const notes = scaleToPianoKeys(scale).reduce((accum, xs) => { + accum.names ? accum.names.push(xs.note) : (accum.names = [xs.note]); + accum.midiKeys + ? accum.midiKeys.push(xs.midiNumber) + : (accum.midiKeys = [xs.midiNumber]); + + return accum; + }, {} as ScaleNotes); + return { name: `${pitch} ${keyQuality}`, - notes: scale, + notes: notes, major: { name: `${majorPitch} major`, notes: majorScale.notes, }, minor: { - name: getRelativeMinorName(pitch), + name: getRelativeMinorName(majorPitch), notes: offsetArr(majorScale.notes, 5), }, - modes(name: TMode) { + modes(name: Mode) { return offsetArr(majorScale.notes, modes.indexOf(name)); }, chords: getScaleChords(scale, friendlyModeName), diff --git a/src/keys/helpers.ts b/src/keys/helpers.ts index e9501b5..7148aa5 100644 --- a/src/keys/helpers.ts +++ b/src/keys/helpers.ts @@ -1,5 +1,5 @@ -import { majorScales } from '../consts.js'; +import { major } from '../db/scales/allScales.js'; export const getRelativeMinorName = (pitchClass: string) => { - return `${majorScales[pitchClass][5]} minor`; + return `${major[pitchClass].notes[5]} minor`; }; diff --git a/src/main.ts b/src/main.ts index 7fb3dc7..c729625 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,33 @@ // Chords -export { getChord } from './chords/getChord'; -export { getScaleChords } from './chords/getScaleChords'; +export { getChord } from './chords/getChord.js'; +export { getScaleChords } from './chords/getScaleChords.js'; // Keys -export { getKey } from './keys/getKey'; - -// Scales -export { guessScale } from './scale/guessScale'; -export { getScale } from './scale/getScale'; +export { getKey } from './keys/getKey.js'; // Notes -export { getEquivalentNote } from './notes/getEquivalentNote'; -export { getInterval } from './notes/getInterval'; -export { transposeNote } from './notes/transpose'; +export { getEquivalentNote } from './notes/getEquivalentNote.js'; +export { getInterval } from './notes/getInterval.js'; +export { getScaleDegreeOfNote } from './notes/getScaleDegreeOfNote.js'; +export { noteToInteger } from './notes/noteToInteger.js'; +export { midiToNote, noteToMidi } from './notes/noteToMidi.js'; +export { transposeNote } from './notes/transpose.js'; + +// Scales +export { getScale } from './scale/getScale.js'; +export { getScaleDegrees } from './scale/getScaleDegrees.js'; +export { guessMajorScale } from './scale/guessMajorScale.js'; +export { scaleToPianoKeys } from './scale/scaleToPianoKeys.js'; +export { scaleToSequentialKeys } from './scale/scaleToSequentialKeys.js'; // Consts -export { majorScales } from './consts'; +export { + scales, + major, + dorian, + phrygian, + lydian, + mixolydian, + minor, + locrian, +} from './db/scales/allScales.js'; diff --git a/src/modes/helpers.ts b/src/modes/helpers.ts index 6b93496..8fc3472 100644 --- a/src/modes/helpers.ts +++ b/src/modes/helpers.ts @@ -1,8 +1,8 @@ import { modes, scaleIntervals } from '../consts.js'; -import type { TMode, TScaleType } from '../types.js'; +import type { Mode, TScaleType } from '../types.js'; -export function isModeName(value: string | undefined): value is TMode { - return modes.includes(value as TMode); +export function isModeName(value: string | undefined): value is Mode { + return modes.includes(value as Mode); } export function isScaleType(value: string): value is TScaleType { diff --git a/src/notes/determineNoteType.ts b/src/notes/determineNoteType.ts index e9a7f9e..667d989 100644 --- a/src/notes/determineNoteType.ts +++ b/src/notes/determineNoteType.ts @@ -1,10 +1,9 @@ -import { isFlat, isSharp } from '../helper.js'; import { NoteType } from '../types.js'; export const determineNoteType = (note: string): NoteType => { - if (isSharp(note)) { + if (/[A-G]#/.test(note)) { return 'sharp'; - } else if (isFlat(note)) { + } else if (/[A-G]b/.test(note)) { return 'flat'; } else { return 'natural'; diff --git a/src/notes/getInterval.ts b/src/notes/getInterval.ts index 4d89538..87ddba5 100644 --- a/src/notes/getInterval.ts +++ b/src/notes/getInterval.ts @@ -1,12 +1,12 @@ import { intervalsBySemitone } from '../consts.js'; -import type { TIntervalShorthand } from '../types.js'; +import type { IntervalShorthand } from '../types.js'; import { resetNotePositions } from './helpers.js'; export const getInterval = ( noteX: string, noteY: string, showAlternatives = false -): TIntervalShorthand | TIntervalShorthand[] => { +): IntervalShorthand | IntervalShorthand[] => { const allNotes = resetNotePositions(noteX); const noteYIndex = allNotes.findIndex((note) => Object.values(note).includes(noteY) diff --git a/src/notes/getScaleDegreeOfNote.ts b/src/notes/getScaleDegreeOfNote.ts index 5dd019b..e154fb0 100644 --- a/src/notes/getScaleDegreeOfNote.ts +++ b/src/notes/getScaleDegreeOfNote.ts @@ -1,12 +1,12 @@ import { romanNumeralCase } from '../chords/helpers.js'; import { majorScaleQualities, modes, romanNumerals } from '../consts.js'; import { offsetArr } from '../helper.js'; -import { TMode } from '../types.js'; +import { Mode } from '../types.js'; export const getScaleDegreeOfNote = ( note: string, scale: string[], - mode: TMode = 'ionian' + mode: Mode = 'ionian' ) => { const reOrdered = offsetArr(majorScaleQualities, modes.indexOf(mode)); return romanNumerals.map((numeral, i) => { diff --git a/src/notes/intervalToSemitone.ts b/src/notes/intervalToSemitone.ts index 1b809e8..fcf3d19 100644 --- a/src/notes/intervalToSemitone.ts +++ b/src/notes/intervalToSemitone.ts @@ -1,8 +1,8 @@ import { intervalsMap } from '../consts.js'; -import type { TIntervalShorthand } from '../types.js'; +import type { IntervalShorthand } from '../types.js'; export const intervalToSemitone = ( - interval: TIntervalShorthand, + interval: IntervalShorthand, normalized = false ) => { const semitones = intervalsMap[interval]; diff --git a/src/notes/noteToMidi.ts b/src/notes/noteToMidi.ts index 3e3cc1e..1757a6d 100644 --- a/src/notes/noteToMidi.ts +++ b/src/notes/noteToMidi.ts @@ -1,4 +1,3 @@ -import { MIDINumber } from '../types.js'; import { isMidiNumber, keyboardNotePositions } from './helpers.js'; export const noteToMidi = (note: string): number | undefined => { diff --git a/src/notes/transpose.test.ts b/src/notes/transpose.test.ts index 7e4fc25..b93fcdf 100644 --- a/src/notes/transpose.test.ts +++ b/src/notes/transpose.test.ts @@ -23,6 +23,10 @@ describe('transposeNote', () => { }); it('uses a scale of notes to constrain the results', () => { - expect(transposeNote('C', 'P1', { keyName: 'C# Major' })).toEqual('B#'); + expect( + transposeNote('C', 'P1', { + scale: ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'], + }) + ).toEqual('B#'); }); }); diff --git a/src/notes/transpose.ts b/src/notes/transpose.ts index 622d8f9..2f03948 100644 --- a/src/notes/transpose.ts +++ b/src/notes/transpose.ts @@ -1,33 +1,8 @@ -import { isFlat, isSharp } from '../helper.js'; -import { findScaleByName, getScale } from '../scale/getScale.js'; -import type { TIntervalShorthand } from '../types.js'; +import type { IntervalShorthand } from '../types.js'; import { intervalToSemitone } from './intervalToSemitone.js'; import { resetNotePositions } from './helpers.js'; import { NotePosition } from '../consts.js'; -interface PickNoteVariantOptions { - forceFlat?: boolean; - forceSharp?: boolean; - forceScale?: string[]; -} - -const pickNoteVariant = ( - notes: NotePosition, - srcNote: string, - options?: PickNoteVariantOptions -) => { - const [hasFlat, hasSharp] = [isFlat(srcNote), isSharp(srcNote)]; - if (options?.forceSharp || hasSharp) { - return notes.sharp; - } - - if (options?.forceFlat || hasFlat) { - return notes.flat; - } - - return notes.natural ? notes.natural : notes.flat; -}; - const pickNoteFromScale = (noteGroup: NotePosition, scale: string[]) => { if (scale === undefined) return 'X'; @@ -45,44 +20,55 @@ const pickNoteFromScale = (noteGroup: NotePosition, scale: string[]) => { interface INoteTransposeOptions { forceFlat?: boolean; forceSharp?: boolean; - forceAliasAccidentals?: boolean; - keyName?: string; + forceSimple?: boolean; + scale?: string[]; } export const transposeNote = ( note: string, - interval: TIntervalShorthand, + interval: IntervalShorthand, options?: INoteTransposeOptions ): string => { const transposedNotes = resetNotePositions(note)[intervalToSemitone(interval, true)]; - const chooseSharp = /a/i.test(interval) || options?.forceSharp; - const chooseFlat = /d/i.test(interval) || options?.forceFlat; - if (Object.values(transposedNotes).length === 1 && transposedNotes.natural) { return transposedNotes.natural; } - if (chooseSharp && transposedNotes.sharp) { + if (options?.forceSimple && transposedNotes.natural) { + return transposedNotes.natural; + } + + // Note: There are individual checks for augmented and diminished intervals because even if the chord + // is spelled with a sharp or flat, you want the transposition to be according to the type of + // interval (augmented, diminished). If you combine the boolean logic it forces notes in a + // Bbaug to be Bb D Gb instead of what it should be which is Bb D F# to reflect the sharpening + // of the 5th in the chord + if (/a/i.test(interval) && transposedNotes.sharp) { return transposedNotes.sharp; } - if (chooseFlat && transposedNotes.flat) { + if (/d/i.test(interval) && transposedNotes.flat) { return transposedNotes.flat; } - if (options?.forceFlat && options.forceSharp) { + if (options?.forceFlat && options?.forceSharp) { console.error( 'You cannot pass both forceFlat and ForceSharp options at the same time' ); } - if (options?.keyName !== undefined) { - return pickNoteFromScale( - transposedNotes, - findScaleByName(options.keyName)?.notes - ); + if (options?.forceSharp && transposedNotes.sharp) { + return transposedNotes.sharp; + } + + if (options?.forceFlat && transposedNotes.flat) { + return transposedNotes.flat; + } + + if (options?.scale !== undefined) { + return pickNoteFromScale(transposedNotes, options.scale); } return transposedNotes.natural diff --git a/src/scale/extractName.ts b/src/scale/extractName.ts index 593d297..f066337 100644 --- a/src/scale/extractName.ts +++ b/src/scale/extractName.ts @@ -1,9 +1,9 @@ import { isScaleType } from '../modes/helpers.js'; -import type { TMode, TScaleType } from '../types.js'; +import type { Mode, TScaleType } from '../types.js'; export const extractScaleName = ( name: string -): [string, TScaleType | TMode] | undefined => { +): [string, TScaleType | Mode] | undefined => { const regex = new RegExp(/([A-G](?:b|#)?) (.*)/); const [pitchClass, scale] = name.match(regex)?.slice(1, 3) || []; const normalScale = scale.toLowerCase().replaceAll(' ', '-'); @@ -11,5 +11,14 @@ export const extractScaleName = ( if ((pitchClass || scale) === undefined) { console.log('Not a scale'); } + + if (normalScale === 'ionian') { + return [pitchClass, 'major']; + } + + if (normalScale === 'aeolian') { + return [pitchClass, 'minor']; + } + if (isScaleType(normalScale)) return [pitchClass, normalScale]; }; diff --git a/src/scale/getMajorFromMode.ts b/src/scale/getMajorFromMode.ts index b7365f5..2581ed0 100644 --- a/src/scale/getMajorFromMode.ts +++ b/src/scale/getMajorFromMode.ts @@ -1,11 +1,12 @@ -import { majorScales, scaleTypes } from '../consts.js'; +import { scaleTypes } from '../consts.js'; +import { major } from '../db/scales/allScales.js'; import type { TScaleType } from '../types.js'; export const getMajorFromMode = (tonic: string, mode: TScaleType) => { const modeIndex = scaleTypes.indexOf(mode); - const scaleIndex = Object.values(majorScales).findIndex((notes) => { + const scaleIndex = Object.values(major).findIndex((notes) => { return notes.notes[modeIndex] === tonic; }); - return Object.keys(majorScales)[scaleIndex]; + return Object.keys(major)[scaleIndex]; }; diff --git a/src/scale/getScale.test.ts b/src/scale/getScale.test.ts index 2b14c30..6503472 100644 --- a/src/scale/getScale.test.ts +++ b/src/scale/getScale.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { getScale } from './getScale.js'; describe('getScale', () => { - it.only('supports major scales', () => { + it('supports major scales', () => { expect(getScale('C major')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); expect(getScale('E major')).toEqual([ 'E', @@ -28,41 +28,9 @@ describe('getScale', () => { ]); }); - it('supports melodic minor scales', () => { - expect(getScale('A melodic minor')).toEqual([ - 'A', - 'B', - 'C', - 'D', - 'E', - 'F#', - 'G#', - ]); - expect(getScale('F melodic minor')).toEqual([ - 'F', - 'G', - 'Ab', - 'Bb', - 'C', - 'D', - 'E', - ]); - }); - it('supports harmonic minor scales', () => { - expect(getScale('B harmonic minor')).toEqual([ - 'B', - 'C#', - 'D', - 'E', - 'F#', - 'G', - 'A#', - ]); - }); - describe('diatonic modes', () => { it('supports ionian mode', () => { - expect(getScale('C ionian')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); + expect(getScale('C major')).toEqual(['C', 'D', 'E', 'F', 'G', 'A', 'B']); }); it('supports dorian mode', () => { expect(getScale('C dorian')).toEqual([ @@ -108,8 +76,8 @@ describe('getScale', () => { 'Bb', ]); }); - it('supports aeolian mode', () => { - expect(getScale('C aeolian')).toEqual([ + it('supports minor mode', () => { + expect(getScale('C minor')).toEqual([ 'C', 'D', 'Eb', diff --git a/src/scale/guessScale.test.ts b/src/scale/guessMajorScale.test.ts similarity index 81% rename from src/scale/guessScale.test.ts rename to src/scale/guessMajorScale.test.ts index 96a64ee..d0a2e3d 100644 --- a/src/scale/guessScale.test.ts +++ b/src/scale/guessMajorScale.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { guessScale } from './guessScale.js'; +import { guessMajorScale } from './guessMajorScale.js'; -describe('guessScale', () => { +describe('guessMajorScale', () => { const testSongs = [ { title: 'Dancing Queen', @@ -51,11 +51,17 @@ describe('guessScale', () => { excludedNotes: [], key: ['Db major', 'C# major'], }, + { + title: 'Antonia', + includedNotes: ['C#', 'D#', 'F', 'F#', 'G#', 'A#', 'B'], + excludedNotes: [], + key: ['Gb major', 'F# major'], + }, ]; it.each(testSongs)('$title', ({ includedNotes, excludedNotes, key }) => { expect( - guessScale(includedNotes, excludedNotes).map(([name]) => name) + guessMajorScale(includedNotes, excludedNotes).map(([name]) => name) ).toStrictEqual(key); }); }); diff --git a/src/scale/guessScale.ts b/src/scale/guessMajorScale.ts similarity index 92% rename from src/scale/guessScale.ts rename to src/scale/guessMajorScale.ts index ab0041e..2e3af33 100644 --- a/src/scale/guessScale.ts +++ b/src/scale/guessMajorScale.ts @@ -1,8 +1,6 @@ import { ScaleGroups, scales } from '../db/scales/allScales.js'; import { noteToInteger } from '../notes/noteToInteger.js'; -type KeyGuess = { name?: string; scale: string[] }; - const getScaleIntegersObj = (name: ScaleGroups) => { return Object.entries(scales[name]).reduce>( (accum, [scaleName, scaleInfo]) => { @@ -13,7 +11,7 @@ const getScaleIntegersObj = (name: ScaleGroups) => { ); }; -export const guessScale = ( +export const guessMajorScale = ( includedNotes: string[], excludedNotes: string[] ) => { diff --git a/src/scale/scaleToMidiKeys.test.ts b/src/scale/scaleToPianoKeys.test.ts similarity index 98% rename from src/scale/scaleToMidiKeys.test.ts rename to src/scale/scaleToPianoKeys.test.ts index b74d5d4..59ae334 100644 --- a/src/scale/scaleToMidiKeys.test.ts +++ b/src/scale/scaleToPianoKeys.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { ScaleKey, scaleToPianoKeys } from './scaleToMidiKeys.js'; +import { ScaleKey, scaleToPianoKeys } from './scaleToPianoKeys.js'; export const majorScales: [string, string[], ScaleKey[]][] = [ [ @@ -199,7 +199,7 @@ export const majorScales: [string, string[], ScaleKey[]][] = [ ], ]; -describe('scaleToMidiKeys', () => { +describe('scaleToPianoKeys', () => { it.each(majorScales)('gets degrees for %s', (_, scale, result) => { expect(scaleToPianoKeys(scale)).toStrictEqual(result); }); diff --git a/src/scale/scaleToMidiKeys.ts b/src/scale/scaleToPianoKeys.ts similarity index 79% rename from src/scale/scaleToMidiKeys.ts rename to src/scale/scaleToPianoKeys.ts index 273374d..12c949e 100644 --- a/src/scale/scaleToMidiKeys.ts +++ b/src/scale/scaleToPianoKeys.ts @@ -1,4 +1,4 @@ -import { scaleToSequentialKeys } from './scaleToMidiNumbers.js'; +import { scaleToSequentialKeys } from './scaleToSequentialKeys.js'; export const scaleToPianoKeys = (scale: string[]) => { const integers = scaleToSequentialKeys(scale); diff --git a/src/scale/scaleToMidiNumbers.test.ts b/src/scale/scaleToSequentialKeys.test.ts similarity index 69% rename from src/scale/scaleToMidiNumbers.test.ts rename to src/scale/scaleToSequentialKeys.test.ts index ceaa7e2..94fa1ad 100644 --- a/src/scale/scaleToMidiNumbers.test.ts +++ b/src/scale/scaleToSequentialKeys.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { scaleToSequentialKeys } from './scaleToMidiNumbers.js'; +import { scaleToSequentialKeys } from './scaleToSequentialKeys.js'; -describe('scaleToIntegers', () => { +describe('scaleToSequentialKeys', () => { it('converts an array of notes to an array of integers', () => { expect( scaleToSequentialKeys(['C', 'D', 'E', 'F', 'G', 'A', 'B']) diff --git a/src/scale/scaleToMidiNumbers.ts b/src/scale/scaleToSequentialKeys.ts similarity index 100% rename from src/scale/scaleToMidiNumbers.ts rename to src/scale/scaleToSequentialKeys.ts diff --git a/src/types.ts b/src/types.ts index b7ced1e..041471f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,13 +12,13 @@ export type Range = Exclude< Enumerate >; -export type TIntervalQuality = +export type IntervalQuality = | 'perfect' | 'major' | 'minor' | 'augmented' | 'diminished'; -export type TChordQuality = +export type ChordQuality = | 'major' | 'minor' | 'augmented' @@ -26,7 +26,7 @@ export type TChordQuality = | 'half-diminished' | 'dominant'; -export type TMode = +export type Mode = | 'ionian' | 'dorian' | 'phrygian' @@ -35,7 +35,7 @@ export type TMode = | 'aeolian' | 'locrian'; -export type TChordType = +export type ChordType = | 'second' | 'triad' | 'fourth' @@ -46,7 +46,7 @@ export type TChordType = | 'eleventh' | 'thirteenth'; -export type TIntervalShorthand = +export type IntervalShorthand = | 'd2' | 'P1' | 'A1' @@ -102,24 +102,25 @@ export type TIntervalShorthand = export type TScaleType = keyof typeof scaleIntervals; -export type TAddType = 'add2' | 'add4' | 'add9' | 'add11' | 'add13'; -export type TSusType = 'sus2' | 'sus4'; +export type AddType = 'add2' | 'add4' | 'add9' | 'add11' | 'add13'; +export type SusType = 'sus2' | 'sus4'; -export interface IChordInfo { +export interface ChordInfo { name: string; note: string; - quality: TChordQuality; - type?: TChordType; + quality: ChordQuality; + type?: ChordType; alteredNotes?: string[]; - addType?: TAddType; - susType?: TSusType; + addType?: AddType; + susType?: SusType; slashNote?: string; pitchClassType: NoteType; } -export interface IChord { +export interface Chord { name: string; notes: string[]; + romanNumeral: string; } export type ScaleQualities = (typeof majorScaleQualities)[number]; From 995353000eeecbf6d0df5965ee78de71d6c119f5 Mon Sep 17 00:00:00 2001 From: Kilian McMahon Date: Thu, 21 Sep 2023 21:34:57 +0200 Subject: [PATCH 3/5] chore: add changeset --- .changeset/silly-tips-shop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/silly-tips-shop.md diff --git a/.changeset/silly-tips-shop.md b/.changeset/silly-tips-shop.md new file mode 100644 index 0000000..e30ef9a --- /dev/null +++ b/.changeset/silly-tips-shop.md @@ -0,0 +1,5 @@ +--- +"@kilmc/music-fns": minor +--- + +Improve system to improve accuracy From 7d751a934ef38a427caceb38fd5ea3056aedea6a Mon Sep 17 00:00:00 2001 From: Kilian McMahon Date: Thu, 21 Sep 2023 21:38:03 +0200 Subject: [PATCH 4/5] chore: fix Chord type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 041471f..b2e6c79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -120,7 +120,7 @@ export interface ChordInfo { export interface Chord { name: string; notes: string[]; - romanNumeral: string; + romanNumeral?: string; } export type ScaleQualities = (typeof majorScaleQualities)[number]; From 4b8e1ee788d013aace9388d6ae6071f92d637046 Mon Sep 17 00:00:00 2001 From: Kilian McMahon Date: Thu, 21 Sep 2023 21:50:22 +0200 Subject: [PATCH 5/5] ci: add build job --- .github/workflows/main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26a0ac0..332f89c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,24 @@ jobs: pnpm install - name: Run Tests run: pnpm run test + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: '18.x' + + - name: Install Dependencies + run: | + npm install -g pnpm + pnpm install + - name: Run Tests + run: pnpm run build release: needs: test name: Release