Source: music21/duration.js

/**
 * music21j -- Javascript reimplementation of Core music21 features.
 * music21/duration -- duration routines
 *
 * Copyright (c) 2013-19, Michael Scott Cuthbert and cuthbertLab
 * Based on music21, Copyright (c) 2006-19, Michael Scott Cuthbert and cuthbertLab
 *
 * Module that holds **music21** classes and
 * tools for dealing with durations, especially
 * the {@link music21.duration.Duration} class.
 *
 * Duration module. See {@link music21.duration}
 *
 * @module music21/duration
 * @namespace music21.duration
 * @memberof music21
 * @requires music21/common
 * @requires music21/prebase
 * @exports music21/duration
 */
import { Music21Exception } from './exceptions21.js';

import * as common from './common.js';
import { debug } from './debug.js';
import * as prebase from './prebase.js';

/**
 * Object mapping int to name, as in `{1: 'whole'}` etc.
 *
 * @memberof music21.duration
 * @type {Object}
 */
export const typeFromNumDict = {
    1: 'whole',
    2: 'half',
    4: 'quarter',
    8: 'eighth',
    16: '16th',
    32: '32nd',
    64: '64th',
    128: '128th',
    256: '256th',
    512: '512th',
    1024: '1024th',
    0: 'zero',
    '0.5': 'breve',
    '0.25': 'longa',
    '0.125': 'maxima',
    '0.0625': 'duplex-maxima',
};

export const quarterTypeIndex = 6; // where is quarter in the following array.

export const ordinalTypeFromNum = [
    'duplex-maxima',
    'maxima',
    'longa',
    'breve',
    'whole',
    'half',
    'quarter',
    'eighth',
    '16th',
    '32nd',
    '64th',
    '128th',
    '256th',
    '512th',
    '1024th',
];

export const vexflowDurationArray = [
    undefined,
    undefined,
    undefined,
    undefined,
    'w',
    'h',
    'q',
    '8',
    '16',
    '32',
    undefined,
    undefined,
    undefined,
    undefined,
    undefined,
];

/**
 * Duration object; found as the `.duration` attribute on {@link music21.base.Music21Object} instances
 * such as {@link music21.note.Note}
 *
 * @class Duration
 * @memberof music21.duration
 * @extends music21.prebase.ProtoM21Object
 * @param {(number|undefined)} ql - quarterLength (default 1.0)
 */
export class Duration extends prebase.ProtoM21Object {
    constructor(ql) {
        super();
        this.isGrace = false;
        this._quarterLength = 0.0;
        this._dots = 0;
        this._durationNumber = undefined;
        this._type = 'zero';
        this._tuplets = [];
        if (typeof ql === 'string') {
            this.type = ql;
        } else if (ql !== undefined) {
            this.quarterLength = ql;
        }
        this._cloneCallbacks._tuplets = this.cloneCallbacksTupletFunction;
    }

    stringInfo() {
        return this.quarterLength.toString();
    }

    /**
     * Read or sets the number of dots on the duration.
     *
     * Updates the quarterLength
     *
     * @type {number}
     * @default 0
     * @example
     * var d = new music21.duration.Duration(2);
     * d.dots === 0; // true
     * d.dots = 1;
     * d.quarterLength == 3; // true;
     */
    get dots() {
        return this._dots;
    }

    set dots(numDots) {
        this._dots = numDots;
        this.updateQlFromFeatures();
    }

    /**
     * Read or sets the quarterLength of the Duration
     *
     * Updates the type, dots, tuplets(?)
     *
     * @type {number}
     * @default 1.0
     * @example
     * var d = new music21.duration.Duration(2);
     * d.quarterLength == 2.0; // true;
     * d.quarterLength = 1.75;
     * d.dots == 2; // true
     * d.type == 'quarter'; // true
     */
    get quarterLength() {
        return this._quarterLength;
    }

    set quarterLength(ql) {
        if (ql === undefined) {
            ql = 1.0;
        }
        this._quarterLength = ql;
        this.updateFeaturesFromQl();
    }

    /**
     * Read or sets the type of the duration.
     *
     * Updates the quarterLength
     *
     * @type {string}
     * @default 'quarter'
     * @example
     * var d = new music21.duration.Duration(2);
     * d.type == 'half; // true
     * d.type = 'breve';
     * d.quarterLength == 8.0; // true
     * d.dots = 1;
     * d.type = 'quarter'; // will not change dots
     * d.quarterLength == 1.5; // true
     */
    get type() {
        return this._type;
    }

    set type(typeIn) {
        const typeNumber = ordinalTypeFromNum.indexOf(typeIn);
        if (typeNumber === -1) {
            console.log('invalid type ' + typeIn);
            throw new Music21Exception('invalid type ' + typeIn);
        }
        this._type = typeIn;
        this.updateQlFromFeatures();
    }

