UNPKG

music21j

Version:

A toolkit for computer-aided musicology, Javascript version

1,421 lines (1,358 loc) 2.73 MB
/** * 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