UNPKG

@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

363 lines (312 loc) 12 kB
import * as Comlink from "../utils/comlink.js"; import { logVANMain as log } from "../logger"; import { api as API } from "../libcsound"; import { csoundApiRename, fetchPlugins, makeProxyCallback, stopableStates } from "../utils"; import { IPCMessagePorts, messageEventHandler } from "./messages.main"; import { EventPromises } from "../utils/event-promises"; import { PublicEventAPI } from "../events"; import VanillaWorker from "../../dist/__compiled.vanilla.worker.inline.js"; class VanillaWorkerMainThread { constructor({ audioContext, audioWorker, audioContextIsProvided, inputChannelCount, outputChannelCount, }) { this.ipcMessagePorts = new IPCMessagePorts(); this.eventPromises = new EventPromises(); this.publicEvents = new PublicEventAPI(this); audioWorker.ipcMessagePorts = this.ipcMessagePorts; audioWorker.csoundWorkerMain = this; audioWorker.publicEvents = this.publicEvents; this.audioWorker = audioWorker; this.audioContextIsProvided = audioContextIsProvided; // Always extract sample rate from audioContext to ensure Csound matches it if (audioContext) { this.sampleRate = audioContext.sampleRate; } if (inputChannelCount) { this.inputChannelCount = inputChannelCount; } if (outputChannelCount) { this.outputChannelCount = outputChannelCount; } /** * @suppress {checkTypes} * @type {CsoundObj} */ this.exportApi = {}; this.csoundInstance = undefined; this.currentPlayState = undefined; this.proxyPort = undefined; this.csoundWorker = undefined; this.midiPortStarted = false; this.onPlayStateChange = this.onPlayStateChange.bind(this); } async terminateInstance() { if (this.csoundWorker) { this.csoundWorker.terminate(); delete this.csoundWorker; } if (this.audioWorker && this.audioWorker.terminateInstance) { await this.audioWorker.terminateInstance(); delete this.audioWorker.terminateInstance; } if (this.proxyPort) { this.proxyPort[Comlink.releaseProxy](); delete this.proxyPort; } if (this.publicEvents) { this.publicEvents.terminateInstance(); } } get api() { return this.exportApi; } handleMidiInput({ data: payload }) { this.ipcMessagePorts.csoundMainRtMidiPort.postMessage && this.ipcMessagePorts.csoundMainRtMidiPort.postMessage(payload); } async prepareRealtimePerformance() { if (!this.csoundInstance) { console.error(`fatal error: csound instance not found?`); return; } this.audioWorker.sampleRate = await this.exportApi.getSr(this.csoundInstance); const inputName = await this.exportApi.getInputName(this.csoundInstance); this.audioWorker.isRequestingInput = inputName.includes("adc"); this.audioWorker["isRequestingMidi"] = await this.exportApi["_isRequestingRtMidiInput"]( this.csoundInstance, ); this.audioWorker.outputsCount = await this.exportApi.getNchnls(this.csoundInstance); // TODO fix upstream: await this.exportApi.csoundGetNchnlsInput(this.csound); this.audioWorker.inputsCount = this.audioWorker.isRequestingInput ? 1 : 0; // if (this.audioWorker.scriptProcessorNode) { // this.audioWorker.softwareBufferSize *= 2; // } log(`vars for rtPerf set`)(); } async onPlayStateChange(newPlayState) { if (!this.publicEvents) { // prevent error after termination return; } this.currentPlayState = newPlayState; switch (newPlayState) { case "realtimePerformanceStarted": { log(`event: realtimePerformanceStarted from worker, now preparingRT..`)(); await this.prepareRealtimePerformance(); break; } case "realtimePerformanceEnded": { log(`event: realtimePerformanceEnded`)(); // a noop if the stop promise already exists this.eventPromises.createStopPromise(); this.midiPortStarted = false; this.publicEvents.triggerRealtimePerformanceEnded(); await this.eventPromises.releaseStopPromise(); break; } case "renderStarted": { await this.eventPromises.releaseStartPromise(); this.publicEvents.triggerRenderStarted(); break; } case "renderEnded": { log(`event: renderEnded received, beginning cleanup`)(); this.publicEvents.triggerRenderEnded(); await this.eventPromises.releaseStopPromise(); break; } default: { break; } } // forward the message from worker to the audioWorker if (!this.audioWorker.ipcMessagePorts) { this.audioWorker.ipcMessagePorts = this.ipcMessagePorts; } await this.audioWorker.onPlayStateChange(newPlayState); } async csoundPause() { if (this.eventPromises.isWaiting("pause")) { return -1; } else { this.eventPromises.createPausePromise(); this.audioWorker && this.audioWorker.workletProxy !== undefined ? await this.audioWorker.workletProxy["pause"]() : await this.audioWorker.onPlayStateChange("realtimePerformancePaused"); await this.eventPromises.waitForPause(); return 0; } } async csoundResume() { if (this.eventPromises.isWaiting("resume")) { return -1; } else { this.eventPromises.createResumePromise(); this.audioWorker && this.audioWorker.workletProxy !== undefined ? await this.audioWorker.workletProxy["resume"]() : await this.audioWorker.onPlayStateChange("realtimePerformanceResumed"); await this.eventPromises.waitForResume(); return 0; } } async initialize({ wasmDataURI, withPlugins }) { const wasmBytes = wasmDataURI(); if (typeof this.audioWorker.initIframe === "function") { await this.audioWorker.initIframe(); } if (withPlugins && withPlugins.length > 0) { withPlugins = await fetchPlugins(withPlugins); } log(`vanilla.main: initialize`)(); this.csoundWorker = this.csoundWorker || new Worker(VanillaWorker()); this.ipcMessagePorts.mainMessagePort.addEventListener("message", messageEventHandler(this)); this.ipcMessagePorts.mainMessagePort2.addEventListener("message", messageEventHandler(this)); this.ipcMessagePorts.mainMessagePort.start(); const proxyPort = Comlink.wrap(this.csoundWorker, undefined); this.proxyPort = proxyPort; const initializePayload = {}; initializePayload["wasmDataURI"] = wasmBytes; initializePayload["messagePort"] = this.ipcMessagePorts.workerMessagePort; initializePayload["requestPort"] = this.ipcMessagePorts.csoundWorkerFrameRequestPort; initializePayload["audioInputPort"] = this.ipcMessagePorts.csoundWorkerAudioInputPort; initializePayload["rtmidiPort"] = this.ipcMessagePorts.csoundWorkerRtMidiPort; // these values are only set if the user provided them // during init or by passing audioContext initializePayload["sampleRate"] = this.sampleRate; initializePayload["inputChannelCount"] = this.inputChannelCount; initializePayload["outputChannelCount"] = this.outputChannelCount; initializePayload["withPlugins"] = withPlugins; this.csoundInstance = await proxyPort["initialize"]( Comlink.transfer(initializePayload, [ wasmBytes, this.ipcMessagePorts.workerMessagePort, this.ipcMessagePorts.csoundWorkerFrameRequestPort, this.ipcMessagePorts.csoundWorkerAudioInputPort, this.ipcMessagePorts.csoundWorkerRtMidiPort, ]), ); this.exportApi["pause"] = this.csoundPause.bind(this); this.exportApi["resume"] = this.csoundResume.bind(this); this.exportApi["terminateInstance"] = this.terminateInstance.bind(this); this.exportApi["getAudioContext"] = async () => this.audioWorker.audioContext; this.exportApi["getNode"] = async () => { const maybeNode = this.audioWorker.audioWorkletNode; if (maybeNode) { return maybeNode; } else { const node = await new Promise((resolve) => { this.exportApi["once"]("onAudioNodeCreated", resolve); }); return node; } }; this.exportApi = this.publicEvents.decorateAPI(this.exportApi); this.exportApi["enableAudioInput"] = () => console.warn( `enableAudioInput was ignored: please use -iadc option before calling start with useWorker=true`, ); this.exportApi["name"] = "Csound: Audio Worklet, Worker"; // the default message listener this.exportApi["addListener"]("message", console.log); for (const apiK of Object.keys(API)) { const reference = API[apiK]; const proxyCallback = makeProxyCallback( proxyPort, this.csoundInstance, apiK, this.currentPlayState, ); switch (apiK) { case "csoundCreate": { break; } case "csoundStart": { const csoundStart = async function () { if (this.eventPromises.isWaiting("start")) { return -1; } else { this.eventPromises.createStartPromise(); const startPayload = {}; startPayload["csound"] = this.csoundInstance; const startResult = await proxyCallback(startPayload); await this.eventPromises.waitForStart(); return startResult; } }; csoundStart["toString"] = () => reference["toString"](); this.exportApi.start = csoundStart.bind(this); break; } case "csoundStop": { const csoundStop = async function () { if (this.eventPromises.isWaiting("stop")) { return -1; } else { this.eventPromises.createStopPromise(); const stopMessagePayload = {}; stopMessagePayload["newPlayState"] = this.currentPlayState === "renderStarted" ? "renderEnded" : "realtimePerformanceEnded"; this.ipcMessagePorts.mainMessagePort.postMessage(stopMessagePayload); await this.eventPromises.waitForStop(); return 0; } }; this.exportApi.stop = csoundStop.bind(this); csoundStop["toString"] = reference["toString"]; break; } case "csoundReset": { const csoundReset = async () => { // no start = noReset if (!this.currentPlayState) { return; } if (this.eventPromises.isWaiting("reset")) { return -1; } else { if (stopableStates.has(this.currentPlayState)) { await this.exportApi.stop(); } const resetResult = await proxyCallback([]); if (!this.audioContextIsProvided) { await this.audioWorker.terminateInstance(); delete this.audioWorker.audioContext; } this.ipcMessagePorts.restartAudioWorkerPorts(); return resetResult; } }; this.exportApi.reset = csoundReset.bind(this); csoundReset["toString"] = reference["toString"]; break; } case "fs": { this.exportApi["fs"] = {}; Object.keys(reference).forEach((method) => { const proxyFsCallback = makeProxyCallback( proxyPort, this.csoundInstance, method, this.currentPlayState, ); proxyFsCallback["toString"] = reference[method]["toString"]; this.exportApi["fs"][method] = proxyFsCallback; }); break; } default: { proxyCallback["toString"] = reference["toString"]; this.exportApi[csoundApiRename(apiK)] = proxyCallback; break; } } } log(`exportAPI generated`)(); } } export default VanillaWorkerMainThread;