    /**
     * Reads the tuplet Array for the duration.
     *
     * The tuplet array should be considered Read Only.
     * Use {@link music21.duration.Duration#appendTuplet} to
     * add a tuplet (no way to remove yet)
     *
     * @type {music21.duration.Tuplet[]}
     * @default []
     */
    get tuplets() {
        return this._tuplets;
    }

    /**
     * Read-only: the duration expressed for VexFlow
     *
     * @type {string}
     * @default 'd'
     * @readonly
     * @example
     * var d = new music21.duration.Duration(2);
     * d.vexflowDuration == 'h'; // true;
     * d.dots = 2;
     * d.vexflowDuration == 'hdd'; // true;
     */
    get vexflowDuration() {
        const typeNumber = ordinalTypeFromNum.indexOf(this.type);
        let vd = vexflowDurationArray[typeNumber];
        if (this.dots > 0) {
            for (let i = 0; i < this.dots; i++) {
                vd += 'd'; // vexflow does not handle double dots .. or does it???
            }
        }
        return vd;
    }

    cloneCallbacksTupletFunction(tupletKey, ret, obj) {
        // make sure that tuplets clone properly
        const newTuplets = [];
        for (let i = 0; i < obj[tupletKey].length; i++) {
            const newTuplet = obj[tupletKey][i].clone();
            // console.log('cloning tuplets', obj[tupletKey][i], newTuplet);
            newTuplets.push(newTuplet);
        }
        ret[tupletKey] = newTuplets;
    }

    /**
     *
     * @param {number} ql
     * @returns {number}
     * @private
     */
    _findDots(ql) {
        if (ql === 0) {
            return 0;
        } // zero length stream probably;
        const typeNumber = ordinalTypeFromNum.indexOf(this._type);
        const powerOfTwo = 2 ** (quarterTypeIndex - typeNumber);
        // alert(undottedQL * 1.5 + " " + ql)
        // console.log('find dots called on ql: ', ql, typeNumber, powerOfTwo);
        for (let dotsNum = 0; dotsNum <= 4; dotsNum++) {
            const dotMultiplier
                = ((2 ** dotsNum) - 1.0) / 2 ** dotsNum;
            const durationMultiplier = 1 + dotMultiplier;
            if (Math.abs(powerOfTwo * durationMultiplier - ql) < 0.0001) {
                return dotsNum;
            }
        }
        if (debug) {
            console.log('no dots available for ql; probably a tuplet', ql);
        }
        return 0;
    }

    updateQlFromFeatures() {
        const typeNumber = ordinalTypeFromNum.indexOf(this._type); // must be set property
        const undottedQuarterLength = (
            2 ** (quarterTypeIndex - typeNumber)
        );
        const dottedMultiplier
            = 1 + ((2 ** this._dots) - 1.0) / (2 ** this._dots);
        const unTupletedQl = undottedQuarterLength * dottedMultiplier;
        let tupletCorrectedQl = unTupletedQl;
        this._tuplets.forEach(tuplet => {
            tupletCorrectedQl *= tuplet.tupletMultiplier();
        });
        this._quarterLength = tupletCorrectedQl;
    }

    updateFeaturesFromQl() {
        const ql = this._quarterLength;
        this._tuplets = [];
        if (ql === 0) {
            this._type = 'zero';
            this._dots = 0;
            return;
        }
        const powerOfTwo = Math.floor(Math.log(ql + 0.00001) / Math.log(2));
        let typeNumber = quarterTypeIndex - powerOfTwo;
        this._type = ordinalTypeFromNum[typeNumber];
        // console.log(this._findDots);
        this._dots = this._findDots(ql);

        const undottedQuarterLength = (
            2 ** (quarterTypeIndex - typeNumber)
        );
        const dottedMultiplier
            = 1 + (2 **  this._dots - 1) / 2 ** this._dots;
        let unTupletedQl = undottedQuarterLength * dottedMultiplier;
        if (unTupletedQl !== ql && ql !== 0) {
            typeNumber -= 1;
            this._type = ordinalTypeFromNum[typeNumber]; // increase type: eighth to quarter etc.
            unTupletedQl *= 2;
            const tupletRatio = ql / unTupletedQl;
            const ratioRat = common.rationalize(tupletRatio);
            if (ratioRat === undefined) {
                // probably a Stream with a length that is inexpressable;
            } else {
                const t = new Tuplet(
                    ratioRat.denominator,
                    ratioRat.numerator,
                    new Duration(unTupletedQl)
                );
                this.appendTuplet(t, true); // skipUpdateQl
            }
            // console.log(ratioRat, ql, unTupletedQl);
        }
    }

