Source: music21/stream.js

/**
 * music21j -- Javascript reimplementation of Core music21p features.
 * music21/stream -- Streams -- objects that hold other Music21Objects
 *
 * Does not implement the full features of music21p Streams by a long shot...
 *
 * Copyright (c) 2013-19, Michael Scott Cuthbert and cuthbertLab
 * Based on music21 (=music21p), Copyright (c) 2006-19, Michael Scott Cuthbert and cuthbertLab
 *
 * powerful stream module, See {@link music21.stream} namespace
 * @exports music21/stream
 *
 * Streams are powerful music21 objects that hold Music21Object collections,
 * such as {@link music21.note.Note} or {@link music21.chord.Chord} objects.
 *
 * Understanding the {@link music21.stream.Stream} object is of fundamental
 * importance for using music21.  Definitely read the music21(python) tutorial
 * on using Streams before proceeding
 *
 * @namespace music21.stream
 * @memberof music21
 * @requires music21/base
 * @requires music21/renderOptions
 * @requires music21/clef
 * @requires music21/vfShow
 * @requires music21/duration
 * @requires music21/common
 * @requires music21/meter
 * @requires music21/pitch
 * @requires jQuery
 *
 */
import * as $ from 'jquery';
import * as MIDI from 'midicube';

import { Music21Exception } from './exceptions21.js';
import { debug } from './debug.js';

import * as base from './base';
import * as beam from './beam.js';
import * as clef from './clef.js';
import * as common from './common.js';
import * as duration from './duration.js';
import * as instrument from './instrument.js';
import * as meter from './meter.js';
import * as note from './note.js';
import * as pitch from './pitch.js';
import * as renderOptions from './renderOptions.js';
import * as vfShow from './vfShow.js';

// eslint-disable-next-line import/no-cycle
import { GeneralObjectExporter } from './musicxml/m21ToXml.js';

import * as filters from './stream/filters.js';
import * as iterator from './stream/iterator.js';

export { filters };
export { iterator };

export class StreamException extends Music21Exception {}

function _exportMusicXMLAsText(s) {
    const gox = new GeneralObjectExporter(s);
    return gox.parse();
}

/**
 * A generic Stream class -- a holder for other music21 objects
 * Will be subclassed into {@link music21.stream.Score},
 * {@link music21.stream.Part},
 * {@link music21.stream.Measure},
 * {@link music21.stream.Voice}, but most functions will be found here.
 *
 * @class Stream
 * @memberof music21.stream
 * @extends music21.base.Music21Object
 *
 * @property {music21.base.Music21Object[]} elements - the elements in the stream. DO NOT MODIFY individual components (consider it like a Python tuple)
 * @property {number} length - (readonly) the number of elements in the stream.
 * @property {music21.duration.Duration} duration - the total duration of the stream's elements
 * @property {number} highestTime -- the highest time point in the stream's elements
 * @property {music21.clef.Clef} clef - the clef for the Stream (if there is one; if there are multiple, then the first clef)
 * @property {music21.meter.TimeSignature} timeSignature - the first TimeSignature of the Stream
 * @property {music21.key.KeySignature} keySignature - the first KeySignature for the Stream
 * @property {music21.renderOptions.RenderOptions} renderOptions - an object specifying how to render the stream
 * @property {music21.stream.Stream} flat - (readonly) a flattened representation of the Stream
 * @property {music21.stream.Stream} notes - (readonly) the stream with only {@link music21.note.Note} and {@link music21.chord.Chord} objects included
 * @property {music21.stream.Stream} notesAndRests - (readonly) like notes but also with {@link music21.note.Rest} objects included
 * @property {music21.stream.Stream} parts - (readonly) a filter on the Stream to just get the parts (NON-recursive)
 * @property {music21.stream.Stream} measures - (readonly) a filter on the Stream to just get the measures (NON-recursive)
 * @property {number} tempo - tempo in beats per minute (will become more sophisticated later, but for now the whole stream has one tempo
 * @property {music21.instrument.Instrument|undefined} instrument - an instrument object associated with the stream (can be set with a string also, but will return an `Instrument` object)
 * @property {Boolean} autoBeam - whether the notes should be beamed automatically or not (will be moved to `renderOptions` soon)
 * @property {Vex.Flow.Stave|undefined} activeVFStave - the current Stave object for the Stream
 * @property {music21.vfShow.Renderer|undefined} activeVFRenderer - the current vfShow.Renderer object for the Stream
 * @property {int} [staffLines=5] - number of staff lines
 * @property {function|undefined} changedCallbackFunction - function to call when the Stream changes through a standard interface
 * @property {number} maxSystemWidth - confusing... should be in renderOptions
 */
export class Stream extends base.Music21Object {
    constructor() {
        super();
        // class variables;
        this.isStream = true;
        this.isMeasure = false;
        this.classSortOrder = -20;
        this.recursionType = 'elementsFirst';

        this._duration = undefined;

        this._elements = [];
        this._offsetDict = new WeakMap();

        this.autoSort = true;
        this.isSorted = true;
        this.isFlat = true;

        this._clef = undefined;
        this.displayClef = undefined;

        this._keySignature = undefined; // a music21.key.KeySignature object
        this._timeSignature = undefined; // a music21.meter.TimeSignature object
        this._instrument = undefined;

        this._autoBeam = undefined;
        this.activeVFStave = undefined;
        this.activeVFRenderer = undefined;
        this.renderOptions = new renderOptions.RenderOptions();
        this._tempo = undefined;

        this.staffLines = 5;

        this._stopPlaying = false;
        this._allowMultipleSimultaneousPlays = true; // not implemented yet.
        this.changedCallbackFunction = undefined; // for editable svges
        /**
         * A function bound to the current stream that
         * will changes the stream. Used in editableAccidentalDOM, among other places.
         *
         *      var can = s.appendNewDOM();
         *      $(can).on('click', s.DOMChangerFunction);
         *
         * @param {MouseEvent|TouchEvent} e
         * @returns {music21.base.Music21Object|undefined} - returns whatever changedCallbackFunction does.
         */
        this.DOMChangerFunction = e => {
            const canvasOrSVGElement = e.currentTarget;
            if (!(canvasOrSVGElement instanceof HTMLElement) && !(canvasOrSVGElement instanceof SVGElement)) {
                return undefined;
            }

            const [clickedDiatonicNoteNum, foundNote] = this.findNoteForClick(
                canvasOrSVGElement,
                e
            );
            if (foundNote === undefined) {
                if (debug) {
                    console.log('No note found');
                }
                return undefined;
            }
            return this.noteChanged(
                clickedDiatonicNoteNum,
                foundNote,
                canvasOrSVGElement
            );
        };
    }

    /**
     *
     * @returns {IterableIterator<music21.base.Music21Object>}
     */
    * [Symbol.iterator]() {
        if (this.autoSort && !this.isSorted) {
            this.sort();
        }

        for (let i = 0; i < this.length; i++) {
            yield this.get(i);
        }
    }

    forEach(callback, thisArg) {
        if (thisArg !== undefined) {
            callback = callback.bind(thisArg);
        }
        let i = 0;
        for (const el of this) {
            callback(el, i, this);
            i += 1;
        }
    }


    get duration() {
        if (this._duration !== undefined) {
            return this._duration;
        }
        return new duration.Duration(this.highestTime);
    }

    set duration(newDuration) {
        this._duration = newDuration;
    }

    get highestTime() {
        let highestTime = 0.0;
        for (const el of this) {
            let endTime = el.offset;
            if (el.duration !== undefined) {
                endTime += el.duration.quarterLength;
            }
            if (endTime > highestTime) {
                highestTime = endTime;
            }
        }
        return highestTime;
    }

    get semiFlat() {
        return this._getFlatOrSemiFlat(true);
    }

    get flat() {
        return this._getFlatOrSemiFlat(false);
    }

    _getFlatOrSemiFlat(retainContainers) {
        const newSt = this.clone(false);
        if (!this.isFlat) {
            newSt.elements = [];
            for (const el of this) {
                if (el.isStream) {
                    if (retainContainers) {
                        newSt.append(el);
                    }
                    const offsetShift = this.elementOffset(el);
                    // console.log('offsetShift', offsetShift, el.classes[el.classes.length -1]);
                    const elFlat = el._getFlatOrSemiFlat(retainContainers);
                    for (const elFlatElement of elFlat) {
                        // offset should NOT be null because already in Stream
                        const elFlatElementOffset = elFlat.elementOffset(elFlatElement);
                        newSt.insert(elFlatElementOffset + offsetShift, elFlatElement);
                    }
                } else {
                    newSt.insert(this.elementOffset(el), el);
                }
            }
        }
        if (!retainContainers) {
            newSt.isFlat = true;
            this.coreElementsChanged({ updateIsFlat: false });
        } else {
            this.coreElementsChanged();
        }
        return newSt;
    }

    get notes() {
        return this.getElementsByClass(['Note', 'Chord']);
    }

    get notesAndRests() {
        return this.getElementsByClass('GeneralNote');
    }

    get tempo() {
        if (this._tempo === undefined && this.activeSite !== undefined) {
            return this.activeSite.tempo;
        } else if (this._tempo === undefined) {
            return 150;
        } else {
            return this._tempo;
        }
    }

    set tempo(newTempo) {
        this._tempo = newTempo;
    }

    get instrument() {
        if (this._instrument === undefined && this.activeSite !== undefined) {
            return this.activeSite.instrument;
        } else {
            return this._instrument;
        }
    }

    set instrument(newInstrument) {
        if (typeof newInstrument === 'string') {
            newInstrument = new instrument.Instrument(newInstrument);
        }
        this._instrument = newInstrument;
    }

    _specialContext(attr) {
        const privAttr = '_' + attr;
        if (this[privAttr] !== undefined) {
            return this[privAttr];
        }
        // should be:
        // const contextClef = this.getContextByClass('Clef');
        //        const context = this.getContextByClass('Stream', { getElementMethod: 'getElementBefore' });
        //        let contextObj;
        //        if (context !== undefined && context !== this) {
        //            contextObj = context[privAttr];
        //        }
        for (const site of this.sites.yieldSites()) {
            if (site === undefined) {
                continue;
            }
            const contextObj = site._firstElementContext('attr') || site._specialContext('attr');
            if (contextObj !== undefined) {
                return contextObj;
            }
        }
        return undefined;
    }

    _firstElementContext(attr) {
        const firstElements = this
            .getElementsByOffset(0.0)
            .getElementsByClass(attr.charAt(0).toUpperCase() + attr.slice(1));
        if (firstElements.length) {
            return firstElements.get(0);
        } else {
            return undefined;
        }
    }

    get clef() {
        return this.getSpecialContext('clef', true);
    }

    set clef(newClef) {
        const oldClef = this._firstElementContext('clef');
        if (oldClef !== undefined) {
            this.replace(oldClef, newClef);
        } else {
            this.insert(0.0, newClef);
        }
        this._clef = newClef;
    }

    get keySignature() {
        return this.getSpecialContext('keySignature', true);
    }

    set keySignature(newKeySignature) {
        const oldKS = this._firstElementContext('keySignature');
        if (oldKS !== undefined) {
            this.replace(oldKS, newKeySignature);
        } else {
            this.insert(0.0, newKeySignature);
        }
        this._keySignature = newKeySignature;
    }

    get timeSignature() {
        return this.getSpecialContext('timeSignature', true);
    }

    set timeSignature(newTimeSignature) {
        if (typeof newTimeSignature === 'string') {
            newTimeSignature = new meter.TimeSignature(newTimeSignature);
        }
        const oldTS = this._firstElementContext('timeSignature');
        if (oldTS !== undefined) {
            this.replace(oldTS, newTimeSignature);
        } else {
            this.insert(0.0, newTimeSignature);
        }
        this._timeSignature = newTimeSignature;
    }

    get autoBeam() {
        return this._specialContext('autoBeam');
    }

    set autoBeam(ab) {
        this._autoBeam = ab;
    }

    get maxSystemWidth() {
        let baseMaxSystemWidth = 750;
        if (
            this.renderOptions.maxSystemWidth === undefined
            && this.activeSite !== undefined
        ) {
            baseMaxSystemWidth = this.activeSite.maxSystemWidth;
        } else if (this.renderOptions.maxSystemWidth !== undefined) {
            baseMaxSystemWidth = this.renderOptions.maxSystemWidth;
        }
        return baseMaxSystemWidth / this.renderOptions.scaleFactor.x;
    }

