Source: music21/chord.js

/**
 * music21j -- Javascript reimplementation of Core music21p features.
 * music21/chord -- Chord
 *
 * Copyright (c) 2013-16, Michael Scott Cuthbert and cuthbertLab
 * Based on music21 (=music21p), Copyright (c) 2006–16, Michael Scott Cuthbert and cuthbertLab
 *
 * chord Module. See {@link music21.chord} namespace for more details.
 * Chord related objects (esp. {@link music21.chord.Chord}) and methods.
 *
 * @exports music21/chord
 * @namespace music21.chord
 * @memberof music21
 * @requires vexflow
 * @requires music21/interval
 * @requires music21/note
 */
import Vex from 'vexflow';

import { Music21Exception } from './exceptions21.js';
import * as interval from './interval.js';
import * as note from './note.js';
import * as chordTables from './chordTables.js';

export { chordTables };

/**
 * Chord related objects (esp. {@link music21.chord.Chord}) and methods.
 *
 * @class Chord
 * @memberof music21.chord
 * @param {Array<string|music21.note.Note|music21.pitch.Pitch>} [notes] - an Array of strings
 * (see {@link music21.pitch.Pitch} for valid formats), note.Note, or pitch.Pitch objects.
 * @extends music21.note.NotRest
 * @property {number} length - the number of pitches in the Chord (readonly)
 * @property {music21.pitch.Pitch[]} pitches - an Array of Pitch objects in the chord. (Consider the Array read only and pass in a new Array to change)
 * @property {Boolean} [isChord=true]
 * @property {Boolean} [isNote=false]
 * @property {Boolean} [isRest=false]
 */
export class Chord extends note.NotRest {
    constructor(notes) {
        super();
        if (typeof notes === 'undefined') {
            notes = [];
        } else if (typeof notes === 'string') {
            notes = notes.split(/\s+/);
        }
        this.isChord = true; // for speed
        this.isNote = false; // for speed
        this.isRest = false; // for speed

        this._overrides = {};
        this._cache = {};

        this._notes = [];
        /**
         *
         * @type {Object|undefined}
         * @private
         */
        this._chordTablesAddress = undefined;
        this._chordTablesAddressNeedsUpdating = true; // only update when needed

        notes.forEach(x => this.add(x, false), this);
        if (notes.length > 0
            && notes[0].duration !== undefined
            && notes[0].duration.quarterLength !== this.duration.quarterLength
        ) {
            this.duration = notes[0].duration;
        }
        this.sortPitches();
    }

    /**
     *
     * @returns {string}
     */
    stringInfo() {
        const info = this.pitches.map(x => x.nameWithOctave);
        return info.join(' ');
    }

    /**
     *
     * @returns {number}
     */
    get length() {
        return this._notes.length;
    }

    /**
     * @type {music21.pitch.Pitch[]}
     */
    get pitches() {
        const tempPitches = [];
        for (let i = 0; i < this._notes.length; i++) {
            tempPitches.push(this._notes[i].pitch);
        }
        return tempPitches;
    }

    set pitches(tempPitches) {
        this._notes = [];
        for (let i = 0; i < tempPitches.length; i++) {
            let addNote;
            if (typeof tempPitches[i] === 'string') {
                addNote = new note.Note(tempPitches[i]);
            } else if (tempPitches[i].isClassOrSubclass('Pitch')) {
                addNote = new note.Note();
                addNote.pitch = tempPitches[i];
            } else if (tempPitches[i].isClassOrSubclass('Note')) {
                addNote = tempPitches[i];
            } else {
                console.warn('bad pitch', tempPitches[i]);
                throw new Music21Exception(
                    'Cannot add pitch from ' + tempPitches[i]
                );
            }
            this._notes.push(addNote);
        }
        this._cache = {};
        this._overrides = {};
    }


    get orderedPitchClasses() {
        const pcGroup = [];
        for (const p of this.pitches) {
            if (pcGroup.includes(p.pitchClass)) {
                continue;
            }
            pcGroup.push(p.pitchClass);
        }
        pcGroup.sort((a, b) => a - b);
        return pcGroup;
    }

