UNPKG

music21j-port

Version:

A toolkit for computer-aided musicology, Javascript version

1,493 lines (1,401 loc) 113 kB
/** * 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-17, Michael Scott Cuthbert and cuthbertLab * Based on music21 (=music21p), Copyright (c) 2006-17, Michael Scott Cuthbert and cuthbertLab * */ import * as $ from 'jquery'; import * as MIDI from 'midicube'; import { Music21Exception } from './exceptions21.js'; import { base } from './base.js'; import { beam } from './beam.js'; import { clef } from './clef.js'; import { common } from './common.js'; import { debug } from './debug.js'; import { duration } from './duration.js'; import { instrument } from './instrument.js'; import { meter } from './meter.js'; import { note } from './note.js'; import { pitch } from './pitch.js'; import { renderOptions } from './renderOptions.js'; import { vfShow } from './vfShow.js'; import { GeneralObjectExporter } from './musicxml/m21ToXml.js'; import * as filters from './stream/filters.js'; import * as iterator from './stream/iterator.js'; export { filters }; export { iterator }; /** * 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 */ export const stream = { filters, iterator, }; class StreamException extends Music21Exception {} stream.StreamException = StreamException; 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 {Event} e * @returns {music21.base.Music21Object|undefined} - returns whatever changedCallbackFunction does. */ this.DOMChangerFunction = e => { const canvasOrSVGElement = e.currentTarget; 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 ); }; } * [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|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; 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; // would prefer weakref, but does not exist in JS. } 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. * * @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 || 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 stream.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 stream.Voice(); v.id = voiceIndex; m.insert(0, v); } post.insert(o, m); o += oneMeasureLength; measureCount += 1; } 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); const mEnd = mStart + lastTimeSignature.barDuration.quarterLength; 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 stream) { 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; } mergeAttributes(other) { super.mergeAttributes(other); for (const attr of [ 'autoSort', 'isSorted', 'definesExplicitSystemBreaks', 'definesEExplicitPageeBreaks', '_atSoundingPitch', '_mutable', ]) { if (Object.prototype.hasOwnProperty.call(other, attr)) { this[attr] = other[attr]; } } } /** * 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()) { $.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 stream.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 = $.extend({}, extendableStepList); lastOctaveStepList.push(tempOctaveStepDict); } const lastOctavelessStepDict = $.extend({}, extendableStepList); // probably unnecessary, but safe... for (const el of this) { if (el.pitch !== undefined) { // note