    /**
     * Add a tuplet to music21j
     *
     * @param {music21.duration.Tuplet} newTuplet - tuplet to add to `.tuplets`
     * @param {boolean} [skipUpdateQl=false] - update the quarterLength afterwards?
     * @returns {this}
     */
    appendTuplet(newTuplet, skipUpdateQl) {
        newTuplet.frozen = true;
        this._tuplets.push(newTuplet);
        if (skipUpdateQl !== true) {
            this.updateQlFromFeatures();
        }
        return this;
    }
}

/**
 * Represents a Tuplet; found in {@link music21.duration.Duration#tuplets}
 *
 * @memberof music21.duration
 * @extends music21.prebase.ProtoM21Object
 * @param {number} [numberNotesActual=3] - numerator of the tuplet
 * @param {number} [numberNotesNormal=2] - denominator of the tuplet
 * @param {(music21.duration.Duration|number)} [durationActual] - duration or quarterLength of duration type, default music21.duration.Duration(0.5)
 * @param {(music21.duration.Duration|number)} [durationNormal] - unused; see music21p for description
 */
export class Tuplet extends prebase.ProtoM21Object {
    constructor(
        numberNotesActual=3,
        numberNotesNormal=2,
        durationActual,
        durationNormal
    ) {
        super();
        this.numberNotesActual = numberNotesActual;
        this.numberNotesNormal = numberNotesNormal;
        this.durationActual = durationActual || new Duration(0.5);
        if (typeof this.durationActual === 'number') {
            this.durationActual = new Duration(this.durationActual);
        }
        this.durationNormal = durationNormal || this.durationActual;

        this.frozen = false;
        this.type = undefined;
        /**
         * Show a bracket above the tuplet
         *
         * @property {boolean} bracket
         * @default true
         */
        this.bracket = true;
        /**
         * Bracket placement. Options are `above` or `below`.
         *
         * @property {string} placement
         * @default 'above'
         */
        this.placement = 'above';

        /**
         * What to show above the Tuplet. Options are `number`, `type`, or (string) `none`.
         *
         * @property {string} tupletActualShow
         * @default 'number'
         */
        this.tupletActualShow = 'number';
        this.tupletNormalShow = undefined; // undefined, 'ratio' for ratios, 'type' for ratioed notes (does not work)
    }

    /**
     * A nice name for the tuplet.
     *
     * @type {string}
     * @readonly
     */
    get fullName() {
        // actual is what is presented to viewer
        const numActual = this.numberNotesActual;
        const numNormal = this.numberNotesNormal;

        if (numActual === 3 && numNormal === 2) {
            return 'Triplet';
        } else if (numActual === 5 && (numNormal === 4 || numNormal === 2)) {
            return 'Quintuplet';
        } else if (numActual === 6 && numNormal === 4) {
            return 'Sextuplet';
        }
        const ordStr = common.ordinalAbbreviation(numNormal, true); // plural
        return (
            'Tuplet of '
            + numActual.toString()
            + '/'
            + numNormal.toString()
            + ordStr
        );
    }

    /**
     * Set both durationActual and durationNormal for the tuplet.
     *
     * @param {string} type - a duration type, such as `half`, `quarter`
     * @returns {music21.duration.Duration} A converted {@link music21.duration.Duration} matching `type`
     */
    setDurationType(type) {
        if (this.frozen === true) {
            throw new Music21Exception(
                'A frozen tuplet (or one attached to a duration) is immutable'
            );
        }
        this.durationActual = new Duration(type);
        this.durationNormal = this.durationActual;
        return this.durationActual;
    }

    /**
     * Sets the tuplet ratio.
     *
     * @param {Number} actual - number of notes in actual (e.g., 3)
     * @param {Number} normal - number of notes in normal (e.g., 2)
     * @returns {undefined}
     */
    setRatio(actual, normal) {
        if (this.frozen === true) {
            throw new Music21Exception(
                'A frozen tuplet (or one attached to a duration) is immutable'
            );
        }
        this.numberNotesActual = actual || 3;
        this.numberNotesNormal = normal || 2;
    }

    /**
     * Get the quarterLength corresponding to the total length that
     * the completed tuplet (i.e., 3 notes in a triplet) would occupy.
     *
     * @returns {Number} A quarter length.
     */
    totalTupletLength() {
        return this.numberNotesNormal * this.durationNormal.quarterLength;
    }

    /**
     * The amount by which each quarter length is multiplied to get
     * the tuplet. For instance, in a normal triplet, this is 0.666
     *
     * @returns {Number} A float of the multiplier
     */
    tupletMultiplier() {
        const lengthActual = this.durationActual.quarterLength;
        return (
            this.totalTupletLength() / (this.numberNotesActual * lengthActual)
        );
    }
}
Music21j, Copyright © 2013-2021 Michael Scott Asato Cuthbert.
Documentation generated by JSDoc 3.6.3 on Wed Jul 31st 2019 using the DocStrap template.