import { CHORD_COMPONENT_ORDERS, ChordAnalysis, ChordBase, ChordComponent, ChordExtensionType, ChordModificationType, Interval, } from './common'; const INTERVAL_COMPONENT_MAPPING = { [Interval.UNISON]: ChordComponent.ROOT, [Interval.MINOR_SECOND]: ChordComponent.SECOND, [Interval.MAJOR_SECOND]: ChordComponent.SECOND, [Interval.MINOR_THIRD]: ChordComponent.THIRD, [Interval.MAJOR_THIRD]: ChordComponent.THIRD, [Interval.PERFECT_FOURTH]: ChordComponent.FOURTH, [Interval.DIMINISHED_FIFTH]: ChordComponent.FIFTH, [Interval.PERFECT_FIFTH]: ChordComponent.FIFTH, [Interval.AUGMENTED_FIFTH]: ChordComponent.FIFTH, [Interval.MAJOR_SIXTH]: ChordComponent.SIXTH, [Interval.MINOR_SEVENTH]: ChordComponent.SEVENTH, [Interval.MAJOR_SEVENTH]: ChordComponent.SEVENTH, [Interval.OCTAVE]: ChordComponent.ROOT, [Interval.MINOR_NINTH]: ChordComponent.NINTH, [Interval.MAJOR_NINTH]: ChordComponent.NINTH, [Interval.AUGMENTED_NINTH]: ChordComponent.NINTH, [Interval.TENTH]: ChordComponent.THIRD, [Interval.MINOR_ELEVENTH]: ChordComponent.ELEVENTH, [Interval.MAJOR_ELEVENTH]: ChordComponent.ELEVENTH, [Interval.TWELFTH]: ChordComponent.FIFTH, [Interval.MINOR_THIRTEENTH]: ChordComponent.THIRTEENTH, [Interval.MAJOR_THIRTEENTH]: ChordComponent.THIRTEENTH, [Interval.AUGMENTED_THIRTEENTH]: ChordComponent.THIRTEENTH, } as const; const normalizeIntervalsFromRoot = (intervalsFromRoot: Interval[]) => intervalsFromRoot.reduce( (normalized, intervalFromRoot) => { const theNormalized = [...normalized]; const { [intervalFromRoot]: component } = INTERVAL_COMPONENT_MAPPING; theNormalized[CHORD_COMPONENT_ORDERS.indexOf(component)] = intervalFromRoot; return theNormalized; }, [] as Interval[], ); const getChordBase = (normalizedIntervals: Interval[]) => { const [, second, third, fourth, fifth] = normalizedIntervals; switch (third) { case Interval.MINOR_THIRD: return { base: fifth === Interval.DIMINISHED_FIFTH ? ChordBase.DIMINISHED : ChordBase.MINOR, }; case Interval.MAJOR_THIRD: case Interval.TENTH: if (fifth === Interval.AUGMENTED_FIFTH) { return { base: ChordBase.AUGMENTED, }; } if (fifth === Interval.DIMINISHED_FIFTH) { return { base: ChordBase.MAJOR, modifications: [ { type: ChordModificationType.LOWERED, component: ChordComponent.FIFTH, }, ], }; } return { base: ChordBase.MAJOR, }; default: break; } if (second === Interval.MAJOR_SECOND) { return { modifications: [ { type: ChordModificationType.SUSPENDED, component: ChordComponent.SECOND, }, ], }; } if (fourth === Interval.PERFECT_FOURTH) { return { modifications: [ { type: ChordModificationType.SUSPENDED, component: ChordComponent.FOURTH, }, ], }; } return {}; }; const getChordSixth = (normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis) => { const { extensions: priorExtensions = [] } = priorAnalysis; const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.FIFTH)]: fifth, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth, } = normalizedIntervals; if (fifth === Interval.PERFECT_FIFTH && sixth === Interval.MAJOR_SIXTH) { return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MAJOR, component: ChordComponent.SIXTH, }], }; } return priorAnalysis; }; const getChordSeventh = ( normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis, ) => { const { extensions: priorExtensions = [], base: oldBase, modifications = [] } = priorAnalysis; const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.THIRD)]: third, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh, } = normalizedIntervals; if (oldBase === ChordBase.DIMINISHED && sixth === Interval.DIMINISHED_SEVENTH) { return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.DIMINISHED, component: ChordComponent.SEVENTH, }], }; } if (seventh === Interval.MINOR_SEVENTH) { let newBase: ChordBase | undefined; if (third === Interval.MINOR_THIRD) { newBase = ChordBase.MINOR; } else if (third === Interval.MAJOR_THIRD || third === Interval.TENTH) { newBase = ChordBase.MAJOR; } else { newBase = oldBase; } if (oldBase === ChordBase.DIMINISHED) { return { ...priorAnalysis, base: newBase, extensions: [...priorExtensions, { type: ChordExtensionType.DOMINANT, component: ChordComponent.SEVENTH, }], modifications: [ ...modifications, { type: ChordModificationType.LOWERED, component: ChordComponent.FIFTH, }, ], }; } return { ...priorAnalysis, base: newBase, extensions: [...priorExtensions, { type: ChordExtensionType.DOMINANT, component: ChordComponent.SEVENTH, }], }; } if (seventh === Interval.MAJOR_SEVENTH) { return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MAJOR, component: ChordComponent.SEVENTH, }], }; } return priorAnalysis; }; const getChordNinth = ( normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis, ) => { const { extensions: priorExtensions = [] } = priorAnalysis; const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth } = normalizedIntervals; switch (ninth) { case Interval.MINOR_NINTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MINOR, component: ChordComponent.NINTH, }], }; case Interval.MAJOR_NINTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MAJOR, component: ChordComponent.NINTH, }], }; case Interval.AUGMENTED_NINTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.AUGMENTED, component: ChordComponent.NINTH, }], }; default: break; } return priorAnalysis; }; const getChordEleventh = ( normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis, ) => { const { extensions: priorExtensions = [] } = priorAnalysis; const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh, } = normalizedIntervals; switch (eleventh) { case Interval.MINOR_ELEVENTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MINOR, component: ChordComponent.ELEVENTH, }], }; case Interval.MAJOR_ELEVENTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MAJOR, component: ChordComponent.ELEVENTH, }], }; default: break; } return priorAnalysis; }; const getChordThirteenth = ( normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis, ) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.THIRTEENTH)]: thirteenth, } = normalizedIntervals; const { extensions: priorExtensions = [] } = priorAnalysis; switch (thirteenth) { case Interval.MINOR_THIRTEENTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MINOR, component: ChordComponent.THIRTEENTH, }], }; case Interval.MAJOR_THIRTEENTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.MAJOR, component: ChordComponent.THIRTEENTH, }], }; case Interval.AUGMENTED_THIRTEENTH: return { ...priorAnalysis, extensions: [...priorExtensions, { type: ChordExtensionType.AUGMENTED, component: ChordComponent.THIRTEENTH, }], }; default: break; } return priorAnalysis; }; const getBaseChordOmissions = (normalizedIntervals: Interval[]) => { const [ root, second, third, fourth, ] = normalizedIntervals; const omissions = []; if (!second && !third && !fourth) { omissions.push(ChordComponent.THIRD); } if (root !== Interval.UNISON) { omissions.push(ChordComponent.ROOT); } return omissions; }; const getSeventhChordOmissions = (normalizedIntervals: Interval[]) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.FIFTH)]: fifth, } = normalizedIntervals; const omissions = getBaseChordOmissions(normalizedIntervals); if (!fifth) { omissions.push(ChordComponent.FIFTH); } return omissions; }; const getNinthChordOmissions = (normalizedIntervals: Interval[]) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh, } = normalizedIntervals; const omissions = getSeventhChordOmissions(normalizedIntervals); if (!seventh && !sixth) { omissions.push(ChordComponent.SEVENTH); } return omissions; }; const getEleventhChordOmissions = (normalizedIntervals: Interval[]) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth, } = normalizedIntervals; const omissions = getNinthChordOmissions(normalizedIntervals); if (!ninth) { omissions.push(ChordComponent.NINTH); } return omissions; }; const getThirteenthChordOmissions = (normalizedIntervals: Interval[]) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh, } = normalizedIntervals; const omissions = getEleventhChordOmissions(normalizedIntervals); if (!eleventh) { omissions.push(ChordComponent.ELEVENTH); } return omissions; }; const getChordOmissions = ( normalizedIntervals: Interval[], priorAnalysis: ChordAnalysis, ) => { const { [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SIXTH)]: sixth, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.SEVENTH)]: seventh, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.NINTH)]: ninth, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.ELEVENTH)]: eleventh, [CHORD_COMPONENT_ORDERS.indexOf(ChordComponent.THIRTEENTH)]: thirteenth, } = normalizedIntervals; if (thirteenth) { return { ...priorAnalysis, omissions: getThirteenthChordOmissions(normalizedIntervals), }; } if (eleventh) { return { ...priorAnalysis, omissions: getEleventhChordOmissions(normalizedIntervals), }; } if (ninth) { return { ...priorAnalysis, omissions: getNinthChordOmissions(normalizedIntervals), }; } if (sixth || seventh) { return { ...priorAnalysis, omissions: getSeventhChordOmissions(normalizedIntervals), }; } return { ...priorAnalysis, omissions: getBaseChordOmissions(normalizedIntervals), }; }; export const analyzeIntervals = (intervalsFromRoot: Interval[]) => { const normalizedIntervals = normalizeIntervalsFromRoot(intervalsFromRoot); let initAnalysis = getChordBase(normalizedIntervals) as ChordAnalysis; initAnalysis = getChordSixth(normalizedIntervals, initAnalysis); initAnalysis = getChordSeventh(normalizedIntervals, initAnalysis); initAnalysis = getChordNinth(normalizedIntervals, initAnalysis); initAnalysis = getChordEleventh(normalizedIntervals, initAnalysis); initAnalysis = getChordThirteenth(normalizedIntervals, initAnalysis); initAnalysis = getChordOmissions(normalizedIntervals, initAnalysis); const retValue = {} as ChordAnalysis; if (typeof initAnalysis.base !== 'undefined') { retValue.base = initAnalysis.base; } if ((initAnalysis.extensions?.length ?? 0) > 0) { retValue.extensions = initAnalysis.extensions; } if ((initAnalysis.modifications?.length ?? 0) > 0) { retValue.modifications = initAnalysis.modifications; } if ((initAnalysis.omissions?.length ?? 0) > 0) { retValue.omissions = initAnalysis.omissions; } return retValue; };