    get chordTablesAddress() {
        if (this._chordTablesAddressNeedsUpdating) {
            this._chordTablesAddress = chordTables.seekChordTablesAddress(this);
        }
        this._chordTablesAddressNeedsUpdating = false;
        return this._chordTablesAddress;
    }

    get commonName() {
        // TODO: many more exemptions from music21p
        const cta = this.chordTablesAddress;
        const ctn = chordTables.addressToCommonNames(cta);
        const forteClass = this.forteClass;
        const enharmonicTests = {
            '3-11A': () => this.isMinorTriad(),
            '3-11B': () => this.isMajorTriad(),
            '3-10': () => this.isDiminishedTriad(),
            '3-12': () => this.isAugmentedTriad(),
        };
        if (enharmonicTests[forteClass] !== undefined) {
            let out = ctn[0];
            const test = enharmonicTests[forteClass];
            if (!test()) {
                out = 'enharmonic equivalent to ' + out;
            }
            return out;
        }

        if (ctn === undefined) {
            return '';
        } else {
            return ctn[0];
        }
    }

    get forteClass() {
        return chordTables.addressToForteName(this.chordTablesAddress, 'tn');
    }

    get forteClassNumber() {
        return this.chordTablesAddress.forteClass;
    }

    get forteClassTnI() {
        return chordTables.addressToForteName(this.chordTablesAddress, 'tni');
    }

    get(i) {
        if (typeof i === 'number') {
            return this._notes[i];
        } else {
            return undefined; // TODO(msc): add other get methods.
        }
    }

    * [Symbol.iterator]() {
        for (let i = 0; i < this.length; i++) {
            yield this.get(i);
        }
    }


    areZRelations(other) {
        const zRelationAddress = chordTables.addressToZAddress(this.chordTablesAddress);
        if (zRelationAddress === undefined) {
            return false;
        }
        for (const key of ['cardinality', 'forteClass', 'inversion']) {
            if (other.chordTablesAddress[key] !== zRelationAddress[key]) {
                return false;
            }
        }
        return true;
    }

    getZRelation() {
        if (!this.hasZRelation) {
            return undefined;
        }
        const chordTablesAddress = this.chordTablesAddress;
        const v = chordTables.addressToIntervalVector(chordTablesAddress);
        const addresses = chordTables.intervalVectorToAddress(v);
        let other;
        for (const thisAddress of addresses) {
            if (thisAddress.forteClass !== chordTablesAddress.forteClass) {
                other = thisAddress;
            }
        }
        // other should always be defined;
        const prime = chordTables.addressToTransposedNormalForm(other);
        return new Chord(prime);
    }

    get hasZRelation() {
        const post = chordTables.addressToZAddress(this.chordTablesAddress);
        if (post !== undefined) {
            return true;
        } else {
            return false;
        }
    }

    get intervalVector() {
        return chordTables.addressToIntervalVector(this.chordTablesAddress);
    }

    //    get intervalVectorString() {
    //
    //    }
    //
    //    static formatVectorString() {
    //        // needs pitch._convertPitchClassToStr
    //    }

    setStemDirectionFromClef(clef) {
        if (clef === undefined) {
            return this;
        } else {
            const midLine = clef.lowestLine + 4;
            // console.log(midLine, 'midLine');
            let maxDNNfromCenter = 0;
            const pA = this.pitches;
            for (let i = 0; i < this.pitches.length; i++) {
                const p = pA[i];
                const DNNfromCenter = p.diatonicNoteNum - midLine;
                // >= not > so that the highest pitch wins the tie and thus stem down.
                if (Math.abs(DNNfromCenter) >= Math.abs(maxDNNfromCenter)) {
                    maxDNNfromCenter = DNNfromCenter;
                }
            }
            if (maxDNNfromCenter >= 0) {
                this.stemDirection = 'down';
            } else {
                this.stemDirection = 'up';
            }
            return this;
        }
    }

