@doc.e.dub/csound-browser
Version:
[](https://www.npmjs.com/package/@csound/browser) [](h
341 lines (298 loc) • 11.4 kB
JavaScript
/*
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);