    set maxSystemWidth(newSW) {
        this.renderOptions.maxSystemWidth
            = newSW * this.renderOptions.scaleFactor.x;
    }

    get parts() {
        return this.getElementsByClass('Part');
    }

    get measures() {
        return this.getElementsByClass('Measure');
    }

    get voices() {
        return this.getElementsByClass('Voice');
    }

    get length() {
        return this._elements.length;
    }

    get elements() {
        return this._elements;
    }

    set elements(newElements) {
        let highestOffsetSoFar = 0.0;
        this.clear();
        const tempInsert = [];
        let i;
        let thisEl;
        if (newElements.isStream === true) {
            // iterate to set active site;
            for (const unused of newElements) {} // eslint-disable-line no-empty
            newElements = newElements.elements;
        }

        for (i = 0; i < newElements.length; i++) {
            thisEl = newElements[i];
            const thisElOffset = thisEl.offset;
            if (thisElOffset === undefined || thisElOffset === highestOffsetSoFar) {
                // append
                this._elements.push(thisEl);
                this.setElementOffset(thisEl, highestOffsetSoFar);
                if (thisEl.duration === undefined) {
                    console.error('No duration for ', thisEl, ' in ', this);
                }
                highestOffsetSoFar += thisEl.duration.quarterLength;
            } else {
                // append -- slow
                tempInsert.push(thisEl);
            }
        }
        // console.warn('end', highestOffsetSoFar, tempInsert);
        for (i = 0; i < tempInsert.length; i++) {
            thisEl = tempInsert[i];
            this.insert(thisEl.offset, thisEl);
        }
        this.coreElementsChanged(); // would be called already if newElements != [];
    }

    /**
     * getSpecialContext is a transitional replacement for
     * .clef, .keySignature, .timeSignature that looks
     * for context to get the appropriate element as ._clef, etc.
     * as a way of making the older music21j attributes still work while
     * transitioning to a more music21p-like approach.
     *
     * May be removed
     */
    getSpecialContext(context, warnOnCall=false) {
        const first_el = this._firstElementContext(context);
        if (first_el !== undefined) {
            return first_el;
        }
        const special_context = this._specialContext(context);
        if (special_context === undefined) {
            return undefined;
        }
        if (warnOnCall) {
            console.warn(`Calling special context ${context}!`);
        }
        return special_context;
    }

    clear() {
        this._elements = [];
        this._offsetDict = new WeakMap();
        this.isFlat = true;
        this.isSorted = true;
    }

    /* override protoM21Object.clone() */
    clone(deep=true) {
        const ret = Object.create(this.constructor.prototype);
        for (const key in this) {
            if ({}.hasOwnProperty.call(this, key) === false) {
                continue;
            }
            if (key === '_activeSite') {
                ret[key] = this[key];
            } else if (key === 'renderOptions') {
                ret[key] = common.merge({}, this[key]);
            } else if (
                deep !== true
                && (key === '_elements')
            ) {
                ret[key] = this[key].slice(); // shallow copy...
            } else if (deep !== true && key === '_offsetDict') {
                ret._offsetDict = new WeakMap();
                for (const el of this.elements) {
                    ret._offsetDict.set(el, this._offsetDict.get(el));
                }
            } else if (
                deep
                && (key === '_elements' || key === '_offsetDict')
            ) {
                if (key === '_elements') {
                    // console.log('got elements for deepcopy');
                    ret.clear();
                    for (let j = 0; j < this._elements.length; j++) {
                        const el = this._elements[j];
                        // console.log('cloning el: ', el.name);
                        const elCopy = el.clone(deep);
                        ret._elements[j] = elCopy;
                        ret._offsetDict.set(elCopy, this._offsetDict.get(el));
                        elCopy.activeSite = ret;
                    }
                }
            } else if (
                key === 'activeVexflowNote'
                || key === 'storedVexflowstave'
            ) {
                // do nothing -- do not copy vexflowNotes -- permanent recursion
            } else if (key in this._cloneCallbacks) {
                if (this._cloneCallbacks[key] === true) {
                    ret[key] = this[key];
                } else if (this._cloneCallbacks[key] === false) {
                    ret[key] = undefined;
                } else {
                    // call the cloneCallbacks function
                    this._cloneCallbacks[key](key, ret, this);
                }
            } else if (
                Object.getOwnPropertyDescriptor(this, key).get !== undefined
                || Object.getOwnPropertyDescriptor(this, key).set !== undefined
            ) {
                // do nothing
            } else if (typeof this[key] === 'function') {
                // do nothing -- events might not be copied.
            } else if (
                this[key] !== null
                && this[key] !== undefined
                && this[key].isMusic21Object === true
            ) {
                // console.log('cloning...', key);
                ret[key] = this[key].clone(deep);
            } else {
                ret[key] = this[key];
            }
        }
        ret.coreElementsChanged();
        return ret;
    }

    coreElementsChanged({
        updateIsFlat=true,
        clearIsSorted=true,
        memo=undefined, // unused
        keepIndex=false, // unused
    }={}) {
        if (clearIsSorted) {
            this.isSorted = false;
        }
        if (updateIsFlat) {
            this.isFlat = true;
            for (const e of this._elements) {
                if (e.isStream) {
                    this.isFlat = false;
                    break;
                }
            }
        }
    }

    recurse({
        streamsOnly=false,
        restoreActiveSites=true,
        classFilter=undefined,
        skipSelf=true,
    }={}) {
        const includeSelf = !skipSelf;
        const ri = new iterator.RecursiveIterator(this,
            {
                streamsOnly,
                restoreActiveSites,
                includeSelf,
            });
        if (classFilter !== undefined) {
            ri.addFilter(new filters.ClassFilter(classFilter));
        }
        return ri;
    }

    /**
     * Add an element to the end of the stream, setting its `.offset` accordingly
     *
     * @param {music21.base.Music21Object|base.Music21Object|Array} elOrElList - element or list of elements to append
     * @returns {this}
     */
    append(elOrElList) {
        if (Array.isArray(elOrElList)) {
            for (const el of elOrElList) {
                this.append(el);
            }
            return this;
        }

        const el = elOrElList;
        if (!(el instanceof base.Music21Object)) {
            throw new Music21Exception('Can only append a music21 object.');
        }

        try {
            if (
                el.isClassOrSubclass !== undefined
                && el.isClassOrSubclass('NotRest')
            ) {
                // set stem direction on output...;
            }
            const elOffset = this.highestTime;
            this._elements.push(el);
            this.setElementOffset(el, elOffset);
            el.offset = elOffset;
            el.sites.add(this);
            el.activeSite = this;
        } catch (err) {
            console.error(
                'Cannot append element ',
                el,
                ' to stream ',
                this,
                ' : ',
                err
            );
        }
        this.coreElementsChanged({ clearIsSorted: false });
        return this;
    }

    sort() {
        if (this.isSorted) {
            return this;
        }
        this._elements.sort((a, b) => this._offsetDict.get(a) - this._offsetDict.get(b)
            || a.priority - b.priority
            || a.classSortOrder - b.classSortOrder);
        this.isSorted = true;
        return this;
    }

    /**
     * Add an element to the specified place in the stream, setting its `.offset` accordingly
     *
     * @param {number} offset - offset to place.
     * @param {music21.base.Music21Object} el - element to append
     * @param {Object} [config] -- configuration options
     * @param {boolean} [config.ignoreSort=false] -- do not sort
     * @param {boolean} [config.setActiveSite=true] -- set the active site for the inserted element.
     * @returns {this}
     */
    insert(offset, el, { ignoreSort=false, setActiveSite=true }={}) {
        if (el === undefined) {
            throw new StreamException('Cannot insert without an element.');
        }
        try {
            if (!ignoreSort) {
                if (offset <= this.highestTime) {
                    this.isSorted = false;
                }
            }
            this._elements.push(el);
            this.setElementOffset(el, offset);
            el.sites.add(this);
            if (setActiveSite) {
                el.activeSite = this;
            }
            this.coreElementsChanged({ clearIsSorted: false });
        } catch (err) {
            console.error(
                'Cannot insert element ',
                el,
                ' to stream ',
                this,
                ' : ',
                err
            );
        }
        return this;
    }

    /**
     * Inserts a single element at offset, shifting elements at or after it begins
     * later in the stream.
     *
     * In single argument form, assumes it is an element and takes the offset from the element.
     *
     * Unlike music21p, does not take a list of elements.  TODO(msc): add this feature.
     *
     * @param {number|music21.base.Music21Object} offset -- offset of the item to insert
     * @param {music21.base.Music21Object} [elementOrNone] -- element.
     * @return {this}
     */
    insertAndShift(offset, elementOrNone) {
        let element;
        if (elementOrNone === undefined) {
            element = offset;
            offset = element.offset;
        } else {
            element = elementOrNone;
        }
        const amountToShift = element.duration.quarterLength;

        let shiftingOffsets = false;
        for (let i = 0; i < this.length; i++) {
            const existingEl = this._elements[i];
            const existingElOffset = this.elementOffset(existingEl);
            if (!shiftingOffsets && existingElOffset >= offset) {
                shiftingOffsets = true;
            }
            if (shiftingOffsets) {
                this.setElementOffset(existingEl, existingElOffset + amountToShift);
            }
        }
        this.insert(offset, element);
        return this;
    }

    /**
     * Return the first matched index
     */
    index(el) {
        if (!this.isSorted && this.autoSort) {
            this.sort();
        }
        const index = this._elements.indexOf(el);
        if (index === -1) {
            // endElements
            throw new StreamException(
                `cannot find object (${el}) in Stream`
            );
        }
        return index;
    }

    /**
     * Remove and return the last element in the stream,
     * or return undefined if the stream is empty
     *
     * @returns {music21.base.Music21Object|undefined} last element in the stream
     */
    pop() {
        if (!this.isSorted && this.autoSort) {
            this.sort();
        }
        // remove last element;
        if (this.length > 0) {
            const el = this.get(-1);
            this._elements.pop();
            this._offsetDict.delete(el);
            el.sites.remove(this);
            this.coreElementsChanged({ clearIsSorted: false });
            return el;
        } else {
            return undefined;
        }
    }

    /**
     * Remove an object from this Stream.  shiftOffsets and recurse do nothing.
     */
    remove(targetOrList,
        {
            shiftOffsets=false,
            recurse=false,
        } = {}) {
        if (shiftOffsets === true) {
            throw new StreamException('sorry cannot shiftOffsets yet');
        }
        if (recurse === true) {
            throw new StreamException('sorry cannot recurse yet');
        }

        let targetList;
        if (!Array.isArray(targetOrList)) {
            targetList = [targetOrList];
        } else {
            targetList = targetOrList;
        }
        //        if (targetList.length > 1) {
        //            sort targetList
        //        }
        // let shiftDur = 0.0; // for shiftOffsets
        let i = -1;
        for (const target of targetList) {
            i += 1;
            let indexInStream;
            try {
                indexInStream = this.index(target);
            } catch (err) {
                if (err instanceof StreamException) {
                    if (recurse) {
                        // do something
                    }
                    continue;
                }
                throw err;
            }

            // const matchOffset = this._offsetDict[indexInStream];
            // let match;
            // handle _endElements
            // let matchedEndElement = false;
            // let baseElementCount = this._elements.length;
            this._elements.splice(indexInStream, 1);
            this._offsetDict.delete(target);
            target.activeSite = undefined;
            target.sites.remove(this);
            // remove from sites if needed.

            //            if (shiftOffsets) {
            //                const matchDuration = target.duration.quarterLength;
            //                const shiftedRegionStart = matchOffset + matchDuration;
            //                shiftDur += matchDuration;
            //                let shiftedRegionEnd;
            //                if ((i + 1) < targetList.length) {
            //                    const nextElIndex = this.index(targetList[i + 1]);
            //                    const nextElOffset = this._offsetDict[nextElIndex];
            //                    shiftedRegionEnd = nextElOffset;
            //                } else {
            //                    shiftedRegionEnd = this.duration.quarterLength;
            //                }
            //                if (shiftDur !== 0.0) {
            //                    for (const e of this.getElementsByOffset(
            //                       shiftedRegionStart,
            //                       shiftedRegionEnd,
            //                       {
            //                           includeEndBoundary: false,
            //                           mustFinishInSpan: false,
            //                           mustBeginInSpan: false,
            //                       }
            //                    )) {
            //                        const elementOffset = this.elementOffset(e);
            //                        this.setElementOffset(e, elementOffset - shiftDur);
            //                    }
            //                }
            //            }
        }
        this.coreElementsChanged({ clearIsSorted: false });
    }

