music21j
Version:
A toolkit for computer-aided musicology, Javascript version
1,421 lines (1,358 loc) • 2.73 MB
JavaScript
/**
* music21j version 0.16.7 built on 2025-02-24.
* Copyright (c) 2013-2025 Michael Scott Asato Cuthbert
* BSD License, see LICENSE
*
* https://github.com/cuthbertLab/music21j
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define("music21", [], factory);
else if(typeof exports === 'object')
exports["music21"] = factory();
else
root["music21"] = factory();
})(self, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/articulations.ts":
/*!******************************!*\
!*** ./src/articulations.ts ***!
\******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Accent: () => (/* binding */ Accent),
/* harmony export */ Articulation: () => (/* binding */ Articulation),
/* harmony export */ ArticulationPlacement: () => (/* binding */ ArticulationPlacement),
/* harmony export */ ArticulationPlacementToVexFlowModifierPosition: () => (/* binding */ ArticulationPlacementToVexFlowModifierPosition),
/* harmony export */ DynamicArticulation: () => (/* binding */ DynamicArticulation),
/* harmony export */ LengthArticulation: () => (/* binding */ LengthArticulation),
/* harmony export */ Marcato: () => (/* binding */ Marcato),
/* harmony export */ PitchArticulation: () => (/* binding */ PitchArticulation),
/* harmony export */ Spiccato: () => (/* binding */ Spiccato),
/* harmony export */ Staccatissimo: () => (/* binding */ Staccatissimo),
/* harmony export */ Staccato: () => (/* binding */ Staccato),
/* harmony export */ StrongAccent: () => (/* binding */ StrongAccent),
/* harmony export */ Tenuto: () => (/* binding */ Tenuto),
/* harmony export */ TimbreArticulation: () => (/* binding */ TimbreArticulation),
/* harmony export */ setPlacementOnVexFlowArticulation: () => (/* binding */ setPlacementOnVexFlowArticulation)
/* harmony export */ });
/* harmony import */ var vexflow__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vexflow */ "./node_modules/vexflow/build/esm/entry/vexflow.js");
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./common */ "./src/common.ts");
/* harmony import */ var _prebase__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./prebase */ "./src/prebase.ts");
/**
* articulations module.
*/
var ArticulationPlacement;
(function (ArticulationPlacement) {
ArticulationPlacement["ABOVE"] = "above";
ArticulationPlacement["BELOW"] = "below";
ArticulationPlacement["LEFT"] = "left";
ArticulationPlacement["RIGHT"] = "right";
ArticulationPlacement["STEM_SIDE"] = "stemSide";
ArticulationPlacement["NOTE_SIDE"] = "noteSide";
})(ArticulationPlacement || (ArticulationPlacement = {}));
const ArticulationPlacementToVexFlowModifierPosition = new Map([[ArticulationPlacement.ABOVE, vexflow__WEBPACK_IMPORTED_MODULE_0__.Modifier.Position.ABOVE], [ArticulationPlacement.BELOW, vexflow__WEBPACK_IMPORTED_MODULE_0__.Modifier.Position.BELOW], [ArticulationPlacement.LEFT, vexflow__WEBPACK_IMPORTED_MODULE_0__.Modifier.Position.LEFT], [ArticulationPlacement.RIGHT, vexflow__WEBPACK_IMPORTED_MODULE_0__.Modifier.Position.RIGHT]]);
/**
* This works the same for music21 Articulations and Expressions
*/
function setPlacementOnVexFlowArticulation(vfa, placement, stemDirection) {
if (placement === undefined) {
return;
}
if ((!stemDirection || stemDirection === 'none') && (placement === ArticulationPlacement.STEM_SIDE || placement === ArticulationPlacement.NOTE_SIDE)) {
placement = ArticulationPlacement.ABOVE;
}
if (placement === ArticulationPlacement.STEM_SIDE) {
if (stemDirection === 'up') {
placement = ArticulationPlacement.ABOVE;
} else {
placement = ArticulationPlacement.BELOW;
}
} else if (placement === ArticulationPlacement.NOTE_SIDE) {
if (stemDirection === 'up') {
placement = ArticulationPlacement.BELOW;
} else {
placement = ArticulationPlacement.ABOVE;
}
}
if (ArticulationPlacementToVexFlowModifierPosition.has(placement)) {
vfa.setPosition(ArticulationPlacementToVexFlowModifierPosition.get(placement));
}
}
/**
* Represents a single articulation, usually in the `.articulations` Array
* on a music21.note.Note or something like that.
*
* @property {string} name
* @property {string} [placement='above']
* @property {string|undefined} vexflowModifier - the string code to get this accidental in Vexflow
* @property {number} [dynamicScale=1.0] - multiplier for the dynamic of a note that this is attached to
* @property {number} [lengthScale=1.0] - multiplier for the length of a note that this is attached to.
*/
class Articulation extends _prebase__WEBPACK_IMPORTED_MODULE_2__.ProtoM21Object {
constructor() {
super(...arguments);
this.placement = ArticulationPlacement.NOTE_SIDE;
this.vexflowModifier = '';
this.dynamicScale = 1.0;
this.lengthScale = 1.0;
}
static get className() {
return 'music21.articulation.Articulation';
}
/**
* Generates a Vex.Flow.Articulation for this articulation if it is something
* vexflow can display.
*/
vexflow() {
let {
stemDirection
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (!this.vexflowModifier) {
return null;
}
const vfa = new vexflow__WEBPACK_IMPORTED_MODULE_0__.Articulation(this.vexflowModifier);
setPlacementOnVexFlowArticulation(vfa, this.placement, stemDirection);
return vfa;
}
}
/**
* base class for articulations that change the length of a note...
*/
class LengthArticulation extends Articulation {
constructor() {
super(...arguments);
this.name = 'length-articulation';
}
static get className() {
return 'music21.articulation.LengthArticulation';
}
}
/**
* base class for articulations that change the dynamic of a note...
*/
class DynamicArticulation extends Articulation {
constructor() {
super(...arguments);
this.name = 'dynamic-articulation';
}
static get className() {
return 'music21.articulation.DynamicArticulation';
}
}
/**
* base class for articulations that change the pitch of a note...
*/
class PitchArticulation extends Articulation {
constructor() {
super(...arguments);
this.name = 'pitch-articulation';
}
static get className() {
return 'music21.articulation.PitchArticulation';
}
}
/**
* base class for articulations that change the timbre of a note...
*/
class TimbreArticulation extends Articulation {
constructor() {
super(...arguments);
this.name = 'timbre-articulation';
}
static get className() {
return 'music21.articulation.TimbreArticulation';
}
}
/**
* 50% louder than usual
*/
class Accent extends DynamicArticulation {
constructor() {
super(...arguments);
this.name = 'accent';
this.vexflowModifier = 'a>';
this.dynamicScale = 1.5;
}
static get className() {
return 'music21.articulation.Accent';
}
}
/**
* 100% louder than usual
*/
class StrongAccent extends Accent {
constructor() {
super(...arguments);
this.name = 'strong accent';
this.vexflowModifier = 'a^';
this.dynamicScale = 2.0;
}
static get className() {
return 'music21.articulation.StrongAccent';
}
}
class Staccato extends LengthArticulation {
constructor() {
super(...arguments);
this.name = 'staccato';
this.vexflowModifier = 'a.';
this.lengthScale = 0.6;
}
static get className() {
return 'music21.articulation.Staccato';
}
}
class Staccatissimo extends Staccato {
constructor() {
super(...arguments);
this.name = 'staccatissimo';
this.vexflowModifier = 'av';
this.lengthScale = 0.3;
}
static get className() {
return 'music21.articulation.Staccatissimo';
}
}
/**
* no difference in playback from staccato. no display.
*/
class Spiccato extends Staccato {
constructor() {
super(...arguments);
this.name = 'spiccato';
this.vexflowModifier = '';
}
static get className() {
return 'music21.articulation.Spiccato';
}
}
/**
* should be both a DynamicArticulation and a LengthArticulation
* TODO(msc): check that `.classes` reflects that in music21j
*/
class Marcato extends DynamicArticulation {
static get className() {
return 'music21.articulation.Marcato';
}
constructor() {
super();
_common__WEBPACK_IMPORTED_MODULE_1__.mixin(LengthArticulation, this);
this.name = 'marcato';
this.vexflowModifier = 'a^';
this.dynamicScale = 1.7;
}
}
class Tenuto extends LengthArticulation {
constructor() {
super(...arguments);
this.name = 'tenuto';
this.vexflowModifier = 'a-';
}
static get className() {
return 'music21.articulation.Tenuto';
}
}
/***/ }),
/***/ "./src/audioRecording.ts":
/*!*******************************!*\
!*** ./src/audioRecording.ts ***!
\*******************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Recorder: () => (/* binding */ Recorder)
/* harmony export */ });
/* harmony import */ var _audioSearch__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./audioSearch */ "./src/audioSearch.ts");
/**
* Adopted from Matt Diamond's recorder.js code MIT License
*/
class Recorder {
constructor() {
let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.recording = false;
const config = cfg || {};
this.bufferLen = config.bufferLen || 4096;
this.config = config;
this.recording = false;
this.currCallback = undefined;
this.audioContext = _audioSearch__WEBPACK_IMPORTED_MODULE_0__.config.audioContext;
this.frequencyCanvasInfo = {
id: 'frequencyAnalyser',
width: undefined,
height: undefined,
canvasContext: undefined,
animationFrameID: undefined
};
this.waveformCanvasInfo = {
id: 'waveformCanvas',
width: undefined,
height: undefined,
canvasContext: undefined
};
this.analyserNode = undefined;
/**
*
* @type {BaseAudioContext|undefined}
*/
this.context = undefined;
}
/**
* Start here -- polyfills navigator, runs getUserMedia and then sends to audioStreamConnected
*/
initAudio() {
const constraints = {
audio: {
echoCancellation: false,
autoGainControl: false,
noiseSuppression: false
},
video: false
};
navigator.mediaDevices.getUserMedia(constraints).then(s => this.audioStreamConnected(s)).catch(error => {
console.log('Error getting audio -- try on google Chrome?');
console.log(error);
});
}
/**
* After the user has given permission to record, this method is called.
* It creates a gain point, and then connects the input source to the gain.
* It connects an analyserNode (fftSize 2048) to the gain.
*
* It creates a second gain of 0.0 connected to the destination, so that
* we're not hearing what we're playing in in an infinite loop (SUCKS to turn this off...)
*
* And it calls this.connectSource on the inputPoint so that
* we can do something with all these wonderful inputs.
*/
audioStreamConnected(stream) {
const inputPoint = this.audioContext.createGain();
// Create an AudioNode from the stream.
const audioInput = this.audioContext.createMediaStreamSource(stream);
audioInput.connect(inputPoint);
const analyserNode = this.audioContext.createAnalyser();
analyserNode.fftSize = _audioSearch__WEBPACK_IMPORTED_MODULE_0__.config.fftSize;
this.analyserNode = analyserNode;
inputPoint.connect(analyserNode);
this.connectSource(inputPoint);
const zeroGain = this.audioContext.createGain();
zeroGain.gain.value = 0.0;
inputPoint.connect(zeroGain);
zeroGain.connect(this.audioContext.destination);
}
/**
* Creates a worker to receive and process all the messages asynchronously.
*/
connectSource(source) {
const context = source.context;
this.context = context;
this.setNode();
// create a Worker with inline code...
const workerBlob = new Blob(['(', recorderWorkerJs, ')()'], {
type: 'application/javascript'
});
const workerURL = URL.createObjectURL(workerBlob);
this.worker = new Worker(workerURL);
/**
* When worker sends a message, we just send it to the currentCallback...
*/
this.worker.onmessage = e => {
const blob = e.data;
this.currCallback(blob);
};
URL.revokeObjectURL(workerURL);
this.worker.postMessage({
command: 'init',
config: {
sampleRate: this.context.sampleRate
}
});
/**
* Whenever the ScriptProcessorNode receives enough audio to process
* (i.e., this.bufferLen stereo samples; default 4096), then it calls onaudioprocess
* which is set up to send the event's .getChannelData to the WebWorker via a
* postMessage.
*
* The 'record' command sends no message back.
*/
// should be replaced by workers somehow.
this.node.onaudioprocess = e => {
if (!this.recording) {
return;
}
this.worker.postMessage({
command: 'record',
buffer: [e.inputBuffer.getChannelData(0), e.inputBuffer.getChannelData(1)]
});
};
source.connect(this.node);
/**
* polyfill for Chrome error.
*
* if the ScriptProcessorNode (this.node) is not connected to an output
* the "onaudioprocess" event is not triggered in Chrome.
*/
this.node.connect(this.context.destination);
}
/**
* Creates a ScriptProcessorNode (preferably) to allow for direct audio processing.
*
* Sets it to this.node and returns it.
*/
setNode() {
const numInputChannels = 2;
const numOutputChannels = 2;
this.node = this.context.createScriptProcessor(this.bufferLen, numInputChannels, numOutputChannels);
return this.node;
}
/**
* Configure from another source...
*/
configure(cfg) {
for (const prop in cfg) {
if (Object.hasOwnProperty.call(cfg, prop)) {
this.config[prop] = cfg[prop];
}
}
}
record() {
this.recording = true;
}
stop() {
this.recording = false;
}
clear() {
this.worker.postMessage({
command: 'clear'
});
}
/**
* Directly get the buffers from the worker and then call cb.
*/
getBuffers(cb) {
this.currCallback = cb || this.config.callback;
this.worker.postMessage({
command: 'getBuffers'
});
}
/**
* call exportWAV or exportMonoWAV on the worker, then call cb or (if undefined) setupDownload.
*/
exportWAV(cb, type, isMono) {
let command = 'exportWAV';
if (isMono === true) {
// default false
command = 'exportMonoWAV';
}
this.currCallback = cb || this.config.callback;
type = type || this.config.type || 'audio/wav';
if (!this.currCallback) {
this.currCallback = blob => {
this.setupDownload(blob, 'myRecording' + Date.now().toString() + '.wav');
};
}
this.worker.postMessage({
command,
type
});
}
exportMonoWAV(cb, type) {
this.exportWAV(cb, type, true);
}
setupDownload(blob) {
let filename = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'output.wav';
let elementId = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'save';
const url = (window.URL || window.webkitURL).createObjectURL(blob);
const link = document.getElementById(elementId);
link.href = url;
link.download = filename;
}
setContextForCanvasInfo(canvasInfo) {
if (canvasInfo.canvasContext) {
return;
}
const canvas = document.getElementById(canvasInfo.id);
if (!canvas) {
return;
}
canvasInfo.width = canvas.width;
canvasInfo.height = canvas.height;
canvasInfo.canvasContext = canvas.getContext('2d');
}
/**
* Update the Analysers.
*/
updateAnalysers(time) {
this.setContextForCanvasInfo(this.frequencyCanvasInfo);
// analyser draw code here
const SPACING = 3;
const BAR_WIDTH = 1;
const numBars = Math.round(this.frequencyCanvasInfo.width / SPACING);
const freqByteData = new Uint8Array(this.analyserNode.frequencyBinCount);
this.analyserNode.getByteFrequencyData(freqByteData);
const canvasContext = this.frequencyCanvasInfo.canvasContext;
canvasContext.clearRect(0, 0, this.frequencyCanvasInfo.width, this.frequencyCanvasInfo.height);
canvasContext.fillStyle = '#F6D565';
canvasContext.lineCap = 'round';
const multiplier = this.analyserNode.frequencyBinCount / numBars;
// Draw rectangle for each frequency bin.
for (let i = 0; i < numBars; ++i) {
let magnitude = 0;
const offset = Math.floor(i * multiplier);
for (let j = 0; j < multiplier; j++) {
magnitude += freqByteData[offset + j];
}
magnitude = magnitude * (this.frequencyCanvasInfo.height / 256) / multiplier;
canvasContext.fillStyle = 'hsl( ' + Math.round(i * 360 / numBars) + ', 100%, 50%)';
canvasContext.fillRect(i * SPACING, this.frequencyCanvasInfo.height, BAR_WIDTH, -1 * magnitude);
}
this.frequencyCanvasInfo.animationFrameID = window.requestAnimationFrame(t => this.updateAnalysers(t));
}
drawWaveformCanvas(buffers) {
const data = buffers[0]; // one track of stereo recording.
this.setContextForCanvasInfo(this.waveformCanvasInfo);
const context = this.waveformCanvasInfo.canvasContext;
const step = Math.ceil(data.length / this.waveformCanvasInfo.width);
const amp = this.waveformCanvasInfo.height / 2;
context.fillStyle = 'silver';
context.clearRect(0, 0, this.waveformCanvasInfo.width, this.waveformCanvasInfo.height);
for (let i = 0; i < this.waveformCanvasInfo.width; i++) {
let min = 1.0;
let max = -1.0;
for (let j = 0; j < step; j++) {
const datum = data[i * step + j];
if (datum < min) {
min = datum;
}
if (datum > max) {
max = datum;
}
}
context.fillRect(i, (1 + min) * amp, 1, Math.max(1, (max - min) * amp));
}
}
/**
* set this as a callback from getBuffers. Returns the source so that a stop() command
* is possible.
*/
playBuffers(buffers) {
const channels = 2;
const numFrames = buffers[0].length;
const audioBuffer = this.context.createBuffer(channels, numFrames, this.context.sampleRate);
for (let channel = 0; channel < channels; channel++) {
const thisChannelBuffer = audioBuffer.getChannelData(channel);
for (let i = 0; i < numFrames; i++) {
thisChannelBuffer[i] = buffers[channel][i];
}
}
const source = this.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.context.destination);
source.start();
return source;
}
}
/**
* This code does NOT go through babel, so no arrow functions, let, const, etc.
*/
const recorderWorkerJs = `function recorderWorkerJs() {
/**
*
* Rewritten from Matt Diamond's recorderWorker -- MIT License
*/
RecorderWorker = function RecorderWorker(parentContext) {
this.parent = parentContext;
this.recLength = 0;
this.recBuffersL = [];
this.recBuffersR = [];
this.sampleRate = undefined;
};
RecorderWorker.prototype.onmessage = function onmessage(e) {
switch (e.data.command) {
case 'init':
this.init(e.data.config);
break;
case 'record':
this.record(e.data.buffer);
break;
case 'exportWAV':
this.exportWAV(e.data.type);
break;
case 'exportMonoWAV':
this.exportMonoWAV(e.data.type);
break;
case 'getBuffers':
this.getBuffers();
break;
case 'clear':
this.clear();
break;
default:
break;
}
};
RecorderWorker.prototype.postMessage = function postMessage(msg) {
this.parent.postMessage(msg);
};
RecorderWorker.prototype.init = function init(config) {
this.sampleRate = config.sampleRate;
};
RecorderWorker.prototype.record = function record(inputBuffer) {
var inputBufferL = inputBuffer[0];
var inputBufferR = inputBuffer[1];
this.recBuffersL.push(inputBufferL);
this.recBuffersR.push(inputBufferR);
this.recLength += inputBufferL.length;
};
RecorderWorker.prototype.exportWAV = function exportWAV(type) {
var bufferL = this.mergeBuffers(this.recBuffersL);
var bufferR = this.mergeBuffers(this.recBuffersR);
var interleaved = this.interleave(bufferL, bufferR);
var dataview = this.encodeWAV(interleaved);
var audioBlob = new Blob([dataview], { 'type': type });
this.postMessage(audioBlob);
};
RecorderWorker.prototype.exportMonoWAV = function exportMonoWAV(type) {
var bufferL = this.mergeBuffers(this.recBuffersL);
var dataview = this.encodeWAV(bufferL);
var audioBlob = new Blob([dataview], { 'type': type });
this.postMessage(audioBlob);
};
RecorderWorker.prototype.mergeBuffers = function mergeBuffers(recBuffers) {
var result = new Float32Array(this.recLength);
var offset = 0;
for (var i = 0; i < recBuffers.length; i++) {
result.set(recBuffers[i], offset);
offset += recBuffers[i].length;
}
return result;
};
RecorderWorker.prototype.getBuffers = function getBuffers() {
var buffers = [];
buffers.push(this.mergeBuffers(this.recBuffersL));
buffers.push(this.mergeBuffers(this.recBuffersR));
this.postMessage(buffers);
};
RecorderWorker.prototype.clear = function clear() {
this.recLength = 0;
this.recBuffersL = [];
this.recBuffersR = [];
}
RecorderWorker.prototype.interleave = function interleave(inputL, inputR) {
var combinedLength = inputL.length + inputR.length;
var result = new Float32Array(combinedLength);
var index = 0;
var inputIndex = 0;
while (index < combinedLength) {
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
inputIndex++;
}
return result;
}
RecorderWorker.prototype.encodeWAV = function encodeWAV(samples, mono) {
var buffer = new ArrayBuffer(44 + (samples.length * 2));
var view = new DataView(buffer);
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 32 + samples.length * 2, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, mono ? 1 : 2, true);
/* sample rate */
view.setUint32(24, this.sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, this.sampleRate * 4, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, 4, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, samples.length * 2, true);
floatTo16BitPCM(view, 44, samples);
return view;
}
function floatTo16BitPCM(output, offset, input) {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
function writeString(view, offset, string) {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
}
var recordWorker = new RecorderWorker(this);
this.onmessage = (function mainOnMessage(e) { recordWorker.onmessage(e) }).bind(this);
}`;
/***/ }),
/***/ "./src/audioSearch.ts":
/*!****************************!*\
!*** ./src/audioSearch.ts ***!
\****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ animateLoop: () => (/* binding */ animateLoop),
/* harmony export */ autoCorrelate: () => (/* binding */ autoCorrelate),
/* harmony export */ config: () => (/* binding */ config),
/* harmony export */ getUserMedia: () => (/* binding */ getUserMedia),
/* harmony export */ midiNumDiffFromFrequency: () => (/* binding */ midiNumDiffFromFrequency),
/* harmony export */ sampleCallback: () => (/* binding */ sampleCallback),
/* harmony export */ smoothPitchExtraction: () => (/* binding */ smoothPitchExtraction),
/* harmony export */ userMediaStarted: () => (/* binding */ userMediaStarted)
/* harmony export */ });
/* harmony import */ var midicube__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! midicube */ "./node_modules/midicube/releases/midicube.js");
/* harmony import */ var midicube__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(midicube__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./common */ "./src/common.ts");
/**
* audioSearch module. See music21.audioSearch namespace
*/
// functions based on the prototype created by Chris Wilson's MIT License version
// and on Jordi Bartolome Guillen's audioSearch module for music21
// TODO(msc): Rewrite as a class -- config is just a class in disguise
class _ConfigSingletonCreator {
constructor() {
this.fftSize = 2048;
this._audioContext = null;
this.animationFrameCallbackId = -1;
this.sampleBuffer = null;
this.minFrequency = 55;
this.maxFrequency = 1050;
this.pitchSmoothingSize = 40;
this.lastPitchClassesDetected = [];
this.lastPitchesDetected = [];
this.lastCentsDeviationsDetected = [];
this.AudioContextCaller = window.AudioContext || window.webkitAudioContext;
}
get audioContext() {
if (this._audioContext !== null) {
return this._audioContext;
} else {
// AudioContext should be a singleton, but MIDI reports loaded before it is!
if (midicube__WEBPACK_IMPORTED_MODULE_0__ !== undefined && midicube__WEBPACK_IMPORTED_MODULE_0__.WebAudio !== undefined && midicube__WEBPACK_IMPORTED_MODULE_0__.WebAudio.getContext() !== undefined) {
window.globalAudioContext = midicube__WEBPACK_IMPORTED_MODULE_0__.WebAudio.getContext();
} else if (typeof window.globalAudioContext === 'undefined') {
window.globalAudioContext = new this.AudioContextCaller();
}
this._audioContext = window.globalAudioContext;
return this._audioContext;
}
}
set audioContext(ac) {
this._audioContext = ac;
}
}
const config = new _ConfigSingletonCreator();
/**
* Note: audioRecording uses the newer getUserMedia routines, so
* this should be ported to be similar to there.
*
* @param {Object} dictionary - optional dictionary to fill
* @param {function} callback - callback on success
* @param {function} error - callback on error
*/
function getUserMedia(dictionary, callback, error) {
if (error === undefined) {
/* eslint no-alert: "off"*/
error = () => {
alert('navigator.getUserMedia either not defined (Safari, IE) or denied.');
};
}
if (callback === undefined) {
callback = mediaStream => {
userMediaStarted(mediaStream);
};
}
const n = navigator;
// need to polyfill navigator, or binding problems are hard...
// noinspection JSUnresolvedVariable
n.getUserMedia = n.getUserMedia || n.webkitGetUserMedia || n.mozGetUserMedia || n.msGetUserMedia;
if (n.getUserMedia === undefined) {
error();
}
if (dictionary === undefined) {
dictionary = {
audio: {
mandatory: {},
optional: []
}
};
}
n.getUserMedia(dictionary, callback, error);
}
function userMediaStarted(audioStream) {
/**
* This function which patches Safari requires some time to get started
* so we call it on object creation.
*/
config.sampleBuffer = new Float32Array(config.fftSize / 2);
const mediaStreamSource = config.audioContext.createMediaStreamSource(audioStream);
const analyser = config.audioContext.createAnalyser();
analyser.fftSize = config.fftSize;
mediaStreamSource.connect(analyser);
config.currentAnalyser = analyser;
animateLoop();
}
const animateLoop = () => {
config.currentAnalyser.getFloatTimeDomainData(config.sampleBuffer);
// returns best frequency or -1
const frequencyDetected = autoCorrelate(config.sampleBuffer, config.audioContext.sampleRate, config.minFrequency, config.maxFrequency);
const retValue = sampleCallback(frequencyDetected);
// callback can be anything.
// noinspection JSIncompatibleTypesComparison
if (retValue !== -1) {
config.animationFrameCallbackId = window.requestAnimationFrame(animateLoop);
}
};
function smoothPitchExtraction(frequency) {
if (frequency === -1) {
config.lastPitchClassesDetected.shift();
config.lastPitchesDetected.shift();
config.lastCentsDeviationsDetected.shift();
} else {
const [midiNum, centsOff] = midiNumDiffFromFrequency(frequency);
if (config.lastPitchClassesDetected.length > config.pitchSmoothingSize) {
config.lastPitchClassesDetected.shift();
config.lastPitchesDetected.shift();
config.lastCentsDeviationsDetected.shift();
}
config.lastPitchClassesDetected.push(midiNum % 12);
config.lastPitchesDetected.push(midiNum);
config.lastCentsDeviationsDetected.push(centsOff);
}
const mostCommonPitchClass = _common__WEBPACK_IMPORTED_MODULE_1__.statisticalMode(config.lastPitchClassesDetected);
if (mostCommonPitchClass === null) {
return [-1, 0];
}
const pitchesMatchingClass = [];
const centsMatchingClass = [];
for (let i = 0; i < config.lastPitchClassesDetected.length; i++) {
if (config.lastPitchClassesDetected[i] === mostCommonPitchClass) {
pitchesMatchingClass.push(config.lastPitchesDetected[i]);
centsMatchingClass.push(config.lastCentsDeviationsDetected[i]);
}
}
const mostCommonPitch = _common__WEBPACK_IMPORTED_MODULE_1__.statisticalMode(pitchesMatchingClass);
// find cents difference; weighing more recent samples more...
let totalSamplePoints = 0;
let totalSample = 0;
for (let j = 0; j < centsMatchingClass.length; j++) {
const weight = Math.pow(j, 2) + 1;
totalSample += weight * centsMatchingClass[j];
totalSamplePoints += weight;
}
const centsOff = Math.floor(totalSample / totalSamplePoints);
return [mostCommonPitch, centsOff];
}
function sampleCallback(frequency) {
// noinspection JSUnusedLocalSymbols
const [unused_midiNum, unused_centsOff] = smoothPitchExtraction(frequency);
return 0;
}
// from Chris Wilson. Replace with Jordi's
function autoCorrelate(buf, sampleRate) {
let minFrequency = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
let maxFrequency = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : undefined;
const SIZE = buf.length;
const MAX_SAMPLES = Math.floor(SIZE / 2);
if (maxFrequency === undefined) {
maxFrequency = sampleRate;
}
let best_offset = -1;
let best_correlation = 0;
let rms = 0;
let foundGoodCorrelation = false;
const correlations = new Array(MAX_SAMPLES);
for (let i = 0; i < SIZE; i++) {
const val = buf[i];
rms += val * val;
}
rms = Math.sqrt(rms / SIZE);
if (rms < 0.01) {
return -1;
} // not enough signal
let lastCorrelation = 1;
for (let offset = 0; offset < MAX_SAMPLES; offset++) {
let correlation = 0;
const offsetFrequency = sampleRate / offset;
if (offsetFrequency < minFrequency) {
break;
}
if (offsetFrequency > maxFrequency) {
continue;
}
for (let i = 0; i < MAX_SAMPLES; i++) {
correlation += Math.abs(buf[i] - buf[i + offset]);
}
correlation = 1 - correlation / MAX_SAMPLES;
correlations[offset] = correlation; // store it, for the tweaking we need to do below.
if (correlation > 0.9 && correlation > lastCorrelation) {
foundGoodCorrelation = true;
if (correlation > best_correlation) {
best_correlation = correlation;
best_offset = offset;
}
} else if (foundGoodCorrelation) {
// short-circuit - we found a good correlation, then a bad one, so we'd just be seeing copies from here.
// Now we need to tweak the offset - by interpolating between the values to the left and right of the
// best offset, and shifting it a bit. This is complex, and HACKY in this code (happy to take PRs!) -
// we need to do a curve fit on correlations[] around best_offset in order to better determine precise
// (anti-aliased) offset.
// we know best_offset >=1,
// since foundGoodCorrelation cannot go to true until the second pass (offset=1), and
// we can't drop into this clause until the following pass (else if).
const shift = (correlations[best_offset + 1] - correlations[best_offset - 1]) / correlations[best_offset];
return sampleRate / (best_offset + 8 * shift);
}
lastCorrelation = correlation;
}
if (best_correlation > 0.01) {
// console.log("f = " + sampleRate/best_offset + "Hz (rms: " + rms + " confidence: " + best_correlation + ")")
return sampleRate / best_offset;
}
return -1;
// var best_frequency = sampleRate/best_offset;
}
function midiNumDiffFromFrequency(frequency) {
const midiNumFloat = 12 * Math.log2(frequency / 440) + 69;
const midiNum = Math.round(midiNumFloat);
const centsOff = Math.round(100 * (midiNumFloat - midiNum));
return [midiNum, centsOff];
}
/***/ }),
/***/ "./src/bar.ts":
/*!********************!*\
!*** ./src/bar.ts ***!
\********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ BarException: () => (/* binding */ BarException),
/* harmony export */ Barline: () => (/* binding */ Barline),
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var _base__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./base */ "./src/base.ts");
/* harmony import */ var _exceptions21__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./exceptions21 */ "./src/exceptions21.ts");
/**
* music21j -- Javascript reimplementation of Core music21p features.
* music21/bar -- Barline objects
*
* Copyright (c) 2013-24, Michael Scott Asato Cuthbert
* Based on music21 (=music21p), Copyright (c) 2006-24, Michael Scott Asato Cuthbert
*
*/
const barTypeList = ['regular', 'dotted', 'dashed', 'heavy', 'double', 'final', 'heavy-light', 'heavy-heavy', 'tick', 'short', 'none'];
const barTypeDict = {
'light-light': 'double',
'light-heavy': 'final'
};
const reverseBarTypeDict = {
'double': 'light-light',
'final': 'light-heavy'
};
class BarException extends _exceptions21__WEBPACK_IMPORTED_MODULE_1__.Music21Exception {}
function typeToMusicXMLBarStyle(value) {
if (reverseBarTypeDict[value] !== undefined) {
return reverseBarTypeDict[value];
} else {
return value;
}
}
function standardizeBarType() {
let value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'regular';
value = value.toLowerCase();
if (barTypeList.includes(value)) {
return value;
}
if (barTypeDict[value] !== undefined) {
return barTypeDict[value];
}
throw new BarException(`cannot process style: ${value}`);
}
class Barline extends _base__WEBPACK_IMPORTED_MODULE_0__.Music21Object {
static get className() {
return 'music21.bar.Barline';
}
constructor() {
let type = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'regular';
let location = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'right';
super();
this.type = type;
this.location = location; // left, right, middle, None
}
get type() {
return this._type;
}
set type(v) {
this._type = standardizeBarType(v);
}
musicXMLBarStyle() {
return typeToMusicXMLBarStyle(this.type);
}
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Barline);
/***/ }),
/***/ "./src/base.ts":
/*!*********************!*\
!*** ./src/base.ts ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Music21Object: () => (/* binding */ Music21Object)
/* harmony export */ });
/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./common */ "./src/common.ts");
/* harmony import */ var _derivation__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./derivation */ "./src/derivation.ts");
/* harmony import */ var _duration__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./duration */ "./src/duration.ts");
/* harmony import */ var _editorial__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./editorial */ "./src/editorial.ts");
/* harmony import */ var _prebase__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./prebase */ "./src/prebase.ts");
/* harmony import */ var _sites__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./sites */ "./src/sites.ts");
/* harmony import */ var _style__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./style */ "./src/style.ts");
/* harmony import */ var _exceptions21__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./exceptions21 */ "./src/exceptions21.ts");
/**
* music21j -- Javascript reimplementation of Core music21p features.
* music21/base -- objects in base in music21p routines
*
* does not load the other modules.
*
* Copyright (c) 2013-24, Michael Scott Asato Cuthbert
* Based on music21 (=music21p), Copyright (c) 2006-24, Michael Scott Asato Cuthbert
*
* module for Music21Objects
*/
/**
* Base class for any object that can be placed in a {@link Stream}.
*
* @property {Stream} [activeSite] - hardlink to a
* {@link Stream} containing the element.
* @property {number} classSortOrder - Default sort order for this class
* (default 20; override in other classes). Lower numbered objects will sort
* before other objects in the staff if priority and offset are the same.
* @property {string[]} groups - An Array of strings representing group
* (equivalent to css classes) to assign to the object. (default [])
* @property {boolean} isMusic21Object - true
* @property {boolean} isStream - false
* @property {number} offset - offset from the beginning of the stream (in quarterLength)
* @property {number} priority - The priority (lower = earlier or more left) for
* elements at the same offset. (default 0)
*/
class Music21Object extends _prebase__WEBPACK_IMPORTED_MODULE_4__.ProtoM21Object {
static get className() {
return 'music21.base.Music21Object';
}
constructor() {
let _keywords = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
super();
this.classSortOrder = 20; // default;
this._activeSiteStoredOffset = 0;
this._naiveOffset = 0;
this._priority = 0;
this.id = 0;
this.groups = []; // custom object in m21p
this.isMusic21Object = true;
this.isStream = false;
this._duration = new _duration__WEBPACK_IMPORTED_MODULE_2__.Duration(0.0);
this.id = _sites__WEBPACK_IMPORTED_MODULE_5__.getId(this);
this.sites = new _sites__WEBPACK_IMPORTED_MODULE_5__.Sites();
this._cloneCallbacks._activeSite = false;
this._cloneCallbacks._activeSiteStoredOffset = false;
this._cloneCallbacks._derivation = (keyName, newObj, _deep, _memo) => {
const newDerivation = new _derivation__WEBPACK_IMPORTED_MODULE_1__.Derivation(newObj);
newDerivation.origin = this;
newDerivation.method = 'clone';
newObj[keyName] = newDerivation;
};
// noinspection JSUnusedLocalSymbols
this._cloneCallbacks.sites = (_keyName, newObj, _deep, _memo) => {
newObj.sites = new _sites__WEBPACK_IMPORTED_MODULE_5__.Sites();
};
}
/**
* Override clone on prebase to add a derivation.
*/
clone() {
let deep = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
let memo = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
const ret = super.clone(deep, memo);
const newDerivation = new _derivation__WEBPACK_IMPORTED_MODULE_1__.Derivation(ret);
newDerivation.origin = this;
newDerivation.method = 'clone'; // '__deepcopy__' in m21p
ret.derivation = newDerivation;
return ret;
}
stringInfo() {
let id16 = this.id;
if (typeof id16 === 'number') {
const idNumber = id16;
id16 = idNumber.toString(16);
while (id16.length < 4) {
id16 = '0' + id16;
}
id16 = '0x' + id16;
}
return id16;
}
get activeSite() {
return this._activeSite;
}
set activeSite(site) {
if (site === undefined) {
this._activeSite = undefined;
this._activeSiteStoredOffset = undefined;
} else {
let offset;
try {
offset = site.elementOffset(this);
} catch (e) {
throw new _sites__WEBPACK_IMPORTED_MODULE_5__.SitesException('activeSite cannot be set for an object not in the stream');
}
this._activeSite = site;
this._activeSiteStoredOffset = offset;
}
}
get derivation() {
if (this._derivation === undefined) {
this._derivation = new _derivation__WEBPACK_IMPORTED_MODULE_1__.Derivation(this);
}
return this._derivation;
}
set derivation(newDerivation) {
this._derivation = newDerivation;
}
/**
* Note that the editorial is typed as Record<string, any>
* but actually returns an editorial object
*/
get editorial() {
if (this._editorial === undefined) {
this._editorial = new _editorial__WEBPACK_IMPORTED_MODULE_3__.Editorial();
}
return this._editorial;
}
set editorial(newEditorial) {
this._editorial = newEditorial;
}
get hasEditorialInformation() {
return this._editorial !== undefined;
}
/**
* Returns true if there is a style.Style object
* already associated with this object, false otherwise.
*
* Calling .style on an object will always create a new
* Style object, so even though a new Style object isn't too expensive
* to create, this property helps to prevent creating new Styles more than
* necessary.
*/
get hasStyleInformation() {
return this._style !== undefined;
}
/**
* Returns (or Creates and then Returns) the Style object
* associated with this object, or sets a new
* style object. Different classes might use
* different Style objects because they might have different
* style needs (such as text formatting or bezier positioning)
*
* Eventually will also query the groups to see if they have
* any styles associated with them.
*/
get style() {
if (!this.hasStyleInformation) {
const StyleClass = this.constructor;
this._style = new StyleClass();
}
return this._style;
}
set style(newStyle) {
this._style = newStyle;
}
get measureNumber() {
if (this.activeSite !== undefined && this.activeSite.classes.includes('Measure')) {
const activeMeasure = this.activeSite;
return activeMeasure.number;
} else {
const m = this.sites.getObjByClass('Measure');
if (m !== undefined) {
return m.number;
} else {
return undefined;
}
}
}
/**
* Try to obtain the nearest Measure that contains this object,
and return the offset of this object within that Measure.
If a Measure is found, and that Measure has padding
defined as `paddingLeft` (for pickup measures, etc.), padding will be added to the
native offset gathered from the object.
*/
_getMeasureOffset() {
let {
includeMeasurePadding = true
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const activeS = this.activeSite;
let offsetLocal;
if (activeS !== undefined && activeS.isMeasure) {
offsetLocal = activeS.elementOffset(this);
if (includeMeasurePadding) {
offsetLocal += activeS.paddingLeft;
}
} else {
const m = this.getContextByClass('Measure', {
sortByCreationTime: true
});
if (m !== undefined) {
try {
offsetLocal = m.elementOffset(this);
if (includeMeasurePadding) {
offsetLocal += m.paddingLeft;
}
} catch (e) {
offsetLocal = this.offset;
}
} else {
offsetLocal = this.offset;
}
}
return offsetLocal;
}
get offset() {
if (this.activeSite === undefined) {
return this._naiveOffset;
} else {
return this.activeSite.elementOffset(this);
}
}
set offset(newOffset) {
newOffset = _common__WEBPACK_IMPORTED_MODULE_0__.opFrac(newOffset);
if (this.activeSite === undefined) {
this._naiveOffset = newOffset;
} else {
this.activeSite.setElementOffset(this, newOffset);
}
}
get priority() {
return this._priority;
}
set priority(p) {
this._priority = p;
}
get duration() {
return this._duration;
}
set duration(newDuration) {
if (typeof newDuration === 'object') {
this._duration = newDuration;
// common errors below...
} else if (typeof newDuration === 'number') {
this._duration.quarterLength = newDuration;
} else if (typeof newDuration === 'string') {
this._duration.type = newDuration;
}
}
get quarterLength() {
return this.duration.quarterLength;
}
set quarterLength(ql) {
this.duration.quarterLength = ql;
}
mergeAttributes(other) {
// id;
this.groups = other.groups.slice();
return this;
}
/**
* Return the offset of this element in a given site -- use .offset if you are sure that
* site === activeSite.
*
* Raises an Error if not in site.
*
* Does not change activeSite or .offset
*
* @param {Stream} site
* @param {boolean} [stringReturns=false] -- allow strings to be returned
* @returns {number|string|undefined}
*/
getOffsetBySite() {
let site = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
let stringReturns = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (site === undefined) {
return this._naiveOffset;
}
return site.elementOffset(this, stringReturns);
}
/**
* setOffsetBySite - sets the offset for a given Stream
*
* @param {Stream} site Stream object
* @param {number} value offset
*/
setOffsetBySite(site, value) {
if (site !== undefined) {
site.setElementOffset(this, value);
} else {
this._naiveOffset = value;
}
}
/**
* For an element which may not be in site, but might be in a Stream
* in site (or further in streams), find the cumulative offset of the
* element in that site.
*
* See also music21.stream.iterator.RecursiveIterator.currentHierarchyOffset for
* a method that is about 10x faster when running through a recursed stream.
*
* @param {Stream} site
* @returns {number|undefined}
*/
getOffsetInHierarchy(site) {
try {
return this.getOffsetBySite(site);
} catch (e) {} // eslint-disable-line no-empty
// noinspection JSUnusedLocalSymbols
for (const [csSite, csOffset, _csRecursionType] of this.contextSites()) {
if (csSite === site) {
return csOffset;
}
}
throw new Error(`Element ${this} is not in hierarchy of ${site}`);
}
// ---------- Contexts -------------
getContextByClass(className) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const params = {
getElementMethod: 'getElementAtOrBefore',
sortByCreationTime: false
};
_common__WEBPACK_IMPORTED_MODULE_0__.merge(params, options);
const getElementMethod = params.getElementMethod;
const sortByCreationTime = params.sortByCreationTime;
if (className !== undefined && !(className instanceof Array)) {
className = [className];
}
if (getElementMethod.includes('At') && this.isClassOrSubclass(className)) {
return this;
}
for (const [site, positionStart, s