burns-audio-wam
Version:
Collection of Web Audio Modules (WAMs) by Burns Audio
463 lines (438 loc) • 17.6 kB
JavaScript
/******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ var __webpack_modules__ = ({
/***/ "./src/Clip.ts":
/*!*********************!*\
!*** ./src/Clip.ts ***!
\*********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Clip: () => (/* binding */ Clip),
/* harmony export */ PP16: () => (/* binding */ PP16),
/* harmony export */ PPQN: () => (/* binding */ PPQN)
/* harmony export */ });
/* harmony import */ var _shared_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../shared/util */ "../shared/util.ts");
const PPQN = 24;
const PP16 = (PPQN / 4);
class Clip {
constructor(id, state) {
if (state) {
this.state = {
id: id || state.id,
length: state.length,
notes: state.notes.map(n => {
return { ...n };
})
};
}
else {
this.state = {
id: id || (0,_shared_util__WEBPACK_IMPORTED_MODULE_0__.token)(),
length: 16 * PP16,
notes: []
};
}
this.dirty = true;
this.quantize = PP16;
}
getState(removeId) {
let state = {
length: this.state.length,
notes: this.state.notes.map(n => {
return { ...n };
})
};
if (!removeId) {
state.id = this.state.id;
}
return state;
}
async setState(state, newId) {
if (!state.id && !newId) {
console.error("Need an id for clip!");
return;
}
this.state.id = newId ? newId : state.id;
this.state.length = state.length;
this.state.notes = state.notes.map(n => {
return { ...n };
});
this.dirty = true;
if (this.updateProcessor)
this.updateProcessor(this);
}
hasNote(tick, number) {
return this.state.notes.some((n) => n.tick == tick && n.number == number);
}
addNote(tick, number, duration, velocity) {
this.dirty = true;
if (this.hasNote(tick, number)) {
return;
}
for (var insertIndex = 0; insertIndex < this.state.notes.length && this.state.notes[insertIndex].tick < tick; insertIndex++)
;
this.state.notes = this.state.notes.slice(0, insertIndex).concat([{ tick, number, duration, velocity }].concat(this.state.notes.slice(insertIndex, this.state.notes.length)));
if (this.updateProcessor)
this.updateProcessor(this);
}
removeNote(tick, number) {
this.dirty = true;
this.state.notes = this.state.notes.filter((n) => n.tick != tick || n.number != number);
if (this.updateProcessor)
this.updateProcessor(this);
}
notesForTick(tick) {
return this.state.notes.filter((n) => n.tick == tick);
}
notesInTickRange(startTick, endTick, note) {
return this.state.notes.filter((n) => {
return n.number == note && n.tick + n.duration > startTick && n.tick < endTick;
});
}
setRenderFlag(dirty) {
this.dirty = dirty;
}
setQuantize(quantize) {
if (this.quantize != quantize) {
this.dirty = true;
}
this.quantize = quantize;
}
needsRender() {
return this.dirty;
}
}
/***/ }),
/***/ "./src/MIDINoteRecorder.ts":
/*!*********************************!*\
!*** ./src/MIDINoteRecorder.ts ***!
\*********************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ MIDINoteRecorder: () => (/* binding */ MIDINoteRecorder)
/* harmony export */ });
/* harmony import */ var _shared_midi__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../shared/midi */ "../shared/midi.ts");
/* harmony import */ var _Clip__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Clip */ "./src/Clip.ts");
class MIDINoteRecorder {
constructor(getClip, addNote) {
this.getClip = getClip;
this.addNote = addNote;
this.states = [];
for (let i = 0; i < 128; i++) {
this.states.push({});
}
this.channel = -1;
}
onMIDI(event, timestamp) {
let isNoteOn = (event[0] & 0xF0) == _shared_midi__WEBPACK_IMPORTED_MODULE_0__.MIDI.NOTE_ON;
let isNoteOff = (event[0] & 0xF0) == _shared_midi__WEBPACK_IMPORTED_MODULE_0__.MIDI.NOTE_OFF;
if ((isNoteOn || isNoteOff) && this.channel != -1 && (event[0] & 0x0f) != this.channel) {
isNoteOn = false;
isNoteOff = false;
}
if (isNoteOn && event[2] == 0) {
isNoteOn = false;
isNoteOff = true;
}
const state = this.states[event[1]];
const tick = this.getTick(timestamp);
if (isNoteOff && state.onTick !== undefined) {
this.finalizeNote(event[1], tick);
}
if (isNoteOn && state.onTick !== undefined) {
this.finalizeNote(event[1], tick);
}
if (isNoteOn) {
this.states[event[1]] = {
onTick: tick,
onVelocity: event[2]
};
}
}
finalizeAllNotes(finalTick) {
for (let i = 0; i < 128; i++) {
if (this.states[i].onTick !== undefined) {
this.finalizeNote(i, finalTick);
}
}
}
finalizeNote(note, tick) {
const state = this.states[note];
if (tick > state.onTick) {
this.addNote(state.onTick, note, tick - state.onTick, state.onVelocity);
}
this.states[note] = {};
}
getTick(timestamp) {
var timeElapsed = timestamp - this.transportData.currentBarStarted;
var beatPosition = (this.transportData.currentBar * this.transportData.timeSigNumerator) + ((this.transportData.tempo / 60.0) * timeElapsed);
var tickPosition = Math.floor(beatPosition * _Clip__WEBPACK_IMPORTED_MODULE_1__.PPQN);
return tickPosition % this.getClip().state.length;
}
}
/***/ }),
/***/ "../shared/midi.ts":
/*!*************************!*\
!*** ../shared/midi.ts ***!
\*************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ MIDI: () => (/* binding */ MIDI)
/* harmony export */ });
class MIDI {
}
MIDI.NOTE_ON = 0x90;
MIDI.NOTE_OFF = 0x80;
MIDI.CC = 0xB0;
/***/ }),
/***/ "../shared/util.ts":
/*!*************************!*\
!*** ../shared/util.ts ***!
\*************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ constantSource: () => (/* binding */ constantSource),
/* harmony export */ noiseSource: () => (/* binding */ noiseSource),
/* harmony export */ token: () => (/* binding */ token)
/* harmony export */ });
function token() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 16);
}
function constantSource(audioContext) {
if (audioContext.createConstantSource) {
let source = audioContext.createConstantSource();
source.start();
return source;
}
else {
let length = audioContext.sampleRate;
var buffer = audioContext.createBuffer(1, length, audioContext.sampleRate);
var noise = buffer.getChannelData(0);
for (var i = 0; i < length; i++) {
noise[i] = 1.0;
}
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.loop = true;
source.loopStart = 0.0;
source.loopEnd = 0.9;
source.start();
return source;
}
}
function noiseSource(audioContext) {
let length = audioContext.sampleRate;
var buffer = audioContext.createBuffer(1, length, audioContext.sampleRate);
var noise = buffer.getChannelData(0);
for (var i = 0; i < length; i++) {
noise[i] = (Math.random() * 2) - 1;
}
var source = audioContext.createBufferSource();
source.buffer = buffer;
source.loop = true;
source.loopStart = 0.0;
source.loopEnd = 0.9;
source.start();
return source;
}
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
/*!***********************************!*\
!*** ./src/PianoRollProcessor.ts ***!
\***********************************/
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ PPQN: () => (/* binding */ PPQN)
/* harmony export */ });
/* harmony import */ var _shared_midi__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../shared/midi */ "../shared/midi.ts");
/* harmony import */ var _Clip__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Clip */ "./src/Clip.ts");
/* harmony import */ var _MIDINoteRecorder__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./MIDINoteRecorder */ "./src/MIDINoteRecorder.ts");
const moduleId = 'com.sequencerParty.pianoRoll';
const PPQN = 24;
const audioWorkletGlobalScope = globalThis;
const ModuleScope = audioWorkletGlobalScope.webAudioModules.getModuleScope(moduleId);
const WamProcessor = ModuleScope.WamProcessor;
class PianoRollProcessor extends WamProcessor {
constructor(options) {
super(options);
const { moduleId, instanceId, } = options.processorOptions;
this.lastTime = null;
this.ticks = -1;
this.clips = new Map();
this.currentClipId = "default";
this.count = 0;
this.isPlaying = false;
this.midiConfig = {
hostRecordingArmed: false,
pluginRecordingArmed: false,
inputMidiChannel: -1,
outputMidiChannel: 0,
};
this.noteRecorder = new _MIDINoteRecorder__WEBPACK_IMPORTED_MODULE_2__.MIDINoteRecorder(() => {
return this.clips.get(this.currentClipId);
}, (tick, number, duration, velocity) => {
super.port.postMessage({ event: "addNote", note: { tick, number, duration, velocity } });
});
super.port.start();
}
_generateWamParameterInfo() {
return {};
}
_process(startSample, endSample, inputs, outputs) {
const { currentTime } = audioWorkletGlobalScope;
if (this.pendingClipChange && this.pendingClipChange.timestamp <= currentTime) {
this.currentClipId = this.pendingClipChange.id;
this.pendingClipChange = undefined;
}
let clip = this.clips.get(this.currentClipId);
if (!clip) {
return;
}
if (!this.transportData) {
return;
}
var schedulerTime = currentTime + 0.05;
if (!this.isPlaying && this.transportData.playing && this.transportData.currentBarStarted <= currentTime) {
this.isPlaying = true;
this.startingTicks = ((this.transportData.currentBar * this.transportData.timeSigNumerator) * PPQN);
this.ticks = this.startingTicks - 1;
}
if (!this.transportData.playing && this.isPlaying) {
this.isPlaying = false;
}
if (this.transportData.playing && this.transportData.currentBarStarted <= schedulerTime) {
var timeElapsed = schedulerTime - this.transportData.currentBarStarted;
var beatPosition = (this.transportData.currentBar * this.transportData.timeSigNumerator) + ((this.transportData.tempo / 60.0) * timeElapsed);
var absoluteTickPosition = Math.floor(beatPosition * PPQN);
let clipPosition = absoluteTickPosition % clip.state.length;
if (this.recordingArmed && (this.ticks % clip.state.length) > clipPosition) {
this.noteRecorder.finalizeAllNotes(clip.state.length - 1);
}
let secondsPerTick = 1.0 / ((this.transportData.tempo / 60.0) * PPQN);
while (this.ticks < absoluteTickPosition) {
this.ticks = this.ticks + 1;
const tickMoment = this.transportData.currentBarStarted + ((this.ticks - this.startingTicks) * secondsPerTick);
clip.notesForTick(this.ticks % clip.state.length).forEach(note => {
this.emitEvents({ type: 'wam-midi', time: tickMoment, data: { bytes: [_shared_midi__WEBPACK_IMPORTED_MODULE_0__.MIDI.NOTE_ON | this.midiConfig.outputMidiChannel, note.number, note.velocity] } }, { type: 'wam-midi', time: tickMoment + (note.duration * secondsPerTick) - 0.001, data: { bytes: [_shared_midi__WEBPACK_IMPORTED_MODULE_0__.MIDI.NOTE_OFF | this.midiConfig.outputMidiChannel, note.number, note.velocity] } });
});
}
}
return;
}
async _onMessage(message) {
if (message.data && message.data.action == "clip") {
let clip = new _Clip__WEBPACK_IMPORTED_MODULE_1__.Clip(message.data.id, message.data.state);
this.clips.set(message.data.id, clip);
}
else if (message.data && message.data.action == "play") {
this.pendingClipChange = {
id: message.data.id,
timestamp: 0,
};
}
else if (message.data && message.data.action == "midiConfig") {
const currentlyRecording = this.midiConfig.hostRecordingArmed && this.midiConfig.pluginRecordingArmed;
const stillRecording = message.data.config.hostRecordingArmed && message.data.config.pluginRecordingArmed;
if (currentlyRecording && !stillRecording) {
this.noteRecorder.finalizeAllNotes(this.ticks);
}
this.midiConfig = message.data.config;
this.noteRecorder.channel = this.midiConfig.inputMidiChannel;
}
else {
super._onMessage(message);
}
}
_onTransport(transportData) {
this.transportData = transportData;
this.noteRecorder.transportData = transportData;
this.isPlaying = false;
super.port.postMessage({
event: "transport",
transport: transportData
});
}
_onMidi(midiData) {
var _a;
const { currentTime } = audioWorkletGlobalScope;
const bytes = midiData.bytes;
if (!(this.midiConfig.pluginRecordingArmed && this.midiConfig.hostRecordingArmed)) {
return;
}
if (!((_a = this.transportData) === null || _a === void 0 ? void 0 : _a.playing) || this.transportData.currentBarStarted > currentTime) {
return;
}
this.noteRecorder.onMIDI(bytes, currentTime);
}
}
try {
audioWorkletGlobalScope.registerProcessor(moduleId, PianoRollProcessor);
}
catch (error) {
console.warn(error);
}
})();
/******/ })()
;