/** * music21j -- Javascript reimplementation of Core music21p features. * music21/roman -- roman.RomanNumeral -- Chord subclass * * Copyright (c) 2013-19, Michael Scott Cuthbert and cuthbertLab * Based on music21 (=music21p), Copyright (c) 2006–19, Michael Scott Cuthbert and cuthbertLab * * Roman numeral module. See {@link music21.roman} namespace * music21.roman -- namespace for dealing with RomanNumeral analysis. * * @exports music21/roman * @namespace music21.roman * @memberof music21 * @requires music21/chord * @requires music21/common * @requires music21/figuredBass * @requires music21/harmony * @requires music21/key * @requires music21/pitch * @requires music21/interval */ import { Music21Exception } from './exceptions21.js'; import * as chord from './chord.js'; import * as common from './common.js'; import * as figuredBass from './figuredBass.js'; import * as harmony from './harmony.js'; import * as interval from './interval.js'; import * as key from './key.js'; import * as pitch from './pitch.js'; import * as scale from './scale.js'; export const figureShorthands = { '53': '', '3': '', '63': '6', '753': '7', '75': '7', // controversial perhaps '73': '7', // controversial perhaps '9753': '9', '975': '9', // controversial perhaps '953': '9', // controversial perhaps '97': '9', // controversial perhaps '95': '9', // controversial perhaps '93': '9', // controversial perhaps '653': '65', '6b53': '6b5', '643': '43', '642': '42', bb7b5b3: 'o7', bb7b53: 'o7', // '6b5bb3': 'o65', b7b5b3: '/o7', }; // noinspection SpellCheckingInspection export const functionalityScores = { I: 100, i: 90, V7: 80, V: 70, V65: 68, I6: 65, V6: 63, V43: 61, I64: 60, IV: 59, i6: 58, viio7: 57, V42: 55, viio65: 53, viio6: 52, '#viio65': 51, ii: 50, '#viio6': 49, ii65: 48, ii43: 47, ii42: 46, IV6: 45, ii6: 43, VI: 42, '#VI': 41, vi: 40, viio: 39, '#viio': 39, iio: 37, // common in Minor iio42: 36, bII6: 35, // Neapolitan iio43: 32, iio65: 31, '#vio': 28, '#vio6': 28, III: 22, v: 20, VII: 19, VII7: 18, IV65: 17, IV7: 16, iii: 15, iii6: 12, vi6: 10, }; /** * expandShortHand - expand a string of numbers into an array * * N.B. this is NOT where abbreviations get expanded * * @memberof music21.roman * @param {string} shorthand string of a figure w/o roman to parse * @return {Array<string>} array of shorthands */ export function expandShortHand(shorthand) { shorthand = shorthand.replace('/', ''); if (shorthand.match(/[b-]$/)) { shorthand += '3'; } shorthand = shorthand.replace('11', 'x'); shorthand = shorthand.replace('13', 'y'); shorthand = shorthand.replace('15', 'z'); const rx = new RegExp('#*-*b*o*[1-9xyz]', 'g'); let shorthandGroups = []; let match = rx.exec(shorthand); while (match !== null) { shorthandGroups.push(match[0]); match = rx.exec(shorthand); } if (shorthandGroups.length === 1 && shorthandGroups[0].endsWith('3')) { shorthandGroups = ['5', shorthandGroups[0]]; } const shGroupOut = []; for (let sh of shorthandGroups) { sh = sh.replace('x', '11'); sh = sh.replace('y', '13'); sh = sh.replace('z', '15'); shGroupOut.push(sh); } return shGroupOut; } /** * correctSuffixForChordQuality - Correct a given inversionString suffix given a * chord of various qualities. * * @memberof music21.roman * @param {music21.chord.Chord} chordObj * @param {string} inversionString a string like '6' to fix. * @return {string} corrected inversionString */ export function correctSuffixForChordQuality( chordObj, inversionString ) { const fifthType = chordObj.semitonesFromChordStep(5); let qualityName = ''; if (fifthType === 6) { qualityName = 'o'; } else if (fifthType === 8) { qualityName = '+'; } if ( inversionString !== undefined && (inversionString.startsWith('o') || inversionString.startsWith('/o')) ) { if (qualityName === 'o') { // don't call viio7, viioo7. qualityName = ''; } } const seventhType = chordObj.semitonesFromChordStep(7); if (seventhType !== undefined && fifthType === 6) { // there is a seventh and this is a diminished 5 if (seventhType === 10 && qualityName === 'o') { qualityName = '/o'; } else if (seventhType !== 9) { // do something for very odd chords built on diminished triad. } } return qualityName + inversionString; } /** * maps an index number to a roman numeral in lowercase * * @memberof music21.roman * @example * music21.roman.romanToNumber[4] * // 'iv' */ export const romanToNumber = [undefined, 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii']; /** * Represents a RomanNumeral. By default, capital Roman Numerals are * major chords; lowercase are minor. * * @class RomanNumeral * @memberof music21.roman * @extends music21.harmony.Harmony * @param {string} figure - the roman numeral as a string, e.g., 'IV', 'viio', 'V7' * @param {string|music21.key.Key} [keyStr='C'] * @property {Array<music21.pitch.Pitch>} scale - (readonly) returns the scale associated with the roman numeral * @property {music21.key.Key} key - the key associated with the RomanNumeral (not allowed to be undefined yet) * @property {string} figure - the figure as passed in * @property {string} degreeName - the name associated with the scale degree, such as "mediant" etc., scale 7 will be "leading tone" or "subtonic" appropriately * @property {int} scaleDegree * @property {string|undefined} impliedQuality - "major", "minor", "diminished", "augmented" * @property {music21.roman.RomanNumeral|undefined} secondaryRomanNumeral * @property {music21.key.Key|undefined} secondaryRomanNumeralKey * @property {string|undefined} frontAlterationString * @property {music21.interval.Interval|undefined} frontAlterationTransposeInterval * @property {music21.pitch.Accidental|undefined} frontAlterationAccidental * @property {string|undefined} romanNumeralAlone * @property {scale.Scale|boolean|undefined} impliedScale * @property {music21.interval.Interval|undefined} scaleOffset * @property {Array<music21.pitch.Pitch>} pitches - RomanNumerals are Chord objects, so .pitches will work for them also. */ export class RomanNumeral extends harmony.Harmony { constructor(figure='', keyStr, keywords) { const params = { updatePitches: false, parseFigure: false }; common.merge(params, keywords); super(figure, params); this._parsingComplete = false; // not yet used... this.primaryFigure = undefined; this.secondaryRomanNumeral = undefined; this.secondaryRomanNumeralKey = undefined; this.pivotChord = undefined; this.scaleCardinality = 7; this._figure = undefined; this.caseMatters = true; if (typeof figure === 'number') { this.caseMatters = false; } this.scaleDegree = undefined; this.frontAlterationString = undefined; this.frontAlterationTransposeInterval = undefined; this.frontAlterationAccidental = undefined; this.romanNumeralAlone = undefined; // TODO(msc) -- this is never defined. this.quality = undefined; this.impliedQuality = undefined; this.impliedScale = undefined; this.scaleOffset = undefined; this.useImpliedScale = false; this.bracketedAlterations = []; this.omittedSteps = []; this.followsKeyChange = false; this._functionalityScore = undefined; /** * * @type {music21.key.Key|music21.scale.Scale|undefined} * @private */ this._scale = undefined; // the Key or Scale this.figure = figure; this.key = keyStr; // to remove... this.numbers = ''; if (figure !== '') { this._parseFigure(); this._parsingComplete = true; this._updatePitches(); } } stringInfo() { return this.figure + ' in ' + this.key.stringInfo(); } _parseFigure() { let workingFigure; let useScale = this.impliedScale; if (!this.useImpliedScale) { useScale = this.key; } [workingFigure, useScale] = this._correctForSecondaryRomanNumeral( useScale ); if (workingFigure === 'Cad64') { if (useScale.mode === 'minor') { workingFigure = 'i64'; } else { workingFigure = 'I64'; } } this.primaryFigure = workingFigure; workingFigure = this._parseOmittedSteps(workingFigure); workingFigure = this._parseBracketedAlterations(workingFigure); workingFigure = workingFigure.replace(/^N6/, 'bII6'); workingFigure = workingFigure.replace(/^N/, 'bII6'); workingFigure = this._parseFrontAlterations(workingFigure); [workingFigure, useScale] = this._parseRNAloneAmidstAug6( workingFigure, useScale ); workingFigure = this._setImpliedQualityFromString(workingFigure); this._tempRoot = useScale.pitchFromDegree(this.scaleDegree); this._fixMinorVIandVII(useScale); const expandedFigure = expandShortHand(workingFigure); this.figuresNotationObj = new figuredBass.Notation( expandedFigure.toString() ); const numbersArr = workingFigure.match(/\d+/); if (numbersArr != null) { // noinspection JSUnusedAssignment workingFigure = workingFigure.replace(/\d+/, ''); this.numbers = parseInt(numbersArr[0]); } } _parseFrontAlterations(workingFigure) { let frontAlterationString = ''; let frontAlterationTransposeInterval; let frontAlterationAccidental; const _alterationRegex = new RegExp('^(b+|-+|#+)'); const match = _alterationRegex.exec(workingFigure); if (match != null) { const group = match[1]; let alteration = group.length; if (group[0] === 'b' || group[0] === '-') { alteration *= -1; } frontAlterationTransposeInterval = interval.intervalFromGenericAndChromatic( 1, alteration ); frontAlterationAccidental = new pitch.Accidental(alteration); frontAlterationString = group; workingFigure = workingFigure.replace(_alterationRegex, ''); } this.frontAlterationString = frontAlterationString; this.frontAlterationTransposeInterval = frontAlterationTransposeInterval; this.frontAlterationAccidental = frontAlterationAccidental; return workingFigure; } _correctBracketedPitches() { for (const innerAlteration of this.bracketedAlterations) { const [alterNotation, chordStep] = innerAlteration; const alterPitch = this.getChordStep(chordStep); if (alterPitch === undefined) { continue; } const newAccidental = new pitch.Accidental(alterNotation); if (alterPitch.accidental === undefined) { alterPitch.accidental = newAccidental; } else { alterPitch.accidental.set( alterPitch.accidental.alter + newAccidental.alter ); } } } _setImpliedQualityFromString(workingFigure) { let impliedQuality = ''; if (workingFigure.startsWith('o')) { impliedQuality = 'diminished'; workingFigure = workingFigure.replace(/^o/, ''); } else if (workingFigure.startsWith('/o')) { impliedQuality = 'half-diminished'; workingFigure = workingFigure.replace(/^\/o/, ''); } else if (workingFigure.startsWith('+')) { impliedQuality = 'augmented'; workingFigure = workingFigure.replace(/^\+/, ''); } else if (workingFigure.endsWith('d7')) { impliedQuality = 'dominant-seventh'; workingFigure = workingFigure.replace(/d7$/, '7'); } else if ( this.caseMatters && this.romanNumeralAlone.toUpperCase() === this.romanNumeralAlone ) { impliedQuality = 'major'; } else if ( this.caseMatters && this.romanNumeralAlone.toLowerCase() === this.romanNumeralAlone ) { impliedQuality = 'minor'; } this.impliedQuality = impliedQuality; return workingFigure; } _fixMinorVIandVII(useScale) { if (useScale.mode !== 'minor') { return; } if (!this.caseMatters) { return; } if (this.scaleDegree !== 6 && this.scaleDegree !== 7) { return; } if ( !['minor', 'diminished', 'half-diminished'].includes( this.impliedQuality ) ) { return; } const fati = this.frontAlterationTransposeInterval; if (fati !== undefined) { const newFati = interval.add([fati, new interval.Interval('A1')]); this.frontAlterationTransposeInterval = newFati; this.frontAlterationAccidental.alter = this.frontAlterationAccidental.alter + 1; } else { this.frontAlterationTransposeInterval = new interval.Interval('A1'); this.frontAlterationAccidental = new pitch.Accidental(1); } this._tempRoot = this.frontAlterationTransposeInterval.transposePitch( this._tempRoot ); } _parseRNAloneAmidstAug6(workingFigure, useScale) { let romanNumeralAlone = ''; const _romanNumeralAloneRegex = new RegExp( '(IV|I{1,3}|VI{0,2}|iv|i{1,3}|vi{0,2}|N)' ); const _augmentedSixthRegex = new RegExp('(It|Ger|Fr|Sw)'); const rm = _romanNumeralAloneRegex.exec(workingFigure); const a6match = _augmentedSixthRegex.exec(workingFigure); if (rm === null && a6match === null) { throw new Music21Exception( `No roman numeral found in ${workingFigure}` ); } if (a6match !== null) { if (useScale.mode === 'major') { useScale = new key.Key(useScale.tonic.name, 'minor'); this.impliedScale = useScale; this.useImpliedScale = true; } romanNumeralAlone = a6match[1]; if (['It', 'Ger'].includes(romanNumeralAlone)) { this.scaleDegree = 4; } else { this.scaleDegree = 2; } workingFigure = workingFigure.replace(_augmentedSixthRegex, ''); this.romanNumeralAlone = romanNumeralAlone; if (romanNumeralAlone !== 'Fr') { this.bracketedAlterations.push(['#', 1]); } if (romanNumeralAlone === 'Fr' || romanNumeralAlone === 'Sw') { this.bracketedAlterations.push(['#', 3]); } } else { romanNumeralAlone = rm[1]; this.scaleDegree = common.fromRoman(romanNumeralAlone); workingFigure = workingFigure.replace(_romanNumeralAloneRegex, ''); this.romanNumeralAlone = romanNumeralAlone; } return [workingFigure, useScale]; } /** * get romanNumeral - return either romanNumeralAlone (II) or with frontAlterationAccidental (#II) * * @return {string} new romanNumeral; */ get romanNumeral() { if (this.frontAlterationAccidental === undefined) { return this.romanNumeralAlone; } else { return ( this.frontAlterationAccidental.modifier + this.romanNumeralAlone ); } } get scale() { if (this._scale !== undefined) { return this._scale; } else { this._scale = this.key.getScale(); return this._scale; } } get key() { return this._scale; } set key(keyOrScale) { if (typeof keyOrScale === 'string') { this._scale = new key.Key(keyOrScale); } else if (typeof keyOrScale === 'undefined') { this._scale = new key.Key('C'); } else { this._scale = keyOrScale; } if (keyOrScale === undefined) { this.useImpliedScale = true; this.impliedScale = new scale.MajorScale('C'); } else { this.useImpliedScale = false; this.impliedScale = false; } if (this._parsingComplete) { this._updatePitches(); } } get figure() { return this._figure; } set figure(newFigure) { this._figure = newFigure; if (this._parsingComplete) { this._parseFigure(); this._updatePitches(); } } get figureAndKey() { let tonicName = this.key.tonic.name; let mode = ''; if (this.key.mode !== undefined) { mode = ' ' + this.key.mode; } if (mode === ' minor') { tonicName = tonicName.toLowerCase(); } else if (mode === ' major') { tonicName = tonicName.toUpperCase(); } return this.figure + ' in ' + tonicName + mode; } get degreeName() { if (this.scaleDegree < 7) { return [ undefined, 'Tonic', 'Supertonic', 'Mediant', 'Subdominant', 'Dominant', 'Submediant', ][this.scaleDegree]; } else { const tonicPitch = this.key.tonic; let diffRootToTonic = (tonicPitch.ps - this.root().ps) % 12; if (diffRootToTonic < 0) { diffRootToTonic += 12; } if (diffRootToTonic === 1) { return 'Leading-tone'; } else { return 'Subtonic'; } } } /** * Update the .pitches array. Called at instantiation, but not automatically afterwards. * */ _updatePitches() { let useScale; if (this.secondaryRomanNumeralKey !== undefined) { useScale = this.secondaryRomanNumeralKey; } else if (!this.useImpliedScale) { useScale = this.key; } else { useScale = this.impliedScale; } this.scaleCardinality = 7; // simple speedup; const bassScaleDegree = this.bassScaleDegreeFromNotation( this.figuresNotationObj ); const bassPitch = useScale.pitchFromDegree( bassScaleDegree, 'ascending' ); const pitches = [bassPitch]; let lastPitch = bassPitch; const numberNotes = this.figuresNotationObj.numbers.length; for (let j = 0; j < numberNotes; j++) { const i = numberNotes - j - 1; const thisScaleDegree = bassScaleDegree + this.figuresNotationObj.numbers[i] - 1; const newPitch = useScale.pitchFromDegree( thisScaleDegree, 'ascending' ); const pitchName = this.figuresNotationObj.modifiers[ i ].modifyPitchName(newPitch.name); const newNewPitch = new pitch.Pitch(pitchName); newNewPitch.octave = newPitch.octave; if (newNewPitch.ps < lastPitch.ps) { newNewPitch.octave += 1; } pitches.push(newNewPitch); lastPitch = newNewPitch; } if (this.frontAlterationTransposeInterval !== undefined) { const newPitches = []; for (const thisPitch of pitches) { const newPitch = this.frontAlterationTransposeInterval.transposePitch( thisPitch ); newPitches.push(newPitch); } this.pitches = newPitches; } else { this.pitches = pitches; } this._matchAccidentalsToQuality(this.impliedQuality); this.scaleOffset = this.frontAlterationTransposeInterval; if (this.omittedSteps.length) { const omittedPitches = []; for (const thisCS of this.omittedSteps) { const p = this.getChordStep(thisCS); if (p !== undefined) { omittedPitches.push(p.name); } } const newPitches = []; for (const thisPitch of pitches) { if (!omittedPitches.includes(thisPitch.name)) { newPitches.push(thisPitch); } } this.pitches = newPitches; // do something... } this._correctBracketedPitches(); } bassScaleDegreeFromNotation(notationObject) { const c = new pitch.Pitch('C3'); const cDNN = c.diatonicNoteNum; // always 22 const pitches = [c]; for (const i of notationObject.numbers) { const distanceToMove = i - 1; const newDiatonicNumber = cDNN + distanceToMove; const [newStep, newOctave] = interval.convertDiatonicNumberToStep( newDiatonicNumber ); const newPitch = new pitch.Pitch('C3'); newPitch.step = newStep; newPitch.octave = newOctave; pitches.push(newPitch); } const tempChord = new chord.Chord(pitches); const rootDNN = tempChord.root().diatonicNoteNum; const staffDistanceFromBassToRoot = rootDNN - cDNN; let bassSD = common.posMod( this.scaleDegree - staffDistanceFromBassToRoot, 7 ); if (bassSD === 0) { bassSD = 7; } return bassSD; } _matchAccidentalsToQuality(impliedQuality) { const correctSemitones = this._findSemitoneSizeForQuality( impliedQuality ); const chordStepsToExamine = [3, 5, 7]; for (let i = 0; i < chordStepsToExamine.length; i++) { const thisChordStep = chordStepsToExamine[i]; const thisCorrect = correctSemitones[i]; const thisSemis = this.semitonesFromChordStep(thisChordStep); if (thisCorrect === undefined) { continue; } if (thisSemis === undefined) { continue; } if (thisSemis === thisCorrect) { continue; } let correctedSemis = thisCorrect - thisSemis; if (correctedSemis >= 6) { correctedSemis = -1 * (12 - correctedSemis); } else if (correctedSemis <= -6) { correctedSemis += 12; } const faultyPitch = this.getChordStep(thisChordStep); // TODO: check for faultyPitch is undefined if (faultyPitch.accidental === undefined) { faultyPitch.accidental = new pitch.Accidental(correctedSemis); } else { const acc = faultyPitch.accidental; correctedSemis += acc.alter; if (correctedSemis >= 6) { correctedSemis = -1 * (12 - correctedSemis); } else if (correctedSemis <= -6) { correctedSemis += 12; } acc.set(correctedSemis); } } } _correctForSecondaryRomanNumeral(useScale, figure) { if (figure === undefined) { figure = this._figure; } let workingFigure = figure; const rx = new RegExp('(.*?)/([#a-np-zA-NP-Z].*)'); const match = rx.exec(figure); if (match !== null) { const primaryFigure = match[1]; const secondaryFigure = match[2]; const secondaryRomanNumeral = new RomanNumeral( secondaryFigure, useScale, this.caseMatters ); this.secondaryRomanNumeral = secondaryRomanNumeral; let secondaryMode; if (secondaryRomanNumeral.quality === 'minor') { secondaryMode = 'minor'; } else if (secondaryRomanNumeral.quality === 'major') { secondaryMode = 'minor'; } else if (secondaryRomanNumeral.semitonesFromChordStep(3) === 3) { secondaryMode = 'minor'; } else { secondaryMode = 'major'; } this.secondaryRomanNumeralKey = new key.Key( secondaryRomanNumeral.root().name, secondaryMode ); useScale = this.secondaryRomanNumeralKey; workingFigure = primaryFigure; } return [workingFigure, useScale]; } _parseOmittedSteps(workingFigure) { const omittedSteps = []; const rx = new RegExp(/\[no(\d+)]s*/); let match = rx.exec(workingFigure); while (match !== null) { let thisStep = match[1]; thisStep = parseInt(thisStep); thisStep = thisStep % 7 || 7; omittedSteps.push(thisStep); workingFigure = workingFigure.replace(rx, ''); match = rx.exec(workingFigure); } this.omittedSteps = omittedSteps; return workingFigure; } _parseBracketedAlterations(workingFigure) { const bracketedAlterations = this.bracketedAlterations; const rx = new RegExp(/\[(b+|-+|#+)(\d+)]/); let match = rx.exec(workingFigure); while (match !== null) { const matchAlteration = match[1]; const matchDegree = parseInt(match[2]); bracketedAlterations.push([matchAlteration, matchDegree]); workingFigure = workingFigure.replace(rx, ''); match = rx.exec(workingFigure); } return workingFigure; } _findSemitoneSizeForQuality(impliedQuality) { let correctSemitones; if (impliedQuality === 'major') { correctSemitones = [4, 7]; } else if (impliedQuality === 'minor') { correctSemitones = [3, 7]; } else if (impliedQuality === 'diminished') { correctSemitones = [3, 6, 9]; } else if (impliedQuality === 'half-diminished') { correctSemitones = [3, 6, 10]; } else if (impliedQuality === 'augmented') { correctSemitones = [4, 8]; } else if (impliedQuality === 'dominant-seventh') { correctSemitones = [4, 7, 10]; } else { correctSemitones = []; } return correctSemitones; } /** * Gives a string display. Note that since inversion is not yet supported * it needs to be given separately. * * Inverting 7th chords does not work. * * @param {string} displayType - ['roman', 'bassName', 'nameOnly', other] * @param {int} [inversion=0] * @returns {string} */ asString(displayType, inversion) { const keyObj = this.key; const tonicName = keyObj.tonic.name; const mode = keyObj.mode; // specifying inversion is for backwards compatibility only. if (inversion === undefined) { inversion = this.inversion(); } let inversionName = ''; if (inversion === 1) { if (displayType === 'roman') { inversionName = '6'; } else { inversionName = ' (first inversion)'; } } else if (inversion === 2) { if (displayType === 'roman') { inversionName = '64'; } else { inversionName = ' (second inversion)'; } } let fullChordName; let connector = ' in '; let suffix = ''; if (displayType === 'roman') { fullChordName = this.figure; fullChordName = fullChordName.replace('/o', 'ø'); } else if (displayType === 'nameOnly') { // use only with only choice being tonicName fullChordName = ''; connector = ''; suffix = ' triad'; } else if (displayType === 'bassName') { fullChordName = this.bass().name.replace(/-/, 'b'); } else { // "default" or "degreeName" submediant, etc... fullChordName = this.degreeName; if (this.numbers !== undefined) { fullChordName += ' ' + this.numbers.toString(); } } let tonicDisplay = tonicName.replace(/-/, 'b'); if (mode === 'minor') { tonicDisplay = tonicDisplay.toLowerCase(); } const chordStr = fullChordName + inversionName + connector + tonicDisplay + ' ' + mode + suffix; return chordStr; } }