    /**
     * Adds a note to the chord, sorting the note array
     *
     * @param {string|string[]|music21.note.Note[]|music21.pitch.Pitch[]} notes - the Note or Pitch to be added or a string defining a pitch.
     * @param {boolean} runSort - Sort after running (default true)
     * @returns {music21.chord.Chord} the original chord.
     */
    add(notes, runSort=true) {
        if (!(notes instanceof Array)) {
            notes = [notes];
        }
        for (let noteObj of notes) {
            // takes in either a note or a pitch
            if (typeof noteObj === 'string') {
                noteObj = new note.Note(noteObj);
            } else if (noteObj.isClassOrSubclass('Pitch')) {
                const pitchObj = noteObj;
                const noteObj2 = new note.Note();
                noteObj2.pitch = pitchObj;
                noteObj = noteObj2;
            }
            this._notes.push(noteObj);
        }
        // inefficient because sorts after each add, but safe and #(p) is small
        if (runSort === true) {
            this.sortPitches();
        }
        this._cache = {};
        return this;
    }

    sortPitches() {
        this._notes.sort((a, b) => a.pitch.ps - b.pitch.ps);
    }

    // TODO: add remove

    /**
     * Removes any pitches that appear more than once (in any octave), removing the higher ones, and returns a new Chord.
     *
     * @returns {music21.chord.Chord} A new Chord object with duplicate pitches removed.
     */
    removeDuplicatePitches() {
        const stepsFound = [];
        const nonDuplicatingPitches = [];
        const pitches = this.pitches;
        for (let i = 0; i < pitches.length; i++) {
            const p = pitches[i];
            if (stepsFound.indexOf(p.step) === -1) {
                stepsFound.push(p.step);
                nonDuplicatingPitches.push(p);
            }
        }
        const closedChord = new Chord(nonDuplicatingPitches);
        return closedChord;
    }

    /**
     * Finds the Root of the chord.
     *
     * @param {music21.pitch.Pitch} [newroot]
     * @returns {music21.pitch.Pitch} the root of the chord.
     */
    root(newroot) {
        if (newroot !== undefined) {
            this._overrides.root = newroot;
            this._cache.root = newroot;
            this._cache.inversion = undefined;
        }

        if (this._overrides.root !== undefined) {
            return this._cache.root;
        }

        if (this._cache.root !== undefined) {
            return this._cache.root;
        }

        const closedChord = this.removeDuplicatePitches();
        /* var chordBass = closedChord.bass(); */
        const closedPitches = closedChord.pitches;
        if (closedPitches.length === 0) {
            throw new Music21Exception('No notes in Chord!');
        } else if (closedPitches.length === 1) {
            return this.pitches[0];
        }
        // const indexOfPitchesWithPerfectlyStackedThirds = [];
        const testSteps = [3, 5, 7, 2, 4, 6];
        for (let i = 0; i < closedPitches.length; i++) {
            const p = closedPitches[i];
            const currentListOfThirds = [];
            for (let tsIndex = 0; tsIndex < testSteps.length; tsIndex++) {
                const chordStepPitch = closedChord.getChordStep(
                    testSteps[tsIndex],
                    p
                );
                if (chordStepPitch !== undefined) {
                    // console.log(p.name + " " + testSteps[tsIndex].toString() + " " + chordStepPitch.name);
                    currentListOfThirds.push(true);
                } else {
                    currentListOfThirds.push(false);
                }
            }
            // console.log(currentListOfThirds);
            let hasFalse = false;
            for (let j = 0; j < closedPitches.length - 1; j++) {
                if (currentListOfThirds[j] === false) {
                    hasFalse = true;
                }
            }
            if (hasFalse === false) {
                // indexOfPitchesWithPerfectlyStackedThirds.push(i);
                return closedChord.pitches[i]; // should do more, but fine...
                // should test rootedness function, etc. 13ths. etc.
            }
        }
        const newRoot = closedChord.pitches[0]; // fallback, just return the bass...
        this._cache.root = newRoot;
        return newRoot;
    }

    /**
     * Returns the number of semitones above the root that a given chordstep is.
     *
     * For instance, in a G dominant 7th chord (G, B, D, F), would
     * return 4 for chordStep=3, since the third of the chord (B) is four semitones above G.
     *
     * @param {number} chordStep - the step to find, e.g., 1, 2, 3, etc.
     * @param {music21.pitch.Pitch} [testRoot] - the pitch to temporarily consider the root.
     * @returns {number|undefined} Number of semitones above the root for this chord step or undefined if no pitch matches that chord step.
     */
    semitonesFromChordStep(chordStep, testRoot) {
        if (testRoot === undefined) {
            testRoot = this.root();
        }
        const tempChordStep = this.getChordStep(chordStep, testRoot);
        if (tempChordStep === undefined) {
            return undefined;
        } else {
            let semitones = (tempChordStep.ps - testRoot.ps) % 12;
            if (semitones < 0) {
                semitones += 12;
            }
            return semitones;
        }
    }

