UNPKG

@doc.e.dub/csound-browser

Version:

[![npm (scoped with tag)](https://shields.shivering-isles.com/npm/v/@csound/browser/latest)](https://www.npmjs.com/package/@csound/browser) [![GitHub Workflow Status](https://shields.shivering-isles.com/github/workflow/status/csound/csound/csound_wasm)](h

341 lines (298 loc) 11.4 kB
/* worklet.singlethread.worker.js Copyright (C) 2018 Steven Yi, Victor Lazzarini This file is part of Csound. The Csound Library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. Csound is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Csound; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ import * as Comlink from "comlink"; import MessagePortState from "@utils/message-port-state"; import { getWorkerFs, syncWorkerFs } from "@root/filesystem/worker-fs"; import libcsoundFactory from "@root/libcsound"; import loadWasm from "@root/module"; import { assoc, pipe } from "ramda"; import { clearArray } from "@utils/clear-array"; import { logSinglethreadWorkletWorker as log } from "@root/logger"; let libraryCsound; let combined; const rtmidiQueue = []; const callUncloned = async (k, arguments_) => { const caller = combined.get(k); const returnValue = caller && caller.apply({}, arguments_ || []); return returnValue; }; class WorkletSinglethreadWorker extends AudioWorkletProcessor { static get parameterDescriptors() { return []; } constructor(options) { super(options); // eslint-disable-next-line no-undef this.sampleRate = sampleRate; this.options = options; this.initialize = this.initialize.bind(this); this.pause = this.pause.bind(this); this.process = this.process.bind(this); this.resume = this.resume.bind(this); this.start = this.start.bind(this); this.needsStartNotification = false; this.isPaused = false; this.callUncloned = () => console.error("Csound worklet thread is still uninitialized!"); this.port.start(); Comlink.expose(this, this.port); this.workerMessagePort = new MessagePortState(); this.initializeMessagePort = ({ messagePort, rtmidiPort }) => { this.workerMessagePort.post = (messageLog) => messagePort.postMessage({ log: messageLog }); this.workerMessagePort.broadcastPlayState = (playStateChange) => { if (this.workerMessagePort.workerState !== playStateChange) { this.workerMessagePort.workerState = playStateChange; } messagePort.postMessage({ playStateChange }); }; this.workerMessagePort.ready = true; log(`initRtMidiEventPort`)(); this.rtmidiPort = rtmidiPort; this.rtmidiPort.addEventListener("message", ({ data: payload }) => { rtmidiQueue.push(payload); }); this.rtmidiPort.start(); }; } async initialize(wasmDataURI, withPlugins) { log("initializing worklet.singlethread.worker")(); let resolver; const waiter = new Promise((resolve) => { resolver = resolve; }); loadWasm({ wasmDataURI, withPlugins, messagePort: this.workerMessagePort }).then( ([wasm, wasmFs]) => { this.wasm = wasm; this.wasmFs = wasmFs; libraryCsound = libcsoundFactory(wasm); this.callUncloned = callUncloned; this.csound = libraryCsound.csoundCreate(0); this.result = 0; this.running = false; this.started = false; this.resetCsound(false); const csoundCreate = async (v) => { return this.csound; }; const allAPI = pipe( assoc("getWorkerFs", getWorkerFs(wasmFs)), assoc("syncWorkerFs", syncWorkerFs(wasm.exports.memory, wasmFs)), assoc("csoundCreate", csoundCreate), assoc("csoundReset", this.resetCsound.bind(this)), assoc("csoundStart", this.start.bind(this)), assoc("csoundStop", this.stop.bind(this)), assoc("wasm", wasm), )(libraryCsound); combined = new Map(Object.entries(allAPI)); log("wasm initialized and api generated")(); resolver(); }, ); log("waiting on wasm initialization to complete")(); await waiter; } async resetCsound(callReset) { // no start = no reset if (callReset && !this.workerMessagePort) { return -1; } if ( callReset && this.workerMessagePort.workerState !== "realtimePerformanceEnded" && this.workerMessagePort.workerState !== "realtimePerformanceStarted" ) { // reset can't be called until performance has started or ended! return -1; } if (callReset && this.workerMessagePort.workerState === "realtimePerformanceStarted") { this.workerMessagePort.broadcastPlayState("realtimePerformanceEnded"); } this.running = false; this.started = false; this.result = 0; const cs = this.csound; if (callReset) { libraryCsound.csoundReset(cs); } libraryCsound.csoundSetMidiCallbacks(cs); libraryCsound.csoundSetOption(cs, "-iadc"); libraryCsound.csoundSetOption(cs, "-odac"); libraryCsound.csoundSetOption(cs, "--sample-rate=" + this.sampleRate); this.nchnls = -1; this.nchnls_i = -1; delete this.csoundOutputBuffer; } stop() { this.workerMessagePort.broadcastPlayState("realtimePerformanceEnded"); if (this.csound) { libraryCsound.csoundStop(this.csound); } } pause() { if (!this.isPaused) { this.workerMessagePort.broadcastPlayState("realtimePerformancePaused"); this.isPaused = true; } } resume() { if (this.isPaused) { this.workerMessagePort.broadcastPlayState("realtimePerformanceResumed"); this.isPaused = false; } } process(inputs, outputs) { if (this.isPaused || !this.csoundOutputBuffer || !this.running) { const output = outputs[0]; const bufferLength = output[0].length; for (let index = 0; index < bufferLength; index++) { for (let channel = 0; channel < output.numberOfChannels; channel++) { const outputChannel = output[channel]; outputChannel[index] = 0; } } return true; } // if we are starting, we need to bordcast it // this late in order to avoid timing issues if (this.needsStartNotification) { this.needsStartNotification = false; this.workerMessagePort.broadcastPlayState("realtimePerformanceStarted"); } if (rtmidiQueue.length > 0) { rtmidiQueue.forEach((event) => { libraryCsound.csoundPushMidiMessage(this.csound, event[0], event[1], event[2]); }); clearArray(rtmidiQueue); } const input = inputs[0]; const output = outputs[0]; const bufferLength = output[0].length; let csOut = this.csoundOutputBuffer; let csIn = this.csoundInputBuffer; const ksmps = this.ksmps; const zerodBFS = this.zerodBFS; let cnt = this.cnt; const nchnls = this.nchnls; const nchnlsIn = this.nchnls_i; let result = this.result; for (let index = 0; index < bufferLength; index++, cnt++) { if (cnt >= ksmps && result === 0) { // if we need more samples from Csound result = libraryCsound.csoundPerformKsmps(this.csound); cnt = 0; if (result !== 0) { this.running = false; this.started = false; libraryCsound.csoundCleanup(this.csound); this.workerMessagePort.broadcastPlayState("realtimePerformanceEnded"); // if (this.stopPromiz) { // this.stopPromiz(); // delete this.stopPromiz; // } } } /* Check if MEMGROWTH occured from csoundPerformKsmps or otherwise. If so, rest output ant input buffers to new pointer locations. */ if (!csOut || csOut.length === 0) { csOut = this.csoundOutputBuffer = new Float64Array( this.wasm.exports.memory.buffer, libraryCsound.csoundGetSpout(this.csound), ksmps * nchnls, ); } if (!csIn || csIn.length === 0) { csIn = this.csoundInputBuffer = new Float64Array( this.wasm.exports.memory.buffer, libraryCsound.csoundGetSpin(this.csound), ksmps * nchnlsIn, ); } // handle 1->1, 1->2, 2->1, 2->2 input channel count mixing and nchnls_i const inputChanMax = Math.min(this.nchnls_i, input.length); for (let channel = 0; channel < inputChanMax; channel++) { const inputChannel = input[channel]; csIn[cnt * nchnlsIn + channel] = inputChannel[index] * zerodBFS; } // Channel mixing matches behavior of: // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Basic_concepts_behind_Web_Audio_API#Up-mixing_and_down-mixing // handle 1->1, 1->2, 2->1, 2->2 output channel count mixing and nchnls if (this.nchnls === output.length) { for (const [channel, outputChannel] of output.entries()) { if (result === 0) outputChannel[index] = csOut[cnt * nchnls + channel] / zerodBFS; else outputChannel[index] = 0; } } else if (this.nchnls === 2 && output.length === 1) { const outputChannel = output[0]; if (result === 0) { const left = csOut[cnt * nchnls] / zerodBFS; const right = csOut[cnt * nchnls + 1] / zerodBFS; outputChannel[index] = 0.5 * (left + right); } else { outputChannel[index] = 0; } } else if (this.nchnls === 1 && output.length === 2) { const outChan0 = output[0]; const outChan1 = output[1]; if (result === 0) { const value = csOut[cnt * nchnls] / zerodBFS; outChan0[index] = value; outChan1[index] = value; } else { outChan0[index] = 0; outChan1[index] = 0; } } else { // FIXME: we do not support other cases at this time } } this.cnt = cnt; this.result = result; return true; } async start() { let returnValueValue = -1; if (!this.started) { log("worklet thread is starting..")(); const cs = this.csound; const ksmps = libraryCsound.csoundGetKsmps(cs); this.ksmps = ksmps; this.cnt = ksmps; this.nchnls = libraryCsound.csoundGetNchnls(cs); this.nchnls_i = libraryCsound.csoundGetNchnlsInput(cs); this.zerodBFS = libraryCsound.csoundGet0dBFS(cs); this.csoundOutputBuffer = new Float64Array( this.wasm.exports.memory.buffer, libraryCsound.csoundGetSpout(cs), ksmps * this.nchnls, ); this.csoundInputBuffer = new Float64Array( this.wasm.exports.memory.buffer, libraryCsound.csoundGetSpin(cs), ksmps * this.nchnls_i, ); returnValueValue = libraryCsound.csoundStart(cs); log("csoundStart called with {} return val", returnValueValue)(); this.started = true; this.needsStartNotification = true; } else { log("worklet was asked to start but it already has!")(); } this.running = true; return returnValueValue; } } registerProcessor("csound-singlethread-worklet-processor", WorkletSinglethreadWorker);