    /**
     *  Given a `target` object, replace it with
     *  the supplied `replacement` object.
     *
     *  `recurse` and `allDerived` do not currently work.
     *
     *  Does nothing if target cannot be found.
     */
    replace(target, replacement, {
        recurse=false,
        allDerivated=true,
    } = {}) {
        try {
            this.index(target);
        } catch (err) {
            if (err instanceof StreamException) {
                return;
            } else {
                throw err;
            }
        }
        const targetOffset = this.elementOffset(target);
        this.remove(target);
        this.insert(targetOffset, replacement);
        this.coreElementsChanged({ clearIsSorted: false });
    }

    /**
     * Get the `index`th element from the Stream.  Equivalent to the
     * music21p format of s[index] using __getitem__.  Can use negative indexing to get from the end.
     *
     * Once Proxy objects are supported by all operating systems for
     *
     * @param {int} index - can be -1, -2, to index from the end, like python
     * @returns {music21.base.Music21Object|undefined}
     */
    get(index) {
        // substitute for Python stream __getitem__; supports -1 indexing, etc.
        if (!this.isSorted) {
            this.sort();
        }

        let el;
        if (index === undefined || Number.isNaN(index)) {
            return undefined;
        } else if (Math.abs(index) > this._elements.length) {
            return undefined;
        } else if (index === this._elements.length) {
            return undefined;
        } else if (index < 0) {
            el = this._elements[this._elements.length + index];
            el.activeSite = this;
            return el;
        } else {
            el = this._elements[index];
            el.activeSite = this;
            return el;
        }
    }

    /**
     *
     */
    set(index, newEl) {
        const replaceEl = this.get(index);
        if (replaceEl === undefined) {
            throw new StreamException(`Cannot set element at index ${index}.`);
        }
        this.replace(replaceEl, newEl);
        return this;
    }


    setElementOffset(el, value, addElement=false) {
        if (!this._elements.includes(el)) {
            if (addElement) {
                this.insert(value, el);
                return;
            } else {
                throw new StreamException(
                    'Cannot set the offset for elemenet '
                            + el.toString()
                            + ', not in Stream'
                );
            }
        }
        this._offsetDict.set(el, value);
        el.activeSite = this;
    }

    elementOffset(element, stringReturns=false) {
        if (!this._offsetDict.has(element)) {
            throw new StreamException(
                'An entry for this object ' + element.toString() + ' is not stored in this Stream.'
            );
        } else {
            return this._offsetDict.get(element);
        }
    }

    /*  --- ############# END ELEMENT FUNCTIONS ########## --- */

    /**
     * Takes a stream and places all of its elements into
     * measures (:class:`~music21.stream.Measure` objects)
     * based on the :class:`~music21.meter.TimeSignature` objects
     * placed within
     * the stream. If no TimeSignatures are found in the
     * stream, a default of 4/4 is used.

     * If `options.inPlace` is true, the original Stream is modified and lost
     * if `options.inPlace` is False, this returns a modified deep copy.

     * @param {Object} [options]
     * @returns {music21.stream.Stream}
     */
    makeMeasures(options) {
        const params = {
            meterStream: undefined,
            refStreamOrTimeRange: undefined,
            searchContext: false,
            innerBarline: undefined,
            finalBarline: 'final',
            bestClef: false,
            inPlace: false,
        };
        common.merge(params, options);
        let voiceCount;
        if (this.hasVoices()) {
            voiceCount = this.getElementsByClass('Voice').length;
        } else {
            voiceCount = 0;
        }
        // meterStream
        const meterStream = this.getElementsByClass('TimeSignature');
        if (meterStream.length === 0) {
            meterStream.append(this.timeSignature);
        }
        // getContextByClass('Clef')
        const clefObj = this.getSpecialContext('clef') || this.getContextByClass('Clef');
        const offsetMap = this.offsetMap();
        let oMax = 0;
        for (let i = 0; i < offsetMap.length; i++) {
            if (offsetMap[i].endTime > oMax) {
                oMax = offsetMap[i].endTime;
            }
        }
        // console.log('oMax: ', oMax);
        const post = new this.constructor();
        // derivation
        let o = 0.0;
        let measureCount = 0;
        let lastTimeSignature;
        let m;
        let mStart;
        while (measureCount === 0 || o < oMax) {
            m = new Measure();
            m.number = measureCount + 1;
            // var thisTimeSignature = meterStream.getElementAtOrBefore(o);
            const thisTimeSignature = this.timeSignature;
            if (thisTimeSignature === undefined) {
                break;
            }
            const oneMeasureLength
                = thisTimeSignature.barDuration.quarterLength;
            if (oneMeasureLength === 0) {
                // if for some reason we are advancing not at all, then get out!
                break;
            }
            if (measureCount === 0) {
                // simplified...
            }
            m.clef = clefObj;
            m.timeSignature = thisTimeSignature.clone();

            for (let voiceIndex = 0; voiceIndex < voiceCount; voiceIndex++) {
                const v = new Voice();
                v.id = voiceIndex;
                m.insert(0, v);
            }
            post.insert(o, m);
            o += oneMeasureLength;
            measureCount += 1;
            lastTimeSignature = thisTimeSignature;
        }
        for (let i = 0; i < offsetMap.length; i++) {
            const ob = offsetMap[i];
            const e = ob.element;
            const start = ob.offset;
            const voiceIndex = ob.voiceIndex;

            // if 'Spanner' in e.classes;
            lastTimeSignature = undefined;
            for (let j = 0; j < post.length; j++) {
                m = post.get(j); // nothing but measures...
                const foundTS = m.getSpecialContext('timeSignature');
                if (foundTS !== undefined) {
                    lastTimeSignature = foundTS;
                }
                mStart = m.getOffsetBySite(post);
                let mEnd;
                if (lastTimeSignature !== undefined) {
                    mEnd
                        = mStart + lastTimeSignature.barDuration.quarterLength;
                } else {
                    mEnd = mStart + 4.0;
                }
                if (start >= mStart && start < mEnd) {
                    break;
                }
            }
            // if not match, raise Exception;
            const oNew = start - mStart;
            if (m.clef === e) {
                continue;
            }
            if (oNew === 0 && e.isClassOrSubclass('TimeSignature')) {
                continue;
            }
            let insertStream = m;
            if (voiceIndex !== undefined) {
                insertStream = m.getElementsByClass('Voice').get(voiceIndex);
            }
            insertStream.insert(oNew, e);
        }
        // set barlines, etc.
        if (params.inPlace !== true) {
            return post;
        } else {
            this.elements = [];
            // endElements
            // elementsChanged;
            for (const e of post) {
                this.insert(e.offset, e);
            }
            return this; // javascript style;
        }
    }

    containerInHierarchy(el, { setActiveSite=true }={}) {
        const elSites = el.sites;
        for (const s of this.recurse({
            skipSelf: false,
            streamOnly: true,
            restoreActiveSites: false,
        })) {
            if (elSites.includes(s)) {
                if (setActiveSite) {
                    el.activeSite = s;
                }
                return s;
            }
        }
        return undefined;
    }

    /**
     * chordify does not yet work...
     */
    chordify({
        addTies=true,
        addPartIdAsGroup=true,
        removeRedundantPitches=true,
        toSoundingPitch=true,
    }={}) {
        const workObj = this;
        let templateStream;
        if (this.hasPartLikeStreams()) {
            templateStream = workObj.getElementsByClass('Stream').get(0);
        } else {
            templateStream = workObj;
        }
        const template = templateStream.template({
            fillWithRests: false,
            removeClasses: ['GeneralNote'],
            retainVoices: false,
        });
        return template;
    }

    template({
        fillWithRests=true,
        removeClasses=[],
        retainVoices=true,
    }={}) {
        const out = this.cloneEmpty('template');
        const restInfo = {
            offset: undefined,
            endTime: undefined,
        };
        const optionalAddRest = function optionalAddRest() {
            if (!fillWithRests) {
                return;
            }
            if (restInfo.offset === undefined) {
                return;
            }
            const restQL = restInfo.endTime - restInfo.offset;
            const restObj = new note.Rest();
            restObj.duration.quarterLength = restQL;
            out.insert(restInfo.offset, restObj);
            restInfo.offset = undefined;
            restInfo.endTime = undefined;
        };
        for (const el of this) {
            if (el.isStream
                    && (retainVoices || el.classes.includes('Voice'))) {
                optionalAddRest();
                const outEl = el.template({
                    fillWithRests,
                    removeClasses,
                    retainVoices,
                });
                out.insert(el.offset, outEl);
            }
        }
    }

    cloneEmpty(derivationMethod) {
        const returnObj = this.constructor();
        // TODO(msc): derivation
        returnObj.mergeAttributes(this);
        return returnObj;
    }

    /**
     *
     * @param {this} other
     * @returns {this}
     */
    mergeAttributes(other) {
        super.mergeAttributes(other);
        for (const attr of [
            'autoSort',
            'isSorted',
            'definesExplicitSystemBreaks',
            'definesExplicitPageBreaks',
            '_atSoundingPitch',
            '_mutable',
        ]) {
            if (Object.prototype.hasOwnProperty.call(other, attr)) {
                this[attr] = other[attr];
            }
        }
        return this;
    }


    /**
     * makeNotation does not do anything yet, but it is a placeholder
     * so it can start to be called.
     */
    makeNotation({ inPlace=true }={}) {
        let out;
        if (inPlace) {
            out = this;
        } else {
            out = this.clone(true);
        }
        this.makeAccidentals();
        return out;
    }


    /**
     * Return a new Stream or modify this stream
     * to have beams.
     *
     * NOT yet being called March 2018
     */
    makeBeams(options) {
        const params = { inPlace: false };
        common.merge(params, options);
        let returnObj = this;
        if (!params.inPlace) {
            returnObj = this.clone(true);
        }
        let mColl;
        if (this.classes.includes('Measure')) {
            mColl = [returnObj];
        } else {
            mColl = [];
            for (const m of returnObj.getElementsByClass('Measure')) {
                mColl.push(m);
            }
        }
        let lastTimeSignature;
        for (const m of mColl) {
            if (m.timeSignature !== undefined) {
                lastTimeSignature = m.timeSignature;
            }
            if (lastTimeSignature === undefined) {
                throw new StreamException('Need a Time Signature to process beams');
            }
            // todo voices!
            if (m.length <= 1) {
                continue; // nothing to beam.
            }
            const noteStream = m.notesAndRests;
            const durList = [];
            for (const n of noteStream) {
                durList.push(n.duration);
            }
            const durSum = durList.map(a => a.quarterLength).reduce((total, val) => total + val);
            const barQL = lastTimeSignature.barDuration.quarterLength;
            if (durSum > barQL) {
                continue;
            }
            let offset = 0.0;
            if (m.paddingLeft !== 0.0 && m.paddingLeft !== undefined) {
                offset = m.paddingLeft;
            } else if (noteStream.highestTime < barQL) {
                offset = barQL - noteStream.highestTime;
            }
            const beamsList = lastTimeSignature.getBeams(noteStream, { measureStartOffset: offset });
            for (let i = 0; i < noteStream.length; i++) {
                const n = noteStream.get(i);
                const thisBeams = beamsList[i];
                if (thisBeams !== undefined) {
                    n.beams = thisBeams;
                } else {
                    n.beams = new beam.Beams();
                }
            }
        }

        // returnObj.streamStatus.beams = true;
        return returnObj;
    }

    /**
     * Returns a boolean value showing if this
     * Stream contains any Parts or Part-like
     * sub-Streams.
     *
     * Will deal with Part-like sub-streams later
     * for now just checks for real Part objects.
     *
     * Part-like sub-streams are Streams that
     * contain Measures or Notes. And where no
     * sub-stream begins at an offset besides zero.
     */
    hasPartLikeStreams() {
        for (const el of this) {
            if (el.classes.includes('Part')) {
                return true;
            }
        }
        return false;
    }