    /**
     * Gets the lowest note (based on .ps not name) in the chord.
     *
     * @returns {music21.pitch.Pitch} bass pitch
     */
    bass() {
        let lowest;
        const pitches = this.pitches;
        for (let i = 0; i < pitches.length; i++) {
            const p = pitches[i];
            if (lowest === undefined) {
                lowest = p;
            } else { // noinspection JSUnusedAssignment
                if (p.ps < lowest.ps) {
                    lowest = p;
                }
            }
        }
        return lowest;
    }

    /**
     * Counts the number of non-duplicate pitch MIDI Numbers in the chord.
     *
     * Call after "closedPosition()" to get Forte style cardinality disregarding octave.
     *
     * @returns {number}
     */
    cardinality() {
        const uniqueChord = this.removeDuplicatePitches();
        return uniqueChord.pitches.length;
    }

    /**
    *
    * @returns {Boolean}
    */
    isMajorTriad() {
        if (this.cardinality() !== 3) {
            return false;
        }
        const thirdST = this.semitonesFromChordStep(3);
        const fifthST = this.semitonesFromChordStep(5);
        if (thirdST === 4 && fifthST === 7) {
            return true;
        } else {
            return false;
        }
    }

    /**
    *
    * @returns {Boolean}
    */
    isMinorTriad() {
        if (this.cardinality() !== 3) {
            return false;
        }
        const thirdST = this.semitonesFromChordStep(3);
        const fifthST = this.semitonesFromChordStep(5);
        if (thirdST === 3 && fifthST === 7) {
            return true;
        } else {
            return false;
        }
    }

    /**
    *
    * @returns {Boolean}
    */
    isDiminishedTriad() {
        if (this.cardinality() !== 3) {
            return false;
        }
        const thirdST = this.semitonesFromChordStep(3);
        const fifthST = this.semitonesFromChordStep(5);
        if (thirdST === 3 && fifthST === 6) {
            return true;
        } else {
            return false;
        }
    }

    /**
    *
    * @returns {Boolean}
    */
    isAugmentedTriad() {
        if (this.cardinality() !== 3) {
            return false;
        }
        const thirdST = this.semitonesFromChordStep(3);
        const fifthST = this.semitonesFromChordStep(5);
        if (thirdST === 4 && fifthST === 8) {
            return true;
        } else {
            return false;
        }
    }


    isDominantSeventh() {
        return this.isSeventhOfType([0, 4, 7, 10]);
    }

    isDiminishedSeventh() {
        return this.isSeventhOfType([0, 3, 6, 9]);
    }

    isSeventhOfType(intervalArray) {
        if (intervalArray === undefined) {
            throw new Music21Exception('intervalArray is required');
        }
        const third = this.third;
        const fifth = this.fifth;
        const seventh = this.seventh;

        if (
            third === undefined
            || fifth === undefined
            || seventh === undefined
        ) {
            return false;
        }

        const root = this.root();

        for (const thisPitch of this.pitches) {
            const thisInterval = new interval.Interval(root, thisPitch);
            if (!intervalArray.includes(thisInterval.chromatic.mod12)) {
                return false;
            }
            //            // check if it doesn't have any other pitches, such as C E F- G Bb != Dominant Seventh
            //            if (!ignoreSpelling && !chordalNames.includes(thisPitch.name)) {
            //                return false;
            //            }
        }
        return true;


    }


