/**
* 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
*
*/
import * as Vex from 'vexflow';
import { Music21Exception } from './exceptions21.js';
import { interval } from './interval.js';
import { note } from './note.js';
/**
* chord Module. See {@link music21.chord} namespace for more details
*
* @exports music21/chord
*/
/**
* Chord related objects (esp. {@link music21.chord.Chord}) and methods.
*
* @namespace music21.chord
* @memberof music21
* @requires music21/note
*/
export const chord = {};
/**
* 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 {Array<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 = [];
notes.forEach(this.add, this, false);
this.sortPitches();
}
get length() {
return this._notes.length;
}
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 = {};
}
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
*
* @memberof music21.chord.Chord
* @param {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.
*
* @memberof music21.chord.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.Chord(nonDuplicatingPitches);
return closedChord;
}
/**
* Finds the Root of the chord.
*
* @memberof music21.chord.Chord
* @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.
*
* @memberof music21.chord.Chord
* @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.
*
* @memberof music21.chord.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 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.
*
* @memberof music21.chord.Chord
* @returns {number}
*/
cardinality() {
const uniqueChord = this.removeDuplicatePitches();
return uniqueChord.pitches.length;
}
/**
*
* @memberof music21.chord.Chord
* @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;
}
}
/**
*
* @memberof music21.chord.Chord
* @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;
}
}
isDominantSeventh() {
const third = this.third;
const fifth = this.fifth;
const seventh = this.seventh;
if (
third === undefined
|| fifth === undefined
|| seventh === undefined
) {
return false;
}
for (const thisPitch of this.pitches) {
const thisInterval = new interval.Interval(this.root(), thisPitch);
if (![0, 4, 7, 10].includes(thisInterval.chromatic.mod12)) {
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
*
* @memberof music21.chord.Chord
* @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.
*
* @memberof music21.chord.Chord
* @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;
}
/**
* @memberof music21.chord.Chord
* @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.
*
* @memberof music21.chord.Chord
* @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);
}
}
chord.Chord = Chord;
chord.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'],
};