music21j-port
Version:
A toolkit for computer-aided musicology, Javascript version
1,493 lines (1,401 loc) • 113 kB
JavaScript
/**
* 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