    /**
     * canBeDominantV - Returns true if the chord is a Major Triad or a Dominant Seventh
     *
     * @return {Boolean}
     */
    canBeDominantV() {
        if (this.isMajorTriad() || this.isDominantSeventh()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns true if the chord is a major or minor triad
     *
     * @returns {Boolean}
     */
    canBeTonic() {
        if (this.isMajorTriad() || this.isMinorTriad()) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns the inversion of the chord as a number (root-position = 0)
     *
     * Unlike music21 version, cannot set the inversion, yet.
     *
     * TODO: add.
     *
     * @returns {number}
     */
    inversion() {
        const bass = this.bass();
        const root = this.root();
        const chordStepsToInversions = [1, 6, 4, 2, 7, 5, 3];
        for (let i = 0; i < chordStepsToInversions.length; i++) {
            const testNote = this.getChordStep(chordStepsToInversions[i], bass);
            if (testNote !== undefined && testNote.name === root.name) {
                return i;
            }
        }
        return undefined;
    }

    /**
     * @param {Object} options - a dictionary of options `{clef: {@music21.clef.Clef} }` is especially important
     * @returns {Vex.Flow.StaveNote}
     */
    vexflowNote(options) {
        const clef = options.clef;

        const pitchKeys = [];
        for (let i = 0; i < this._notes.length; i++) {
            pitchKeys.push(this._notes[i].pitch.vexflowName(clef));
        }
        const vfn = new Vex.Flow.StaveNote({
            keys: pitchKeys,
            duration: this.duration.vexflowDuration,
        });
        this.vexflowAccidentalsAndDisplay(vfn, options); // clean up stuff...
        for (let i = 0; i < this._notes.length; i++) {
            const tn = this._notes[i];
            if (tn.pitch.accidental !== undefined) {
                if (
                    tn.pitch.accidental.vexflowModifier !== 'n'
                    && tn.pitch.accidental.displayStatus !== false
                ) {
                    vfn.addAccidental(
                        i,
                        new Vex.Flow.Accidental(
                            tn.pitch.accidental.vexflowModifier
                        )
                    );
                } else if (
                    tn.pitch.accidental.displayType === 'always'
                    || tn.pitch.accidental.displayStatus === true
                ) {
                    vfn.addAccidental(
                        i,
                        new Vex.Flow.Accidental(
                            tn.pitch.accidental.vexflowModifier
                        )
                    );
                }
            }
        }
        this.activeVexflowNote = vfn;
        return vfn;
    }

    /**
     * Returns the Pitch object that is a Generic interval (2, 3, 4, etc., but not 9, 10, etc.) above
     * the `.root()`
     *
     * In case there is more that one note with that designation (e.g., `[A-C-C#-E].getChordStep(3)`)
     * the first one in `.pitches` is returned.
     *
     * @param {int} chordStep a positive integer representing the chord step
     * @param {music21.pitch.Pitch} [testRoot] - the Pitch to use as a temporary root
     * @returns {music21.pitch.Pitch|undefined}
     */
    getChordStep(chordStep, testRoot) {
        if (testRoot === undefined) {
            testRoot = this.root();
        }
        if (chordStep >= 8) {
            chordStep %= 7;
        }
        const thisPitches = this.pitches;
        const testRootDNN = testRoot.diatonicNoteNum;
        for (let i = 0; i < thisPitches.length; i++) {
            const thisPitch = thisPitches[i];
            let thisInterval
                = (thisPitch.diatonicNoteNum - testRootDNN + 1) % 7; // fast cludge
            if (thisInterval <= 0) {
                thisInterval += 7;
            }
            if (thisInterval === chordStep) {
                return thisPitch;
            }
        }
        return undefined;
    }

    get third() {
        return this.getChordStep(3);
    }

    get fifth() {
        return this.getChordStep(5);
    }

    get seventh() {
        return this.getChordStep(7);
    }
}

export const chordDefinitions = {
    major: ['M3', 'm3'],
    minor: ['m3', 'M3'],
    diminished: ['m3', 'm3'],
    augmented: ['M3', 'M3'],
    'major-seventh': ['M3', 'm3', 'M3'],
    'dominant-seventh': ['M3', 'm3', 'm3'],
    'minor-seventh': ['m3', 'M3', 'm3'],
    'diminished-seventh': ['m3', 'm3', 'm3'],
    'half-diminished-seventh': ['m3', 'm3', 'M3'],
};
Music21j, Copyright © 2013-2021 Michael Scott Asato Cuthbert.
Documentation generated by JSDoc 3.6.3 on Wed Jul 31st 2019 using the DocStrap template.