@csound/browser
Version:
[](https://www.npmjs.com/package/@csound/browser) [](h
331 lines (287 loc) • 11.3 kB
JavaScript
/* eslint-disable unicorn/require-post-message-target-origin */
import * as Comlink from "../utils/comlink.js";
import MessagePortState from "../utils/message-port-state";
import { logVANWorker as log } from "../logger";
import { RING_BUFFER_SIZE } from "../constants.js";
import { handleCsoundStart, instantiateAudioPacket, renderFunction } from "./common.utils";
import libcsoundFactory from "../libcsound";
import loadWasm from "../module";
import { clearArray } from "../utils/clear-array";
let combined;
let audioProcessCallback = ({ numFrames /** number */ }) => {};
const rtmidiQueue = [];
const createAudioInputBuffers = (audioInputs, inputsCount) => {
for (let channelIndex = 0; channelIndex < inputsCount; ++channelIndex) {
audioInputs.buffers.push(new Float64Array(RING_BUFFER_SIZE));
}
};
const generateAudioFrames = (arguments_, workerMessagePort) => {
if (workerMessagePort.vanillaWorkerState !== "realtimePerformanceEnded") {
return audioProcessCallback({ numFrames: arguments_["numFrames"] });
}
};
const createRealtimeAudioThread =
({
libraryCsound,
wasm,
wasi,
workerMessagePort,
audioInputs,
inputChannelCount,
outputChannelCount,
sampleRate,
}) =>
(payload) => {
const csound = payload["csound"];
// Prompt for midi-input on demand
// const isRequestingRtMidiInput = libraryCsound._isRequestingRtMidiInput(csound);
// Prompt for microphone only on demand!
const isExpectingInput = libraryCsound.csoundGetInputName(csound).includes("adc");
// Audio params are now set immediately after csoundCreate() to ensure
// they take precedence over CSD file settings
const nchnls = libraryCsound.csoundGetNchnls(csound);
const nchnlsInput =
inputChannelCount > 0
? inputChannelCount
: isExpectingInput
? libraryCsound.csoundGetNchnlsInput(csound)
: 0;
const zeroDecibelFullScale = libraryCsound.csoundGet0dBFS(csound);
// const { buffer } = wasm.wasi.memory;
const inputBufferPtr = libraryCsound.csoundGetSpin(csound);
const outputBufferPtr = libraryCsound.csoundGetSpout(csound);
const ksmps = libraryCsound.csoundGetKsmps(csound);
let csoundInputBuffer = new Float64Array(
wasm.wasi.memory.buffer,
inputBufferPtr,
ksmps * nchnlsInput,
);
let csoundOutputBuffer = new Float64Array(
wasm.wasi.memory.buffer,
outputBufferPtr,
ksmps * nchnls,
);
let lastPerformance = 0;
let currentCsoundBufferPos = 0;
workerMessagePort.broadcastPlayState("realtimePerformanceStarted");
audioProcessCallback = ({ numFrames /** number */ }) => {
const outputAudioPacket = instantiateAudioPacket(nchnls, numFrames);
const hasInput = audioInputs.buffers.length > 0 && audioInputs.availableFrames >= numFrames;
if (rtmidiQueue.length > 0) {
rtmidiQueue.forEach((event) => {
libraryCsound.csoundPushMidiMessage(csound, event[0], event[1], event[2]);
});
clearArray(rtmidiQueue);
}
for (let index = 0; index < numFrames; index++) {
currentCsoundBufferPos = (currentCsoundBufferPos + 1) % ksmps;
if (workerMessagePort.vanillaWorkerState === "realtimePerformanceEnded") {
if (lastPerformance === 0) {
libraryCsound.csoundStop(csound);
lastPerformance = libraryCsound.csoundPerformKsmps(csound);
}
workerMessagePort.broadcastPlayState("realtimePerformanceEnded");
audioProcessCallback = () => {};
clearArray(rtmidiQueue);
audioInputs.port = undefined;
return { framesLeft: index };
}
if (currentCsoundBufferPos === 0 && lastPerformance === 0) {
lastPerformance = libraryCsound.csoundPerformKsmps(csound);
if (lastPerformance !== 0) {
workerMessagePort.broadcastPlayState("realtimePerformanceEnded");
audioProcessCallback = () => {};
clearArray(rtmidiQueue);
audioInputs.port = undefined;
return { framesLeft: index };
}
}
// MEMGROW KILLS REFERENCES!
// https://github.com/emscripten-core/emscripten/issues/6747#issuecomment-400081465
if (csoundInputBuffer.length === 0) {
csoundInputBuffer = new Float64Array(
wasm.wasi.memory.buffer,
libraryCsound.csoundGetSpin(csound),
ksmps * nchnlsInput,
);
}
if (csoundOutputBuffer.length === 0) {
csoundOutputBuffer = new Float64Array(
wasm.wasi.memory.buffer,
libraryCsound.csoundGetSpout(csound),
ksmps * nchnls,
);
}
outputAudioPacket.forEach((channel, channelIndex) => {
if (csoundOutputBuffer.length > 0) {
channel[index] =
(csoundOutputBuffer[currentCsoundBufferPos * nchnls + channelIndex] || 0) /
zeroDecibelFullScale;
}
});
if (hasInput) {
for (let ii = 0; ii < nchnlsInput; ii++) {
csoundInputBuffer[currentCsoundBufferPos * nchnlsInput + ii] =
(audioInputs.buffers[ii][index + (audioInputs.inputReadIndex % RING_BUFFER_SIZE)] ||
0) * zeroDecibelFullScale;
}
}
}
if (hasInput) {
audioInputs.availableFrames -= numFrames;
audioInputs.inputReadIndex += numFrames % RING_BUFFER_SIZE;
}
return { audioPacket: outputAudioPacket, framesLeft: 0 };
};
};
const callUncloned = async (k, arguments_) => {
const caller = combined.get(k);
return caller && caller.apply({}, arguments_ || []);
};
const initMessagePort = ({ port }) => {
log(`initMessagePort`)();
const workerMessagePort = new MessagePortState();
workerMessagePort.port = port;
workerMessagePort.post = (messageLog) => port.postMessage({ log: messageLog });
workerMessagePort.broadcastPlayState = (playStateChange) => {
workerMessagePort.vanillaWorkerState = playStateChange;
const playStateChangePayload = {};
playStateChangePayload["playStateChange"] = playStateChange;
port.postMessage(playStateChangePayload);
};
workerMessagePort.ready = true;
return workerMessagePort;
};
const initRequestPort = (csoundWorkerFrameRequestPort, workerMessagePort) => {
log(`initRequestPort`)();
csoundWorkerFrameRequestPort.addEventListener("message", (requestEvent) => {
const { framesLeft = 0, audioPacket } =
generateAudioFrames(requestEvent.data, workerMessagePort) || {};
const frameRequestPayload = {};
frameRequestPayload["numFrames"] = requestEvent.data.numFrames - framesLeft;
frameRequestPayload["audioPacket"] = audioPacket;
csoundWorkerFrameRequestPort.postMessage({ ...frameRequestPayload, ...requestEvent.data });
});
csoundWorkerFrameRequestPort.start();
return csoundWorkerFrameRequestPort;
};
const initAudioInputPort = ({ port }) => {
log(`initAudioInputPort`)();
const audioInputs = {
availableFrames: 0,
buffers: [],
inputReadIndex: 0,
inputWriteIndex: 0,
port,
};
audioInputs.port.addEventListener("message", ({ data: pkgs }) => {
if (audioInputs.buffers.length === 0) {
createAudioInputBuffers(audioInputs, pkgs.length);
}
audioInputs.buffers.forEach((buf, index) => {
buf.set(pkgs[index], audioInputs.inputWriteIndex);
});
audioInputs.inputWriteIndex += pkgs[0].length;
audioInputs.availableFrames += pkgs[0].length;
if (audioInputs.inputWriteIndex >= RING_BUFFER_SIZE) {
audioInputs.inputWriteIndex = 0;
}
});
audioInputs.port.start();
return audioInputs;
};
const initRtMidiEventPort = (rtmidiPort) => {
log(`initRtMidiEventPort`)();
rtmidiPort.addEventListener("message", ({ data: payload }) => {
rtmidiQueue.push(payload);
});
rtmidiPort.start();
return rtmidiPort;
};
const initialize = async (payload) => {
const audioInputPort = payload["audioInputPort"];
const inputChannelCount = payload["inputChannelCount"];
const messagePort = payload["messagePort"];
const outputChannelCount = payload["outputChannelCount"];
const requestPort = payload["requestPort"];
const rtmidiPort = payload["rtmidiPort"];
const sampleRate = payload["sampleRate"];
const wasmDataURI = payload["wasmDataURI"];
const wasmTransformerDataURI = payload["wasmTransformerDataURI"];
const withPlugins = payload["withPlugins"] || [];
log(`initializing wasm and exposing csoundAPI functions from worker to main`)();
const workerMessagePort = initMessagePort({ port: messagePort });
const audioInputs = initAudioInputPort({ port: audioInputPort });
initRequestPort(requestPort, workerMessagePort);
initRtMidiEventPort(rtmidiPort);
const [wasm, wasi] = await loadWasm({
wasmDataURI,
wasmTransformerDataURI,
withPlugins,
messagePort: workerMessagePort,
});
wasm.wasi = wasi;
const libraryCsound = libcsoundFactory(wasm);
const startHandler = (_, arguments_) =>
handleCsoundStart(
workerMessagePort,
libraryCsound,
wasi,
createRealtimeAudioThread({
audioInputs,
inputChannelCount,
libraryCsound,
outputChannelCount,
sampleRate,
wasm,
wasi,
workerMessagePort,
}),
renderFunction({
inputChannelCount,
libraryCsound,
outputChannelCount,
wasm,
workerMessagePort,
}),
)(arguments_);
const allAPI = { ...libraryCsound, csoundStart: startHandler, wasm };
combined = new Map(Object.entries(allAPI));
libraryCsound.csoundInitialize(0);
const csoundInstance = libraryCsound.csoundCreate();
// Set audio parameters immediately after creation, before any compilation
// This ensures AudioContext sample rate takes precedence over CSD file settings
if (sampleRate) {
const result = libraryCsound.csoundSetOption(csoundInstance, "--sample-rate=" + sampleRate);
result !== 0 && console.error("csoundSetOption sample-rate failed:", result);
}
if (outputChannelCount) {
const result = libraryCsound.csoundSetOption(csoundInstance, "--nchnls=" + outputChannelCount);
result !== 0 && console.error("csoundSetOption nchnls failed:", result);
}
if (inputChannelCount) {
const result = libraryCsound.csoundSetOption(csoundInstance, "--nchnls_i=" + inputChannelCount);
result !== 0 && console.error("csoundSetOption nchnls_i failed:", result);
}
workerMessagePort.port.addEventListener("message", (event) => {
if (event.data && event.data["newPlayState"]) {
if (event.data["newPlayState"] === "realtimePerformanceEnded") {
libraryCsound.csoundStop(csoundInstance);
if (workerMessagePort.vanillaWorkerState !== "realtimePerformanceEnded") {
libraryCsound.csoundPerformKsmps(csoundInstance);
}
// ping-pong for better timing of events:
// the event is only sent from main but state isn't stored
// until it arrived back
workerMessagePort.broadcastPlayState("realtimePerformanceEnded");
}
workerMessagePort.vanillaWorkerState = event.data["newPlayState"];
}
});
workerMessagePort.port.start();
return csoundInstance;
};
Comlink.expose({
initialize,
callUncloned,
});