    /**
     * Returns true if any note in the stream has lyrics, otherwise false
     *
     * @returns {Boolean}
     */
    hasLyrics() {
        for (const el of this) {
            if (el.lyric !== undefined) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns a list of OffsetMap objects
     *
     * @returns [music21.stream.OffsetMap]
     */
    offsetMap() {
        const offsetMap = [];
        let groups = [];
        if (this.hasVoices()) {
            // TODO(msc) -- remove jQuery each...
            $.each(this.getElementsByClass('Voice'), (i, v) => {
                groups.push([v.flat, i]);
            });
        } else {
            groups = [[this, undefined]];
        }
        for (let i = 0; i < groups.length; i++) {
            const group = groups[i][0];
            const voiceIndex = groups[i][1];
            for (let j = 0; j < group.length; j++) {
                const e = group.get(j);
                const dur = e.duration.quarterLength;
                const offset = group.elementOffset(e);
                const endTime = offset + dur;
                const thisOffsetMap = new OffsetMap(
                    e,
                    offset,
                    endTime,
                    voiceIndex
                );
                offsetMap.push(thisOffsetMap);
            }
        }
        return offsetMap;
    }

    get iter() {
        return new iterator.StreamIterator(this);
    }

    /**
     * Find all elements with a certain class; if an Array is given, then any
     * matching class will work.
     *
     * @param {string[]|string} classList - a list of classes to find
     * @returns {music21.stream.Stream}
     */
    getElementsByClass(classList) {
        return this.iter.getElementsByClass(classList);
    }

    /**
     * Find all elements NOT with a certain class; if an Array is given, then any
     * matching class will work.
     *
     * @param {string[]|string} classList - a list of classes to find
     * @returns {music21.stream.Stream}
     */
    getElementsNotOfClass(classList) {
        return this.iter.getElementsNotOfClass(classList);
    }

    //    getElementsByClass(classList) {
    //        const tempEls = [];
    //        for (const thisEl of this) {
    //            // console.warn(thisEl);
    //            if (thisEl.isClassOrSubclass === undefined) {
    //                console.error(
    //                    'what the hell is a ',
    //                    thisEl,
    //                    'doing in a Stream?'
    //                );
    //            } else if (thisEl.isClassOrSubclass(classList)) {
    //                tempEls.push(thisEl);
    //            }
    //        }
    //        const newSt = this.clone(false);
    //        newSt.elements = tempEls;
    //        return newSt;
    //    }

    /**
     * Returns a new stream [StreamIterator does not yet exist in music21j]
     * containing all Music21Objects that are found at a certain offset or
     * within a certain offset time range (given the offsetStart and
     * (optional) offsetEnd values).
     *
     * See music21p documentation for the effect of various parameters.
     */
    getElementsByOffset(
        offsetStart,
        offsetEnd,
        {
            includeEndBoundary=true,
            mustFinishInSpan=false,
            mustBeginInSpan=true,
            includeElementsThatEndAtStart=true,
            classList=undefined,
        }={}
    ) {

        let s;
        if (classList !== undefined) {
            s = this.iter.getElementsByClass(classList);
        } else {
            s = this.iter;
        }
        s.getElementsByOffset(offsetStart, offsetEnd, {
            includeEndBoundary,
            mustFinishInSpan,
            mustBeginInSpan,
            includeElementsThatEndAtStart,
        });
        return s;
    }

    /**
     *  Given an element (from another Stream) returns the single element
     *  in this Stream that is sounding while the given element starts.
     *
     *  If there are multiple elements sounding at the moment it is
     *  attacked, the method returns the first element of the same class
     *  as this element, if any. If no element
     *  is of the same class, then the first element encountered is
     *  returned. For more complex usages, use allPlayingWhileSounding.
     *
     *  Returns None if no elements fit the bill.
     *
     *  The optional elStream is the stream in which el is found.
     *  If provided, el's offset
     *  in that Stream is used.  Otherwise, the current offset in
     *  el is used.  It is just
     *  in case you are paranoid that el.offset might not be what
     *  you want, because of some fancy manipulation of
     *  el.activeSite
     *
     * @param {music21.base.Music21Object} el - object with an offset and class to search for.
     * @param {music21.stream.Stream|undefined} elStream - a place to get el's offset from.
     * @returns {music21.base.Music21Object|undefined}
     */
    playingWhenAttacked(el, elStream) {
        let elOffset;
        if (elStream !== undefined) {
            elOffset = el.getOffsetBySite(elStream);
        } else {
            elOffset = el.offset;
        }

        const otherElements = this.getElementsByOffset(elOffset, elOffset, { mustBeginInSpan: false });
        if (otherElements.length === 0) {
            return undefined;
        } else if (otherElements.length === 1) {
            return otherElements.get(0);
        } else {
            for (const thisEl of otherElements) {
                if (el.constructor === thisEl.constructor) {
                    return thisEl;
                }
            }
            return otherElements.get(0);
        }
    }


    /**
     * Sets Pitch.accidental.displayStatus for every element with a
     * pitch or pitches in the stream. If a natural needs to be displayed
     * and the Pitch does not have an accidental object yet, adds one.
     *
     * Called automatically before appendDOM routines are called.
     *
     * @returns {this}
     */
    makeAccidentals() {
        // cheap version of music21p method
        const extendableStepList = {};
        const stepNames = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
        const ks = this.keySignature || this.getContextByClass('KeySignature');
        for (const stepName of stepNames) {
            let stepAlter = 0;
            if (ks !== undefined) {
                const tempAccidental = ks.accidentalByStep(
                    stepName
                );
                if (tempAccidental !== undefined) {
                    stepAlter = tempAccidental.alter;
                    // console.log(stepAlter + " " + stepName);
                }
            }
            extendableStepList[stepName] = stepAlter;
        }
        const lastOctaveStepList = [];
        for (let i = 0; i < 10; i++) {
            const tempOctaveStepDict = {...extendableStepList};  // clone
            lastOctaveStepList.push(tempOctaveStepDict);
        }
        const lastOctavelessStepDict = {...extendableStepList};  // probably unnecessary, but safe...

        for (const el of this) {
            if (el.pitch !== undefined) {
                // note
                const p = el.pitch;
                const lastStepDict = lastOctaveStepList[p.octave];
                this._makeAccidentalForOnePitch(
                    p,
                    lastStepDict,
                    lastOctavelessStepDict
                );
            } else if (el._notes !== undefined) {
                // chord
                for (const chordNote of el._notes) {
                    const p = chordNote.pitch;
                    const lastStepDict = lastOctaveStepList[p.octave];
                    this._makeAccidentalForOnePitch(
                        p,
                        lastStepDict,
                        lastOctavelessStepDict
                    );
                }
            }
        }
        return this;
    }

    //  returns pitch
    _makeAccidentalForOnePitch(p, lastStepDict, lastOctavelessStepDict) {
        if (lastStepDict === undefined) {
            // octave < 0 or > 10? -- error that appeared sometimes.
            lastStepDict = {};
        }
        let newAlter;
        if (p.accidental === undefined) {
            newAlter = 0;
        } else {
            newAlter = p.accidental.alter;
        }
        // console.log(p.name + " " + lastStepDict[p.step].toString());
        if (
            lastStepDict[p.step] !== newAlter
            || lastOctavelessStepDict[p.step] !== newAlter
        ) {
            if (p.accidental === undefined) {
                p.accidental = new pitch.Accidental('natural');
            }
            p.accidental.displayStatus = true;
            // console.log("setting displayStatus to true");
        } else if (
            lastStepDict[p.step] === newAlter
            && lastOctavelessStepDict[p.step] === newAlter
        ) {
            if (p.accidental !== undefined) {
                p.accidental.displayStatus = false;
            }
            // console.log("setting displayStatus to false");
        }
        lastStepDict[p.step] = newAlter;
        lastOctavelessStepDict[p.step] = newAlter;
        return p;
    }

    /**
     * Sets the render options for any substreams (such as placing them
     * in systems, etc.) DOES NOTHING for music21.stream.Stream, but is
     * overridden in subclasses.
     *
     * @returns {this}
     */
    setSubstreamRenderOptions() {
        /* does nothing for standard streams ... */
        return this;
    }

    /**
     * Resets all the RenderOptions back to defaults. Can run recursively
     * and can also preserve the `RenderOptions.events` object.
     *
     * @param {Boolean} [recursive=false]
     * @param {Boolean} [preserveEvents=false]
     * @returns {this}
     */
    resetRenderOptions(recursive, preserveEvents) {
        const oldEvents = this.renderOptions.events;
        this.renderOptions = new renderOptions.RenderOptions();
        if (preserveEvents) {
            this.renderOptions.events = oldEvents;
        }

        if (recursive) {
            for (const el of this) {
                if (el.isClassOrSubclass('Stream')) {
                    el.resetRenderOptions(recursive, preserveEvents);
                }
            }
        }
        return this;
    }

    //  * *********  VexFlow functionality

    renderVexflowOnCanvas(canvasOrSVG) {
        console.warn(
            'renderVexflowOnCanvas is deprecated; call renderVexflow instead'
        );
        return this.renderVexflow(canvasOrSVG);
    }

    write(format='musicxml') {
        return _exportMusicXMLAsText(this);
    }


    /**
     * Uses {@link music21.vfShow.Renderer} to render Vexflow onto an
     * existing canvas or SVG object.
     *
     * Runs `this.setRenderInteraction` on the canvas.
     *
     * Will be moved to vfShow eventually when converter objects are enabled...maybe.
     *
     * @param {jQuery|HTMLElement} $canvasOrSVG - a canvas or the div surrounding an SVG object
     * @returns {vfShow.Renderer}
     */
    renderVexflow($canvasOrSVG) {
        /**
         * @type {HTMLElement|undefined}
         */
        let canvasOrSVG;
        if ($canvasOrSVG instanceof $) {
            canvasOrSVG = $canvasOrSVG[0];
        } else {
            canvasOrSVG = $canvasOrSVG;
        }
        const DOMContains = document.body.contains(canvasOrSVG);
        if (!DOMContains) {
            // temporarily add to DOM so Firefox can measure it...
            document.body.appendChild(canvasOrSVG);
        }
        const tagName = canvasOrSVG.tagName.toLowerCase();

        if (this.autoBeam === true) {
            try {
                this.makeBeams({ inPlace: true });
            } catch (e) {
                if (!e.toString().includes('Time Signature')) {
                    throw e;
                }
            }
        }
        const vfr = new vfShow.Renderer(this, canvasOrSVG);
        if (tagName === 'canvas') {
            vfr.rendererType = 'canvas';
        } else if (tagName === 'svg') {
            vfr.rendererType = 'svg';
        }
        vfr.render();
        this.setRenderInteraction(canvasOrSVG);
        this.activeVFRenderer = vfr;
        if (!DOMContains) {
            // remove the adding to DOM so that Firefox could measure it...
            document.body.removeChild(canvasOrSVG);
        }

        return vfr;
    }

    /**
     * Estimate the stream height for the Stream.
     *
     * If there are systems they will be incorporated into the height unless `ignoreSystems` is `true`.
     *
     * @param {Boolean} [ignoreSystems=false]
     * @returns {number} height in pixels
     */
    estimateStreamHeight(ignoreSystems) {
        const staffHeight = this.renderOptions.naiveHeight;
        let systemPadding = this.systemPadding;
        if (systemPadding === undefined) {
            systemPadding = 0;
        }
        let numSystems;
        if (this.isClassOrSubclass('Score')) {
            const numParts = this.parts.length;
            numSystems = this.numSystems();
            if (numSystems === undefined || ignoreSystems) {
                numSystems = 1;
            }
            let scoreHeight
                = numSystems * staffHeight * numParts
                + (numSystems - 1) * systemPadding;
            if (numSystems > 1) {
                // needs a little extra padding for some reason...
                scoreHeight += systemPadding / 2;
            }

            // console.log('scoreHeight of ' + scoreHeight);
            return scoreHeight;
        } else if (this.isClassOrSubclass('Part')) {
            numSystems = 1;
            if (!ignoreSystems) {
                numSystems = this.numSystems();
            }
            if (debug) {
                console.log(
                    'estimateStreamHeight for Part: numSystems ['
                        + numSystems
                        + '] * staffHeight ['
                        + staffHeight
                        + '] + (numSystems ['
                        + numSystems
                        + '] - 1) * systemPadding ['
                        + systemPadding
                        + '].'
                );
            }
            return numSystems * staffHeight + (numSystems - 1) * systemPadding;
        } else {
            return staffHeight;
        }
    }

    /**
     * Estimates the length of the Stream in pixels.
     *
     * @returns {number} length in pixels
     */
    estimateStaffLength() {
        let i;
        let totalLength;
        if (this.renderOptions.overriddenWidth !== undefined) {
            // console.log("Overridden staff width: " + this.renderOptions.overriddenWidth);
            return this.renderOptions.overriddenWidth;
        }
        if (this.hasVoices()) {
            let maxLength = 0;
            for (const v of this) {
                if (v.isClassOrSubclass('Stream')) {
                    const thisLength
                        = v.estimateStaffLength() + v.renderOptions.staffPadding;
                    if (thisLength > maxLength) {
                        maxLength = thisLength;
                    }
                }
            }
            return maxLength;
        } else if (!this.isFlat) {
            // part
            totalLength = 0;
            for (i = 0; i < this.length; i++) {
                const m = this.get(i);
                if (m.isClassOrSubclass('Stream')) {
                    totalLength
                        += m.estimateStaffLength() + m.renderOptions.staffPadding;
                    if (i !== 0 && m.renderOptions.startNewSystem === true) {
                        break;
                    }
                }
            }
            return totalLength;
        } else {
            const rendOp = this.renderOptions;
            totalLength = 30 * this.notesAndRests.length;
            totalLength += rendOp.displayClef ? 30 : 0;
            totalLength
                += rendOp.displayKeySignature && this.getSpecialContext('keySignature')
                    ? this.getSpecialContext('keySignature').width
                    : 0;
            totalLength += rendOp.displayTimeSignature ? 30 : 0;
            // totalLength += rendOp.staffPadding;
            return totalLength;
        }
    }

    //  * ***** MIDI related routines...

    /**
     * Plays the Stream through the MIDI/sound playback (for now, only MIDI.js is supported)
     *
     * `options` can be an object containing:
     * - instrument: {@link music21.instrument.Instrument} object (default, `this.instrument`)
     * - tempo: number (default, `this.tempo`)
     *
     * @param {Object} [options] - object of playback options
     * @returns {this}
     */
    playStream(options) {
        const params = {
            instrument: this.instrument,
            tempo: this.tempo,
            done: undefined,
            startNote: undefined,
        };
        common.merge(params, options);
        const startNoteIndex = params.startNote;
        let currentNoteIndex = 0;
        if (startNoteIndex !== undefined) {
            currentNoteIndex = startNoteIndex;
        }
        const flatEls = this.flat.elements;
        const lastNoteIndex = flatEls.length - 1;
        this._stopPlaying = false;
        const thisStream = this;

        const playNext = function playNext(elements, params) {
            if (currentNoteIndex <= lastNoteIndex && !thisStream._stopPlaying) {
                const el = elements[currentNoteIndex];
                let nextNote;
                let playDuration;
                if (currentNoteIndex < lastNoteIndex) {
                    nextNote = elements[currentNoteIndex + 1];
                    playDuration = nextNote.offset - el.offset;
                } else {
                    playDuration = el.duration.quarterLength;
                }
                const milliseconds = playDuration * 1000 * 60 / params.tempo;
                if (debug) {
                    console.log(
                        'playing: ',
                        el,
                        playDuration,
                        milliseconds,
                        params.tempo
                    );
                }

                if (el.playMidi !== undefined) {
                    el.playMidi(params.tempo, nextNote, params);
                }
                currentNoteIndex += 1;
                setTimeout(() => {
                    playNext(elements, params);
                }, milliseconds);
            } else if (params && params.done) {
                params.done.call();
            }
        };
        playNext(flatEls, params);
        return this;
    }

    /**
     * Stops a stream from playing if it currently is.
     *
     * @returns {this}
     */
    stopPlayStream() {
        // turns off all currently playing MIDI notes (on any stream) and stops playback.
        this._stopPlaying = true;
        for (let i = 0; i < 127; i++) {
            MIDI.noteOff(0, i, 0);
        }
        return this;
    }
    /* ----------------------------------------------------------------------
     *
     *  SVG/Canvas DOM routines -- to be factored out eventually.
     *
     */

    createNewCanvas(width, height, elementType='svg') {
        console.warn('createNewCanvas is deprecated, use createNewDOM instead');
        return this.createNewDOM(width, height, elementType);
    }

    /**
     * Creates and returns a new `&lt;canvas&gt;` or `&lt;svg&gt;` object.
     *
     * Calls setSubstreamRenderOptions() first.
     *
     * Does not render on the DOM element.
     *
     * @param {number|string|undefined} width - will use `this.estimateStaffLength()` + `this.renderOptions.staffPadding` if not given
     * @param {number|string|undefined} height - if undefined will use `this.renderOptions.height`. If still undefined, will use `this.estimateStreamHeight()`
     * @param {string} elementType - what type of element, default = svg
     * @returns {jQuery} svg in jquery.
     */
    createNewDOM(width, height, elementType='svg') {
        if (!this.isFlat) {
            this.setSubstreamRenderOptions();
        }

        // we render SVG on a Div for Vexflow
        let renderElementType = 'div';
        if (elementType === 'canvas') {
            renderElementType = 'canvas';
        }

        const $newCanvasOrDIV = $('<' + renderElementType + '/>');
        $newCanvasOrDIV.addClass('streamHolding'); // .css('border', '1px red solid');
        $newCanvasOrDIV.css('display', 'inline-block');

        if (width !== undefined) {
            if (typeof width === 'string') {
                width = common.stripPx(width);
            }
            $newCanvasOrDIV.attr('width', width);
        } else {
            const computedWidth
                = this.estimateStaffLength()
                + this.renderOptions.staffPadding;
            $newCanvasOrDIV.attr('width', computedWidth);
        }
        if (height !== undefined) {
            $newCanvasOrDIV.attr('height', height);
        } else {
            let computedHeight;
            if (this.renderOptions.height === undefined) {
                computedHeight = this.estimateStreamHeight();
                // console.log('computed Height estim: ' + computedHeight);
            } else {
                computedHeight = this.renderOptions.height;
                // console.log('computed Height: ' + computedHeight);
            }
            $newCanvasOrDIV.attr(
                'height',
                computedHeight * this.renderOptions.scaleFactor.y
            );
        }
        return $newCanvasOrDIV;
    }

    createPlayableCanvas(width, height, elementType = 'svg') {
        console.warn(
            'createPlayableCanvas is deprecated, use createPlayableDOM instead'
        );
        return this.createPlayableDOM(width, height, elementType);
    }

    /**
     * Creates a rendered, playable svg where clicking plays it.
     *
     * Called from appendNewDOM() etc.
     *
     * @param {number|string|undefined} [width]
     * @param {number|string|undefined} [height]
     * @param {string} [elementType='svg'] - what type of element, default = svg
     * @returns {jQuery} canvas or svg
     */
    createPlayableDOM(width, height, elementType='svg') {
        this.renderOptions.events.click = 'play';
        return this.createDOM(width, height, elementType);
    }

    createCanvas(width, height, elementType='svg') {
        console.warn('createCanvas is deprecated, use createDOM');
        return this.createDOM(width, height, elementType);
    }

    /**
     * Creates a new svg and renders vexflow on it
     *
     * @param {number|string|undefined} [width]
     * @param {number|string|undefined} [height]
     * @param {string} elementType - what type of element svg or canvas, default = svg
     * @returns {jQuery} canvas or SVG
     */
    createDOM(width, height, elementType='svg') {
        const $newSvg = this.createNewDOM(width, height, elementType);
        // temporarily append the SVG to the document to fix a Firefox bug
        // where nothing can be measured unless is it in the document.
        this.renderVexflow($newSvg);
        return $newSvg;
    }

    appendNewCanvas(appendElement, width, height, elementType='svg') {
        console.warn('appendNewCanvas is deprecated, use appendNewDOM instead');
        return this.appendNewDOM(appendElement, width, height, elementType);
    }

    /**
     * Creates a new canvas, renders vexflow on it, and appends it to the DOM.
     *
     * @param {jQuery|HTMLElement} [appendElement=document.body] - where to place the svg
     * @param {number|string} [width]
     * @param {number|string} [height]
     * @param {string} elementType - what type of element, default = svg
     * @returns {SVGElement|HTMLElement} svg (not the jQuery object --
     * this is a difference with other routines and should be fixed. TODO: FIX)
     *
     */
    appendNewDOM(appendElement, width, height, elementType='svg') {
        if (appendElement === undefined) {
            appendElement = document.body;
        }
        let $appendElement = appendElement;
        if (!(appendElement instanceof $)) {
            $appendElement = $(appendElement);
        }

        //      if (width === undefined && this.renderOptions.maxSystemWidth === undefined) {
        //      var $bodyElement = bodyElement;
        //      if (!(bodyElement instanceof $) {
        //      $bodyElement = $(bodyElement);
        //      }
        //      width = $bodyElement.width();
        //      };

        const svgOrCanvasBlock = this.createDOM(width, height, elementType);
        $appendElement.append(svgOrCanvasBlock);
        return svgOrCanvasBlock[0];
    }

    replaceCanvas(where, preserveSvgSize, elementType = 'svg') {
        console.warn('replaceCanvas is deprecated, use replaceDOM instead');
        return this.replaceDOM(where, preserveSvgSize, elementType);
    }

    /**
     * Replaces a particular Svg with a new rendering of one.
     *
     * Note that if 'where' is empty, will replace all svg elements on the page.
     *
     * @param {jQuery|HTMLElement} [where] - the canvas or SVG to replace or a container holding the canvas(es) to replace.
     * @param {Boolean} [preserveSvgSize=false]
     * @param {string} elementType - what type of element, default = svg
     * @returns {jQuery} the svg
     */
    replaceDOM(where, preserveSvgSize, elementType='svg') {
        // if called with no where, replaces all the svgs on the page...
        if (where === undefined) {
            where = document.body;
        }
        let $where;
        if (!(where instanceof $)) {
            $where = $(where);
        } else {
            $where = where;
            // where = $where[0];
        }
        let $oldSVGOrCanvas;

        if ($where.hasClass('streamHolding')) {
            $oldSVGOrCanvas = $where;
        } else {
            $oldSVGOrCanvas = $where.find('.streamHolding');
        }
        // TODO: Max Width!
        if ($oldSVGOrCanvas.length === 0) {
            throw new Music21Exception('No svg defined for replaceDOM!');
        } else if ($oldSVGOrCanvas.length > 1) {
            // change last svg...
            // replacing each with svgBlock doesn't work
            // anyhow, it just resizes the svg but doesn't
            // draw.
            $oldSVGOrCanvas = $($oldSVGOrCanvas[$oldSVGOrCanvas.length - 1]);
        }

        let svgBlock;
        if (preserveSvgSize) {
            const width = $oldSVGOrCanvas.width() || parseInt($oldSVGOrCanvas.attr('width'));
            const height = $oldSVGOrCanvas.attr('height'); // height manipulates
            svgBlock = this.createDOM(width, height, elementType);
        } else {
            svgBlock = this.createDOM(undefined, undefined, elementType);
        }

        $oldSVGOrCanvas.replaceWith(svgBlock);
        return svgBlock;
    }


    /**
     * Set the type of interaction on the svg based on
     *    - Stream.renderOptions.events.click
     *    - Stream.renderOptions.events.dblclick
     *    - Stream.renderOptions.events.resize
     *
     * Currently the only options available for each are:
     *    - 'play' (string)
     *    - 'reflow' (string; only on event.resize)
     *    - customFunction (will receive event as a first variable; should set up a way to
     *                    find the original stream; var s = this; var f = function () { s...}
     *                   )
     *
     * @param {jQuery|HTMLElement} canvasOrDiv - canvas or the Div surrounding it.
     * @returns {this}
     */
    setRenderInteraction(canvasOrDiv) {
        let $svg = canvasOrDiv;
        if (canvasOrDiv === undefined) {
            return this;
        } else if (!(canvasOrDiv instanceof $)) {
            $svg = $(canvasOrDiv);
        }
        const playFunc = () => {
            this.playStream();
        };

        for (const [eventType, eventFunction] of Object.entries(this.renderOptions.events)) {
            $svg.off(eventType);
            if (
                typeof eventFunction === 'string'
                && eventFunction === 'play'
            ) {
                $svg.on(eventType, playFunc);
            } else if (
                typeof eventFunction === 'string'
                && eventType === 'resize'
                && eventFunction === 'reflow'
            ) {
                this.windowReflowStart($svg);
            } else if (eventFunction !== undefined) {
                $svg.on(eventType, eventFunction);
            }
        }
        return this;
    }

    /**
     *
     * Recursively search downward for the closest storedVexflowStave...
     *
     * @returns {Vex.Flow.Stave|undefined}
     */
    recursiveGetStoredVexflowStave() {
        const storedVFStave = this.storedVexflowStave;
        if (storedVFStave === undefined) {
            if (this.isFlat) {
                return undefined;
            } else {
                const subStreams = this.getElementsByClass('Stream');
                const first_subStream = subStreams.get(0);
                return first_subStream.recursiveGetStoredVexflowStave();
            }
        }
        return storedVFStave;
    }

    getUnscaledXYforCanvas(svg, e) {
        console.warn(
            'getUnscaledXYforCanvas is deprecated, use getUnscaledXYforDOM instead'
        );
        return this.getUnscaledXYforDOM(svg, e);
    }

    /**
     * Given a mouse click, or other event with .pageX and .pageY,
     * find the x and y for the svg.
     *
     * @param {HTMLElement|SVGElement} svg - a canvas or SVG object
     * @param {MouseEvent|TouchEvent} e
     * @returns {Array<number>} two-elements, [x, y] in pixels.
     */
    getUnscaledXYforDOM(svg, e) {
        let offset = null;
        if (svg === undefined) {
            offset = { left: 0, top: 0 };
        } else {
            offset = $(svg).offset();
        }
        /*
         * mouse event handler code from: http://diveintohtml5.org/canvas.html
         */
        let xClick;
        let yClick;
        if (e.pageX !== undefined && e.pageY !== undefined) {
            xClick = e.pageX;
            yClick = e.pageY;
        } else {
            xClick
                = e.clientX
                + document.body.scrollLeft
                + document.documentElement.scrollLeft;
            yClick
                = e.clientY
                + document.body.scrollTop
                + document.documentElement.scrollTop;
        }
        const xPx = xClick - offset.left;
        const yPx = yClick - offset.top;
        return [xPx, yPx];
    }

    getScaledXYforCanvas(svg, e) {
        console.warn(
            'getScaledXYforCanvas is deprecated, use getScaledXYforDOM instead'
        );
        return this.getScaledXYforDOM(svg, e);
    }

    /**
     * return a list of [scaledX, scaledY] for
     * a svg element.
     *
     * xScaled refers to 1/scaleFactor.x -- for instance, scaleFactor.x = 0.7 (default)
     * x of 1 gives 1.42857...
     *
     * @param {Node|SVGElement} svg -- a canvas or SVG object
     * @param {Event} e
     * @returns {Array<number>} [scaledX, scaledY]
     */
    getScaledXYforDOM(svg, e) {
        const [xPx, yPx] = this.getUnscaledXYforDOM(svg, e);
        const pixelScaling = this.renderOptions.scaleFactor;

        const yPxScaled = yPx / pixelScaling.y;
        const xPxScaled = xPx / pixelScaling.x;
        return [xPxScaled, yPxScaled];
    }

    /**
     *
     * Given a Y position find the diatonicNoteNum that a note at that position would have.
     *
     * searches this.storedVexflowStave
     *
     * Y position must be offset from the start of the stave...
     *
     * @param {number} yPxScaled
     * @returns {number}
     */
    diatonicNoteNumFromScaledY(yPxScaled) {
        const storedVFStave = this.recursiveGetStoredVexflowStave();
        if (storedVFStave === undefined) {
            throw new StreamException('Could not find vexflowStave for getting size');
        }

        // for (var i = -10; i < 10; i++) {
        //    console.log("line: " + i + " y: " + storedVFStave.getYForLine(i));
        // }
        const thisClef = this.clef || this.getContextByClass('Clef');
        const lowestLine = (thisClef !== undefined) ? thisClef.lowestLine : 31;

        const lineSpacing = storedVFStave.options.spacing_between_lines_px;
        const linesAboveStaff = storedVFStave.options.space_above_staff_ln;

        const notesFromTop = yPxScaled * 2 / lineSpacing;
        const notesAboveLowestLine
            = (storedVFStave.options.num_lines - 1 + linesAboveStaff) * 2
            - notesFromTop;
        const clickedDiatonicNoteNum = lowestLine + Math.round(notesAboveLowestLine);
        return clickedDiatonicNoteNum;
    }

    /**
     * Returns the stream that is at X location xPxScaled and system systemIndex.
     *
     * Override in subclasses, always returns this; here.
     *
     * @param {number} [xPxScaled]
     * @param {number} [systemIndex]
     * @returns {this}
     *
     */
    getStreamFromScaledXandSystemIndex(xPxScaled, systemIndex) {
        return this;
    }

    /**
     *
     * Return the note (or chord or rest) at pixel X (or within allowablePixels [default 10])
     * of the note.
     *
     * systemIndex element is not used on bare Stream
     *
     * options can be a dictionary of: 'allowBackup' which gets the closest
     * note within the window even if it's beyond allowablePixels (default: true)
     * and 'backupMaximum' which specifies a maximum distance even for backup
     * (default: 70);
     *
     * @param {number} xPxScaled
     * @param {number} [allowablePixels=10]
     * @param {number} [systemIndex]
     * @param {Object} [options]
     * @returns {music21.base.Music21Object|undefined}
     */
    noteElementFromScaledX(xPxScaled, allowablePixels, systemIndex, options) {
        const params = {
            allowBackup: true,
            backupMaximum: 70,
        };
        common.merge(params, options);
        let foundNote;
        if (allowablePixels === undefined) {
            allowablePixels = 10;
        }
        const subStream = this.getStreamFromScaledXandSystemIndex(
            xPxScaled,
            systemIndex
        );
        if (subStream === undefined) {
            return undefined;
        }
        const backup = {
            minDistanceSoFar: params.backupMaximum,
            note: undefined,
        }; // a backup in case we did not find within allowablePixels

        for (const n of subStream.flat.notesAndRests) {
            /* should also
             * compensate for accidentals...
             */
            const leftDistance = Math.abs(n.x - xPxScaled);
            const rightDistance = Math.abs(n.x + n.width - xPxScaled);
            const minDistance = Math.min(leftDistance, rightDistance);

            if (
                leftDistance < allowablePixels
                && rightDistance < allowablePixels
            ) {
                foundNote = n;
                break; /* O(n); can be made O(log n) */
            } else if (
                leftDistance < params.backupMaximum
                && rightDistance < params.backupMaximum
                && minDistance < backup.minDistanceSoFar
            ) {
                backup.note = n;
                backup.minDistanceSoFar = minDistance;
            }
        }
        // console.log('note here is: ', foundNote);
        if (params.allowBackup && foundNote === undefined) {
            foundNote = backup.note;
            // console.log('used backup: closest was: ', backup.minDistanceSoFar);
        }
        // console.log(foundNote);
        return foundNote;
    }

    /**
     * Given an event object, and an x and y location, returns a two-element array
     * of the pitch.Pitch.diatonicNoteNum that was clicked (i.e., if C4 was clicked this
     * will return 29; if D4 was clicked this will return 30) and the closest note in the
     * stream that was clicked.
     *
     * Return a list of [diatonicNoteNum, closestXNote]
     * for an event (e) called on the svg (svg)
     *
     * @param {HTMLElement|SVGElement} svg
     * @param {MouseEvent|TouchEvent} e
     * @param {number} [x]
     * @param {number} [y]
     * @returns {Array} [diatonicNoteNum, closestXNote]
     */
    findNoteForClick(svg, e, x, y) {
        if (x === undefined || y === undefined) {
            [x, y] = this.getScaledXYforDOM(svg, e);
        }
        const clickedDiatonicNoteNum = this.diatonicNoteNumFromScaledY(y);
        const foundNote = this.noteElementFromScaledX(x);
        return [clickedDiatonicNoteNum, foundNote];
    }

    /**
     * Change the pitch of a note given that it has been clicked and then
     * call changedCallbackFunction
     *
     * To be removed...
     *
     * @param {number} clickedDiatonicNoteNum
     * @param {music21.note.Note} foundNote
     * @param {Node} svg
     * @returns {*} output of changedCallbackFunction
     */
    noteChanged(clickedDiatonicNoteNum, foundNote, svg) {
        const n = foundNote;
        const p = new pitch.Pitch('C');
        p.diatonicNoteNum = clickedDiatonicNoteNum;
        p.accidental = n.pitch.accidental;
        n.pitch = p;
        n.stemDirection = undefined;
        this.activeNote = n;
        const $newSvg = this.redrawDOM(svg);
        const params = { foundNote: n, svg: $newSvg };
        if (this.changedCallbackFunction !== undefined) {
            return this.changedCallbackFunction(params);
        } else {
            return params;
        }
    }

    redrawCanvas(svg) {
        console.warn('redrawCanvas is deprecated, use redrawDOM instead');
        return this.redrawDOM(svg);
    }

    /**
     * Redraws an svgDiv, keeping the events of the previous svg.
     *
     * @param {jQuery|Node} svg
     * @returns {jQuery}
     */
    redrawDOM(svg) {
        // this.resetRenderOptions(true, true); // recursive, preserveEvents
        if (!this.isFlat) {
            this.setSubstreamRenderOptions();
        }
        const $svg = $(svg); // works even if svg is already $jquery
        const $newSvg = this.createNewDOM(svg.width, svg.height);
        this.renderVexflow($newSvg);
        $svg.replaceWith($newSvg);
        return $newSvg;
    }

    editableAccidentalCanvas(width, height) {
        console.warn(
            'editableAccidentalCanvas is deprecated, use editableAccidentalDOM instead'
        );
        return this.editableAccidentalDOM(width, height);
    }

    /**
     * Renders a stream on svg with the ability to edit it and
     * a toolbar that allows the accidentals to be edited.
     *
     * @param {number} [width]
     * @param {number} [height]
     * @returns {Node} the div tag around the svg.
     */
    editableAccidentalDOM(width, height) {
        /*
         * Create an editable svg with an accidental selection bar.
         */
        const d = $('<div/>')
            .css('text-align', 'left')
            .css('position', 'relative');

        this.renderOptions.events.click = this.DOMChangerFunction;
        const $svgDiv = this.createDOM(width, height);
        const buttonDiv = this.getAccidentalToolbar(
            undefined,
            undefined,
            $svgDiv
        );
        d.append(buttonDiv);
        d.append($("<br style='clear: both;' />"));
        d.append($svgDiv);
        return d;
    }

    /*
     * SVG toolbars...
     */

    /**
     *
     * @param {int} minAccidental - alter of the min accidental (default -1)
     * @param {int} maxAccidental - alter of the max accidental (default 1)
     * @param {jQuery} $siblingSvg - svg to use for redrawing;
     * @returns {jQuery} the accidental toolbar.
     */
    getAccidentalToolbar(minAccidental, maxAccidental, $siblingSvg) {
        if (minAccidental === undefined) {
            minAccidental = -1;
        }
        if (maxAccidental === undefined) {
            maxAccidental = 1;
        }
        minAccidental = Math.round(minAccidental);
        maxAccidental = Math.round(maxAccidental);

        const addAccidental = (newAlter, clickEvent) => {
            /*
             * To be called on a button...
             */
            let $useSvg = $siblingSvg;
            if ($useSvg === undefined) {
                let $searchParent = $(clickEvent.target).parent();
                let maxSearch = 99;
                while (
                    maxSearch > 0
                    && $searchParent !== undefined
                    && ($useSvg === undefined || $useSvg[0] === undefined)
                ) {
                    maxSearch -= 1;
                    $useSvg = $searchParent.find('.streamHolding');
                    $searchParent = $searchParent.parent();
                }
                if ($useSvg[0] === undefined) {
                    console.log('Could not find a svg...');
                    return;
                }
            }
            if (this.activeNote !== undefined) {
                const n = this.activeNote;
                n.pitch.accidental = new pitch.Accidental(newAlter);
                /* console.log(n.pitch.name); */
                const $newSvg = this.redrawDOM($useSvg[0]);
                if (this.changedCallbackFunction !== undefined) {
                    this.changedCallbackFunction({
                        foundNote: n,
                        svg: $newSvg,
                    });
                }
            }
        };

        const $buttonDiv = $('<div/>').attr(
            'class',
            'accidentalToolbar scoreToolbar'
        );
        for (let i = minAccidental; i <= maxAccidental; i++) {
            const acc = new pitch.Accidental(i);
            const $button = $(
                '<button>' + acc.unicodeModifier + '</button>'
            ).click(e => addAccidental(i, e));
            if (Math.abs(i) > 1) {
                $button.css('font-family', 'Bravura Text');
                $button.css('font-size', '20px');
            }
            $buttonDiv.append($button);
        }
        return $buttonDiv;
    }

    /**
     *
     * @returns {jQuery} a Div containing two buttons -- play and stop
     */
    getPlayToolbar() {
        const $buttonDiv = $('<div/>').attr(
            'class',
            'playToolbar scoreToolbar'
        );
        const $bPlay = $('<button>&#9658</button>');
        $bPlay.click(() => {
            this.playStream();
        });
        $buttonDiv.append($bPlay);
        const $bStop = $('<button>&#9724</button>');
        $bStop.click(() => {
            this.stopPlayStream();
        });
        $buttonDiv.append($bStop);
        return $buttonDiv;
    }
    //  reflow

    /**
     * Begins a series of bound events to the window that makes it
     * so that on resizing the stream is redrawn and reflowed to the
     * new size.
     *
     * @param {jQuery} jSvg
     * @returns {this}
     */
    windowReflowStart(jSvg) {
        // set up a bunch of windowReflow bindings that affect the svg.
        const callingStream = this;
        let jSvgNow = jSvg;
        $(window).bind('resizeEnd', () => {
            // do something, window hasn't changed size in 500ms
            const jSvgParent = jSvgNow.parent();
            const newWidth = jSvgParent.width();
            const svgWidth = newWidth;
            // console.log(svgWidth);
            console.log('resizeEnd triggered', newWidth);
            // console.log(callingStream.renderOptions.events.click);
            callingStream.resetRenderOptions(true, true); // recursive, preserveEvents
            // console.log(callingStream.renderOptions.events.click);
            callingStream.maxSystemWidth = svgWidth - 40;
            jSvgNow.remove();
            const svgObj = callingStream.appendNewDOM(jSvgParent);
            jSvgNow = $(svgObj);
        });
        $(window).resize(function resizeSvgTo() {
            if (this.resizeTO) {
                clearTimeout(this.resizeTO);
            }
            this.resizeTO = setTimeout(function resizeToTimeout() {
                $(this).trigger('resizeEnd');
            }, 200);
        });
        setTimeout(function triggerResizeOnCreateSvg() {
            const $window = $(window);
            const doResize = $window.data('triggerResizeOnCreateSvg');
            if (doResize === undefined || doResize === true) {
                $(this).trigger('resizeEnd');
                $window.data('triggerResizeOnCreateSvg', false);
            }
        }, 1000);
        return this;
    }

    /**
     * Does this stream have a {@link music21.stream.Voice} inside it?
     *
     * @returns {Boolean}
     */
    hasVoices() {
        for (const el of this) {
            if (el.isClassOrSubclass('Voice')) {
                return true;
            }
        }
        return false;
    }
}

/**
 *
 * @class Voice
 * @memberof music21.stream
 * @extends music21.stream.Stream
 */
export class Voice extends Stream {
    constructor() {
        super();
        this.recursionType = 'elementsFirst';
    }
}

/**
 * @class Measure
 * @memberof music21.stream
 * @extends music21.stream.Stream
 */
export class Measure extends Stream {
    constructor() {
        super();
        this.recursionType = 'elementsFirst';
        this.isMeasure = true;
        this.number = 0; // measure number
        this.numberSuffix = '';
    }

    stringInfo() {
        return this.measureNumberWithSuffix() + ' offset=' + this.offset.toString();
    }

    measureNumberWithSuffix() {
        return this.number.toString() + this.numberSuffix;
    }
}

/**
 * Part -- specialized to handle Measures inside it
 *
 * @class Part
 * @memberof music21.stream
 * @extends music21.stream.Stream
 */
export class Part extends Stream {
    constructor() {
        super();
        this.recursionType = 'flatten';
        this.systemHeight = this.renderOptions.naiveHeight;
    }

    /**
     * How many systems does this Part have?
     *
     * Does not change any reflow information, so by default it's always 1.
     *
     * @returns {Number}
     */
    numSystems() {
        let numSystems = 1;
        const subStreams = this.getElementsByClass('Stream');
        for (let i = 1; i < subStreams.length; i++) {
            if (subStreams.get(i).renderOptions.startNewSystem) {
                numSystems += 1;
            }
        }
        return numSystems;
    }

    /**
     * Find the width of every measure in the Part.
     *
     * @returns {Array<number>}
     */
    getMeasureWidths() {
        /* call after setSubstreamRenderOptions */
        const measureWidths = [];
        for (const el of this) {
            if (el.isClassOrSubclass('Measure')) {
                const elRendOp = el.renderOptions;
                measureWidths[elRendOp.measureIndex] = elRendOp.width;
            }
        }
        /* console.log(measureWidths);
         *
         */
        return measureWidths;
    }

    /**
     * Overrides the default music21.stream.Stream#estimateStaffLength
     *
     * @returns {number}
     */
    estimateStaffLength() {
        if (this.renderOptions.overriddenWidth !== undefined) {
            // console.log("Overridden staff width: " + this.renderOptions.overriddenWidth);
            return this.renderOptions.overriddenWidth;
        }
        if (!this.isFlat) {
            // part with Measures underneath
            let totalLength = 0;
            let isFirst = true;
            for (const m of this.getElementsByClass('Measure')) {
                // this looks wrong, but actually seems to be right. moving it to
                // after the break breaks things.
                totalLength
                    += m.estimateStaffLength() + m.renderOptions.staffPadding;
                if (!isFirst && m.renderOptions.startNewSystem === true) {
                    break;
                }
                isFirst = false;
            }
            return totalLength;
        }
        // no measures found in part... treat as measure
        const tempM = new Measure();
        tempM.elements = this;
        return tempM.estimateStaffLength();
    }

    /**
     * Divide a part up into systems and fix the measure
     * widths so that they are all even.
     *
     * Note that this is done on the part level even though
     * the measure widths need to be consistent across parts.
     *
     * This is possible because the system is deterministic and
     * will come to the same result for each part.  Opportunity
     * for making more efficient through this...
     *
     * @param {number} systemHeight
     * @returns {Array}
     */
    fixSystemInformation(systemHeight) {
        /*
         * console.log('system height: ' + systemHeight);
         */
        if (systemHeight === undefined) {
            systemHeight = this.systemHeight; /* part.show() called... */
        } else if (debug) {
            console.log('overridden systemHeight: ' + systemHeight);
        }
        const systemPadding
            = this.renderOptions.systemPadding
            || this.renderOptions.naiveSystemPadding;
        const measureWidths = this.getMeasureWidths();
        const maxSystemWidth = this.maxSystemWidth; /* of course fix! */
        const systemCurrentWidths = [];
        const systemBreakIndexes = [];
        let lastSystemBreak = 0; /* needed to ensure each line has at least one measure */
        const startLeft = 20; /* TODO: make it obtained elsewhere */
        let currentLeft = startLeft;
        let i;
        for (i = 0; i < measureWidths.length; i++) {
            const currentRight = currentLeft + measureWidths[i];
            /* console.log("left: " + currentLeft + " ; right: " + currentRight + " ; m: " + i); */
            if (currentRight > maxSystemWidth && lastSystemBreak !== i) {
                systemBreakIndexes.push(i - 1);
                systemCurrentWidths.push(currentLeft);

                // console.log('setting new width at ' + currentLeft);
                currentLeft = startLeft + measureWidths[i]; // 20 + this width;
                lastSystemBreak = i;
            } else {
                currentLeft = currentRight;
            }
        }
        // console.log(systemCurrentWidths);
        // console.log(systemBreakIndexes);

        let currentSystemIndex = 0;
        let leftSubtract = 0;
        let newLeftSubtract;
        for (i = 0; i < this.length; i++) {
            const m = this.get(i);
            if (m.renderOptions === undefined) {
                continue;
            }
            if (i === 0) {
                m.renderOptions.startNewSystem = true;
            }
            currentLeft = m.renderOptions.left;

            if (systemBreakIndexes.indexOf(i - 1) !== -1) {
                /* first measure of new System */
                const oldWidth = m.renderOptions.width;
                const oldEstimate = m.estimateStaffLength() + m.renderOptions.staffPadding;
                const offsetFromEstimate = oldWidth - oldEstimate;
                // we look at the offset from the current estimate to see how much
                // the staff length may have been adjusted to compensate for other
                // parts with different lengths.

                // but setting these options is bound to change something
                m.renderOptions.displayClef = true;
                m.renderOptions.displayKeySignature = true;
                m.renderOptions.startNewSystem = true;

                // so we get a new estimate.
                const newEstimate = m.estimateStaffLength() + m.renderOptions.staffPadding;

                // and adjust it for the change.
                const newWidth = newEstimate + offsetFromEstimate;
                m.renderOptions.width = newWidth;
                leftSubtract = currentLeft - 20;
                // after this one, we'll have a new left subtract...
                newLeftSubtract = leftSubtract - (newWidth - oldWidth);

                currentSystemIndex += 1;
            } else if (i !== 0) {
                m.renderOptions.startNewSystem = false;
                m.renderOptions.displayClef = false; // check for changed clef first?
                m.renderOptions.displayKeySignature = false; // check for changed KS first?
            }
            m.renderOptions.systemIndex = currentSystemIndex;
            let currentSystemMultiplier;
            if (currentSystemIndex >= systemCurrentWidths.length) {
                /* last system... non-justified */
                currentSystemMultiplier = 1;
            } else {
                const currentSystemWidth
                    = systemCurrentWidths[currentSystemIndex];
                currentSystemMultiplier = maxSystemWidth / currentSystemWidth;
                // console.log('systemMultiplier: ' + currentSystemMultiplier + ' max: ' + maxSystemWidth + ' current: ' + currentSystemWidth);
            }
            /* might make a small gap? fix? */
            const newLeft = currentLeft - leftSubtract;
            if (newLeftSubtract !== undefined) {
                leftSubtract = newLeftSubtract;
                newLeftSubtract = undefined;
            }
            // console.log('M: ' + i + ' ; old left: ' + currentLeft + ' ; new Left: ' + newLeft);
            m.renderOptions.left = Math.floor(
                newLeft * currentSystemMultiplier
            );
            m.renderOptions.width = Math.floor(
                m.renderOptions.width * currentSystemMultiplier
            );
            const newTop
                = m.renderOptions.top
                + currentSystemIndex * (systemHeight + systemPadding);
            // console.log('M: ' + i + '; New top: ' + newTop + " ; old Top: " + m.renderOptions.top);
            m.renderOptions.top = newTop;
        }

        return systemCurrentWidths;
    }

    /**
     * overrides music21.stream.Stream#setSubstreamRenderOptions
     *
     * figures out the `.left` and `.top` attributes for all contained measures
     *
     */
    setSubstreamRenderOptions() {
        let currentMeasureIndex = 0; /* 0 indexed for now */
        let currentMeasureLeft = 20;
        const rendOp = this.renderOptions;
        let lastTimeSignature;
        let lastKeySignature;
        let lastClef;

        for (const m of this.getElementsByClass('Measure')) {
            const mRendOp = m.renderOptions;
            mRendOp.measureIndex = currentMeasureIndex;
            mRendOp.top = rendOp.top;
            mRendOp.partIndex = rendOp.partIndex;
            mRendOp.left = currentMeasureLeft;

            if (currentMeasureIndex === 0) {
                lastClef = m._clef;
                lastTimeSignature = m._timeSignature;
                lastKeySignature = m._keySignature;

                mRendOp.displayClef = true;
                mRendOp.displayKeySignature = true;
                mRendOp.displayTimeSignature = true;
            } else {
                if (
                    m._clef !== undefined
                    && lastClef !== undefined
                    && m._clef.name !== lastClef.name
                ) {
                    console.log(
                        'changing clefs for ',
                        mRendOp.measureIndex,
                        ' from ',
                        lastClef.name,
                        ' to ',
                        m._clef.name
                    );
                    lastClef = m._clef;
                    mRendOp.displayClef = true;
                } else {
                    mRendOp.displayClef = false;
                }

                if (
                    m._keySignature !== undefined
                    && lastKeySignature !== undefined
                    && m._keySignature.sharps !== lastKeySignature.sharps
                ) {
                    lastKeySignature = m._keySignature;
                    mRendOp.displayKeySignature = true;
                } else {
                    mRendOp.displayKeySignature = false;
                }

                if (
                    m._timeSignature !== undefined
                    && lastTimeSignature !== undefined
                    && m._timeSignature.ratioString
                        !== lastTimeSignature.ratioString
                ) {
                    lastTimeSignature = m._timeSignature;
                    mRendOp.displayTimeSignature = true;
                } else {
                    mRendOp.displayTimeSignature = false;
                }
            }
            mRendOp.width
                = m.estimateStaffLength() + mRendOp.staffPadding;
            mRendOp.height = m.estimateStreamHeight();
            currentMeasureLeft += mRendOp.width;
            currentMeasureIndex += 1;
        }
        return this;
    }

    /**
     * systemIndexAndScaledY - given a scaled Y, return the systemIndex
     * and the scaledYRelativeToSystem
     *
     * @param  {number} y the scaled Y
     * @return {number[]}  systemIndex, scaledYRelativeToSystem
     */
    systemIndexAndScaledY(y) {
        let systemPadding = this.renderOptions.systemPadding;
        if (systemPadding === undefined) {
            systemPadding = this.renderOptions.naiveSystemPadding;
        }
        const systemIndex = Math.floor(y / (this.systemHeight + systemPadding));
        const scaledYRelativeToSystem
            = y - systemIndex * (this.systemHeight + systemPadding);
        return [systemIndex, scaledYRelativeToSystem];
    }

    /**
     * Overrides the default music21.stream.Stream#findNoteForClick
     * by taking into account systems
     *
     * @param {HTMLElement | SVGElement} svg
     * @param {MouseEvent|TouchEvent} e
     * @param {number} [x]
     * @param {number} [y]
     * @returns {Array} [clickedDiatonicNoteNum, foundNote]
     */
    findNoteForClick(svg, e, x, y) {
        if (x === undefined || y === undefined) {
            [x, y] = this.getScaledXYforDOM(svg, e);
        }
        // debug = true;
        if (debug) {
            console.log(
                'this.estimateStreamHeight(): '
                    + this.estimateStreamHeight()
                    + ' / $(svg).height(): '
                    + $(svg).height()
            );
        }
        // TODO(msc) -- systemPadding was never used -- should it be?
        // let systemPadding = this.renderOptions.systemPadding;
        // if (systemPadding === undefined) {
        //     systemPadding = this.renderOptions.naiveSystemPadding;
        // }
        const [systemIndex, scaledYRelativeToSystem] = this.systemIndexAndScaledY(y);
        const clickedDiatonicNoteNum = this.diatonicNoteNumFromScaledY(
            scaledYRelativeToSystem
        );

        const foundNote = this.noteElementFromScaledX(
            x,
            undefined,
            systemIndex
        );
        return [clickedDiatonicNoteNum, foundNote];
    }

    /**
     * Returns the measure that is at X location xPxScaled and system systemIndex.
     *
     * @param {number} [xPxScaled]
     * @param {number} [systemIndex]
     * @returns {music21.stream.Stream}
     *
     */
    getStreamFromScaledXandSystemIndex(xPxScaled, systemIndex) {
        let gotMeasure;
        const measures = this.measures;
        for (const m of measures) {
            const rendOp = m.renderOptions;
            const left = rendOp.left;
            const right = left + rendOp.width;
            const top = rendOp.top;
            const bottom = top + rendOp.height;
            if (debug) {
                console.log(
                    'Searching for X:'
                        + Math.round(xPxScaled)
                        + ' in Measure '
                        + ' with boundaries L:'
                        + left
                        + ' R:'
                        + right
                        + ' T: '
                        + top
                        + ' B: '
                        + bottom
                );
            }
            if (xPxScaled >= left && xPxScaled <= right) {
                if (systemIndex === undefined) {
                    gotMeasure = m;
                    break;
                } else if (rendOp.systemIndex === systemIndex) {
                    gotMeasure = m;
                    break;
                }
            }
        }
        return gotMeasure;
    }
}

/**
 * Scores with multiple parts
 *
 * @class Score
 * @memberof music21.stream
 * @extends music21.stream.Stream
 */
export class Score extends Stream {
    constructor() {
        super();
        this.recursionType = 'elementsOnly';
        this.measureWidths = [];
        this.partSpacing = this.renderOptions.naiveHeight;
    }

    get clef() { // TODO: remove -- this is unlike m21p
        const c = super.clef;
        if (c === undefined) {
            return new clef.TrebleClef();
        } else {
            return c;
        }
    }

    set clef(newClef) {
        super.clef = newClef;
    }

    get systemPadding() {
        const numParts = this.parts.length;
        let systemPadding = this.renderOptions.systemPadding;
        if (systemPadding === undefined) {
            if (numParts === 1) {
                systemPadding = this.renderOptions.naiveSystemPadding; // fix to 0
            } else {
                systemPadding = this.renderOptions.naiveSystemPadding;
            }
        }
        return systemPadding;
    }

    /**
     * Returns the measure that is at X location xPxScaled and system systemIndex.
     *
     * Always returns the measure of the top part...
     *
     * @param {number} [xPxScaled]
     * @param {number} [systemIndex]
     * @returns {music21.stream.Stream} usually a Measure
     *
     */
    getStreamFromScaledXandSystemIndex(xPxScaled, systemIndex) {
        const parts = this.parts;
        return parts
            .get(0)
            .getStreamFromScaledXandSystemIndex(xPxScaled, systemIndex);
    }

    /**
     * overrides music21.stream.Stream#setSubstreamRenderOptions
     *
     * figures out the `.left` and `.top` attributes for all contained parts
     *
     * @returns {this} this
     */
    setSubstreamRenderOptions() {
        let currentPartNumber = 0;
        let currentPartTop = 0;
        const partSpacing = this.partSpacing;
        for (const p of this.parts) {
            p.renderOptions.partIndex = currentPartNumber;
            p.renderOptions.top = currentPartTop;
            p.setSubstreamRenderOptions();
            currentPartTop += partSpacing;
            currentPartNumber += 1;
        }
        this.evenPartMeasureSpacing();
        const ignoreNumSystems = true;
        const currentScoreHeight = this.estimateStreamHeight(ignoreNumSystems);
        for (const p of this.parts) {
            p.fixSystemInformation(currentScoreHeight);
        }
        this.renderOptions.height = this.estimateStreamHeight();
        return this;
    }

    /**
     * Overrides the default music21.stream.Stream#estimateStaffLength
     *
     * @returns {number}
     */
    estimateStaffLength() {
        // override
        if (this.renderOptions.overriddenWidth !== undefined) {
            // console.log("Overridden staff width: " + this.renderOptions.overriddenWidth);
            return this.renderOptions.overriddenWidth;
        }
        let maxWidth = -1;
        for (const p of this.parts) {
            const pWidth = p.estimateStaffLength();
            if (pWidth > maxWidth) {
                maxWidth = pWidth;
            }
        }
        if (maxWidth > -1) {
            return maxWidth;
        }

        // no parts found in score... use part...
        console.log('no parts found in score');
        const tempPart = new Part();
        tempPart.elements = this;
        return tempPart.estimateStaffLength();
    }

    /* MIDI override */
    /**
     * Overrides the default music21.stream.Stream#playStream
     *
     * Works crappily -- just starts *n* midi players.
     *
     * Render scrollable score works better...
     *
     * @param {Object} params -- passed to each part
     * @returns {this}
     */
    playStream(params) {
        // play multiple parts in parallel...
        for (const el of this) {
            if (el.isClassOrSubclass('Part')) {
                el.playStream(params);
            }
        }
        return this;
    }

    /**
     * Overrides the default music21.stream.Stream#stopPlayScore()
     *
     * @returns {this}
     */
    stopPlayStream() {
        for (const el of this) {
            if (el.isClassOrSubclass('Part')) {
                el.stopPlayStream();
            }
        }
        return this;
    }

    /*
     * Svg routines
     */
    /**
     * call after setSubstreamRenderOptions
     * gets the maximum measure width for each measure
     * by getting the maximum for each measure of
     * Part.getMeasureWidths();
     *
     * Does this work? I found a bug in this and fixed it that should have
     * broken it!
     *
     * @returns Array<number>
     */
    getMaxMeasureWidths() {
        const maxMeasureWidths = [];
        const measureWidthsArrayOfArrays = [];
        let i;
        // TODO: Do not crash on not partlike...
        for (const p of this.parts) {
            measureWidthsArrayOfArrays.push(p.getMeasureWidths());
        }
        for (i = 0; i < measureWidthsArrayOfArrays[0].length; i++) {
            let maxFound = 0;
            for (let j = 0; j < this.length; j++) {
                if (measureWidthsArrayOfArrays[j][i] > maxFound) {
                    maxFound = measureWidthsArrayOfArrays[j][i];
                }
            }
            maxMeasureWidths.push(maxFound);
        }
        // console.log(measureWidths);
        return maxMeasureWidths;
    }

    /**
     * systemIndexAndScaledY - given a scaled Y, return the systemIndex
     * and the scaledYRelativeToSystem
     *
     * @param  {number} y the scaled Y
     * @return Array<number>   systemIndex, scaledYRelativeToSystem
     */
    systemIndexAndScaledY(y) {
        // TODO(msc) -- systemPadding was not being used; should it be?
        // let systemPadding = this.renderOptions.systemPadding;
        // if (systemPadding === undefined) {
        //     systemPadding = this.renderOptions.naiveSystemPadding;
        // }

        const numParts = this.parts.length;
        const systemHeight = numParts * this.partSpacing + this.systemPadding;
        const systemIndex = Math.floor(y / systemHeight);
        const scaledYRelativeToSystem = y - systemIndex * systemHeight;
        return [systemIndex, scaledYRelativeToSystem];
    }

    /**
     * Returns a list of [clickedDiatonicNoteNum, foundNote] for a
     * click event, taking into account that the note will be in different
     * Part objects (and different Systems) given the height and possibly different Systems.
     *
     * @param {HTMLElement|SVGElement} svg
     * @param {MouseEvent|TouchEvent} e
     * @param {number} [x]
     * @param {number} [y]
     * @returns {Array} [diatonicNoteNum, m21Element]
     */
    findNoteForClick(svg, e, x, y) {
        if (x === undefined || y === undefined) {
            [x, y] = this.getScaledXYforDOM(svg, e);
        }
        const [systemIndex, scaledYFromSystemTop] = this.systemIndexAndScaledY(
            y
        );
        const partIndex = Math.floor(scaledYFromSystemTop / this.partSpacing);
        const scaledYinPart
            = scaledYFromSystemTop - partIndex * this.partSpacing;
        // console.log('systemIndex: ' + systemIndex + " partIndex: " + partIndex);
        const rightPart = this.parts.get(partIndex);
        if (rightPart === undefined) {
            return [undefined, undefined]; // may be too low?
        }

        const clickedDiatonicNoteNum = rightPart.diatonicNoteNumFromScaledY(
            scaledYinPart
        );

        const foundNote = rightPart.noteElementFromScaledX(
            x,
            undefined,
            systemIndex
        );
        return [clickedDiatonicNoteNum, foundNote];
    }

    /**
     * How many systems are there? Calls numSystems() on the first part.
     *
     * @returns {int}
     */
    numSystems() {
        return this.getElementsByClass('Part')
            .get(0)
            .numSystems();
    }

    /**
     * Fixes the part measure spacing for all parts.
     *
     * @param {Object} options
     * @param {Boolean} [options.setLeft=true]
     * @returns {this}
     */
    evenPartMeasureSpacing({ setLeft=true }={}) {
        const measureStacks = [];
        let currentPartNumber = 0;
        const maxMeasureWidth = []; // the maximum measure width among all parts
        let j;
        for (const p of this.parts) {
            const measureWidths = p.getMeasureWidths();
            for (j = 0; j < measureWidths.length; j++) {
                const thisMeasureWidth = measureWidths[j];
                if (measureStacks[j] === undefined) {
                    measureStacks[j] = [];
                    maxMeasureWidth[j] = thisMeasureWidth;
                } else if (thisMeasureWidth > maxMeasureWidth[j]) {
                    maxMeasureWidth[j] = thisMeasureWidth;
                }
                measureStacks[j][currentPartNumber] = thisMeasureWidth;
            }
            currentPartNumber += 1;
        }
        let currentLeft = 20;
        for (let i = 0; i < maxMeasureWidth.length; i++) {
            const measureNewWidth = maxMeasureWidth[i];
            for (const part of this.parts) {
                const measure = part.getElementsByClass('Measure').get(i);
                const rendOp = measure.renderOptions;
                rendOp.width = measureNewWidth;
                if (setLeft) {
                    rendOp.left = currentLeft;
                }
            }
            currentLeft += measureNewWidth;
        }
        return this;
    }
}

// TODO(msc) -- Opus

// small Class; a namedtuple in music21p
export class OffsetMap {
    constructor(element, offset, endTime, voiceIndex) {
        this.element = element;
        this.offset = offset;
        this.endTime = endTime;
        this.voiceIndex = voiceIndex;
    }
}
Music21j, Copyright © 2013-2021 Michael Scott Asato Cuthbert.
Documentation generated by JSDoc 3.6.3 on Wed Jul 31st 2019 using the DocStrap template.