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

558 lines (491 loc) 20.1 kB
import * as Comlink from "../utils/comlink.js"; import { api as API } from "../libcsound"; import { messageEventHandler, IPCMessagePorts } from "./messages.main"; import { AUDIO_STATE, MAX_CHANNELS, RING_BUFFER_SIZE, MIDI_BUFFER_PAYLOAD_SIZE, MIDI_BUFFER_SIZE, initialSharedState, } from "../constants"; import { logSABMain as log } from "../logger"; import { csoundApiRename, fetchPlugins, makeProxyCallback, stopableStates } from "../utils"; import { EventPromises } from "../utils/event-promises"; import { PublicEventAPI } from "../events"; import SABWorker from "../../dist/__compiled.sab.worker.inline.js"; class SharedArrayBufferMainThread { constructor({ audioContext, audioWorker, audioContextIsProvided, inputChannelCount, outputChannelCount, }) { this.csoundInstance = undefined; this.currentPlayState = undefined; this.wasmTransformerDataURI = undefined; this.wasmTransformerDataURI = undefined; /** * @type {SABMainProxy} * @suppress {checkTypes} */ this.proxyPort = undefined; this.csoundWorker = undefined; this.hasSharedArrayBuffer = true; this.ipcMessagePorts = new IPCMessagePorts(); this.eventPromises = new EventPromises(); this.publicEvents = new PublicEventAPI(this); audioWorker.ipcMessagePorts = this.ipcMessagePorts; this.audioContextIsProvided = audioContextIsProvided; this.audioWorker = audioWorker; this.audioWorker.onPlayStateChange = this.audioWorker.onPlayStateChange.bind(audioWorker); this.currentDerivedPlayState = "stop"; this.exportApi = {}; this.callbackId = 0; this.callbackBuffer = {}; this.audioStateBuffer = new SharedArrayBuffer( initialSharedState.length * Int32Array.BYTES_PER_ELEMENT, ); this.audioStatePointer = new Int32Array(this.audioStateBuffer); // Always extract sample rate from audioContext to ensure Csound matches it if (audioContext) { Atomics.store(this.audioStatePointer, AUDIO_STATE.SAMPLE_RATE, audioContext.sampleRate); } if (inputChannelCount) { Atomics.store(this.audioStatePointer, AUDIO_STATE.NCHNLS_I, inputChannelCount); } if (outputChannelCount) { Atomics.store(this.audioStatePointer, AUDIO_STATE.NCHNLS, outputChannelCount); } this.audioStreamIn = new SharedArrayBuffer( MAX_CHANNELS * RING_BUFFER_SIZE * Float64Array.BYTES_PER_ELEMENT, ); this.audioStreamOut = new SharedArrayBuffer( MAX_CHANNELS * RING_BUFFER_SIZE * Float64Array.BYTES_PER_ELEMENT, ); this.midiBufferSAB = new SharedArrayBuffer( MIDI_BUFFER_SIZE * MIDI_BUFFER_PAYLOAD_SIZE * Int32Array.BYTES_PER_ELEMENT, ); this.midiBuffer = new Int32Array(this.midiBufferSAB); this.onPlayStateChange = this.onPlayStateChange.bind(this); this.prepareRealtimePerformance = this.prepareRealtimePerformance.bind(this); log(`SharedArrayBufferMainThread got constructed`)(); } 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: [status, data1, data2] }) { const currentQueueLength = Atomics.load( this.audioStatePointer, AUDIO_STATE.AVAIL_RTMIDI_EVENTS, ); const rtmidiBufferIndex = Atomics.load(this.audioStatePointer, AUDIO_STATE.RTMIDI_INDEX); const nextIndex = (currentQueueLength * MIDI_BUFFER_PAYLOAD_SIZE + rtmidiBufferIndex) % MIDI_BUFFER_SIZE; Atomics.store(this.midiBuffer, nextIndex, status); Atomics.store(this.midiBuffer, nextIndex + 1, data1); Atomics.store(this.midiBuffer, nextIndex + 2, data2); Atomics.add(this.audioStatePointer, AUDIO_STATE.AVAIL_RTMIDI_EVENTS, 1); } async csoundPause() { if (this.eventPromises.isWaiting("pause")) { return -1; } else { this.eventPromises.createPausePromise(); Atomics.store(this.audioStatePointer, AUDIO_STATE.IS_PAUSED, 1); await this.eventPromises.waitForPause(); this.onPlayStateChange("realtimePerformancePaused"); return 0; } } async csoundResume() { if ( Atomics.load(this.audioStatePointer, AUDIO_STATE.IS_PAUSED) === 1 && Atomics.load(this.audioStatePointer, AUDIO_STATE.STOP) !== 1 && Atomics.load(this.audioStatePointer, AUDIO_STATE.IS_PERFORMING) === 1 ) { Atomics.store(this.audioStatePointer, AUDIO_STATE.IS_PAUSED, 0); Atomics.notify(this.audioStatePointer, AUDIO_STATE.IS_PAUSED); this.onPlayStateChange("realtimePerformanceResumed"); } } async onPlayStateChange(newPlayState) { if (this === undefined) { console.log("Failed to announce playstatechange", newPlayState); return; } this.currentPlayState = newPlayState; if (!this.publicEvents || !newPlayState) { // prevent late timers from calling terminated fn return; } switch (newPlayState) { case "realtimePerformanceStarted": { log( `event: realtimePerformanceStarted received,` + ` proceeding to call prepareRealtimePerformance`, )(); try { await this.prepareRealtimePerformance(); } catch (error) { console.error(error); } break; } case "realtimePerformanceEnded": { this.eventPromises.createStopPromise(); // flush out events sent during the time which the worker was stopping Object.values(this.callbackBuffer).forEach((payload) => { this.proxyPort["callUncloned"](payload["apiKey"], payload["argumentz"]).then( payload["resolveCallback"], ); }); this.callbackBuffer = {}; log(`event: realtimePerformanceEnded received, beginning cleanup`)(); // re-initialize SAB initialSharedState.forEach((value, index) => { Atomics.store(this.audioStatePointer, index, value); }); break; } case "renderStarted": { this.publicEvents.triggerRenderStarted(); this.eventPromises.releaseStartPromise(); break; } case "renderEnded": { log(`event: renderEnded received, beginning cleanup`)(); this.publicEvents.triggerRenderEnded(); this.eventPromises && this.eventPromises.releaseStopPromise(); break; } default: { break; } } // forward the message from worker to the audioWorker try { await this.audioWorker.onPlayStateChange(newPlayState); } catch (error) { console.error(error); } } async prepareRealtimePerformance() { log(`prepareRealtimePerformance`)(); const outputsCount = Atomics.load(this.audioStatePointer, AUDIO_STATE.NCHNLS); const inputCount = Atomics.load(this.audioStatePointer, AUDIO_STATE.NCHNLS_I); this.audioWorker.isRequestingInput = Atomics.load( this.audioStatePointer, AUDIO_STATE.IS_REQUESTING_MIC, ); this.audioWorker["isRequestingMidi"] = Atomics.load( this.audioStatePointer, AUDIO_STATE.IS_REQUESTING_RTMIDI, ); const ksmps = Atomics.load(this.audioStatePointer, AUDIO_STATE.KSMPS); const sampleRate = Atomics.load(this.audioStatePointer, AUDIO_STATE.SAMPLE_RATE); this.audioWorker.ksmps = ksmps; this.audioWorker.sampleRate = sampleRate; this.audioWorker.inputCount = inputCount; this.audioWorker.outputsCount = outputsCount; } async initialize({ wasmDataURI, withPlugins }) { if (withPlugins && withPlugins.length > 0) { withPlugins = await fetchPlugins(withPlugins); } log(`initialization: instantiate the SABWorker Thread`)(); const csoundWorker = new Worker(SABWorker()); this.csoundWorker = csoundWorker; const audioStateBuffer = this.audioStateBuffer; const audioStatePointer = this.audioStatePointer; const audioStreamIn = this.audioStreamIn; const audioStreamOut = this.audioStreamOut; const midiBuffer = this.midiBuffer; log(`providing the audioWorker a pointer to SABMain's instance`)(); this.audioWorker.csoundWorkerMain = this; // both audio worker and csound worker use 1 handler // simplifies flow of data (csound main.worker is always first to receive) log(`adding message eventListeners for mainMessagePort and mainMessagePortAudio`)(); this.ipcMessagePorts.mainMessagePort.addEventListener("message", messageEventHandler(this)); this.ipcMessagePorts.mainMessagePort.start(); this.ipcMessagePorts.mainMessagePortAudio.addEventListener( "message", messageEventHandler(this), ); this.ipcMessagePorts.mainMessagePortAudio.start(); log(`(postMessage) making a message channel from SABMain to SABWorker via workerMessagePort`)(); this.ipcMessagePorts.sabMainCallbackReply.addEventListener("message", (event) => { switch (event.data) { case "poll": { if (this.ipcMessagePorts && this.ipcMessagePorts.sabMainCallbackReply) { this.ipcMessagePorts.sabMainCallbackReply.postMessage( Object.keys(this.callbackBuffer).map((id) => { const callbackReplyPayload = {}; callbackReplyPayload["id"] = id; callbackReplyPayload["apiKey"] = this.callbackBuffer[id]["apiKey"]; callbackReplyPayload["argumentz"] = this.callbackBuffer[id]["argumentz"]; return callbackReplyPayload; }), ); } break; } case "releaseStop": { this.onPlayStateChange( this.currentPlayState === "renderStarted" ? "renderEnded" : "realtimePerformanceEnded", ); this.publicEvents && this.publicEvents.triggerRealtimePerformanceEnded(); this.eventPromises && this.eventPromises.releaseStopPromise(); break; } case "releasePause": { this.publicEvents.triggerRealtimePerformancePaused(); this.eventPromises.releasePausePromise(); break; } case "releaseResumed": { this.publicEvents.triggerRealtimePerformanceResumed(); this.eventPromises.releaseResumePromise(); break; } default: { event.data.forEach((payload) => { this.callbackBuffer[payload["id"]]["resolveCallback"](payload["answer"]); delete this.callbackBuffer[payload["id"]]; }); } } }); this.ipcMessagePorts.sabMainCallbackReply.start(); const proxyPort = Comlink.wrap(csoundWorker, undefined); const wasmBytes = wasmDataURI(); this.proxyPort = proxyPort; const initializePayload = {}; initializePayload["wasmDataURI"] = wasmBytes; initializePayload["wasmTransformerDataURI"] = this.wasmTransformerDataURI; initializePayload["messagePort"] = this.ipcMessagePorts.workerMessagePort; initializePayload["callbackPort"] = this.ipcMessagePorts.sabWorkerCallbackReply; initializePayload["withPlugins"] = withPlugins; const csoundInstance = await proxyPort["initialize"]( Comlink.transfer(initializePayload, [ wasmBytes, this.ipcMessagePorts.workerMessagePort, this.ipcMessagePorts.sabWorkerCallbackReply, ]), ); this.csoundInstance = csoundInstance; this.ipcMessagePorts.mainMessagePort.start(); this.ipcMessagePorts.mainMessagePortAudio.start(); log(`A proxy port from SABMain to SABWorker established`)(); this.exportApi["pause"] = this.csoundPause.bind(this); this.exportApi["resume"] = this.csoundResume.bind(this); this.exportApi["terminateInstance"] = this.terminateInstance.bind(this); this.exportApi["enableAudioInput"] = () => console.warn( `enableAudioInput was ignored: please use -iadc option before calling start with useWorker=true`, ); this.exportApi["name"] = "Csound: Audio Worklet, Shared-Array Buffer"; 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["getAudioContext"] = async () => this.audioWorker.audioContext; this.exportApi = this.publicEvents.decorateAPI(this.exportApi); // the default message listener this.exportApi.addListener("message", console.log); for (const apiKey of Object.keys(API)) { const proxyCallback = makeProxyCallback( proxyPort, csoundInstance, apiKey, this.currentPlayState, ); const reference = API[apiKey]; switch (apiKey) { case "csoundCreate": { break; } case "csoundStart": { const csoundStart = async function () { if (!csoundInstance || typeof csoundInstance !== "number") { console.error("starting csound failed because csound instance wasn't created"); return -1; } if (this.eventPromises.isWaiting("start")) { return -1; } else { this.eventPromises.createStartPromise(); const startPayload = {}; startPayload["audioStateBuffer"] = audioStateBuffer; startPayload["audioStreamIn"] = audioStreamIn; startPayload["audioStreamOut"] = audioStreamOut; startPayload["midiBuffer"] = midiBuffer; startPayload["csound"] = csoundInstance; const startResult = await proxyCallback(startPayload); await this.eventPromises.waitForStart(); this.ipcMessagePorts && this.ipcMessagePorts.sabMainCallbackReply.postMessage({ unlock: true }); return startResult; } }; csoundStart["toString"] = () => reference["toString"](); this.exportApi.start = csoundStart.bind(this); break; } case "csoundStop": { const csoundStop = async () => { log( [ "Checking if it's safe to call stop:", stopableStates.has(this.currentPlayState), "currentPlayState is", this.currentPlayState, ].join("\n"), )(); if (this.eventPromises.isWaiting("stop")) { log("already waiting to stop, doing nothing")(); return -1; } else if (stopableStates.has(this.currentPlayState)) { log("Marking SAB's state to STOP")(); this.eventPromises.createStopPromise(); Atomics.store(this.audioStatePointer, AUDIO_STATE.STOP, 1); log("Marking that performance is not running anymore (stops the audio too)")(); Atomics.store(this.audioStatePointer, AUDIO_STATE.IS_PERFORMING, 0); // A potential case where the thread is locked because of pause if (this.currentPlayState === "realtimePerformancePaused") { Atomics.store(this.audioStatePointer, AUDIO_STATE.IS_PAUSED, 0); Atomics.notify(this.audioStatePointer, AUDIO_STATE.IS_PAUSED); } if (this.currentPlayState !== "renderStarted") { !Atomics.compareExchange(this.audioStatePointer, AUDIO_STATE.CSOUND_LOCK, 0, 1) && Atomics.notify(this.audioStatePointer, AUDIO_STATE.CSOUND_LOCK); } await this.eventPromises.waitForStop(); return 0; } else { return -1; } }; 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(); } this.ipcMessagePorts.restartAudioWorkerPorts(); if (!this.audioContextIsProvided) { await this.audioWorker.terminateInstance(); delete this.audioWorker.audioContext; } const resetResult = await proxyCallback([]); return resetResult; } }; this.exportApi.reset = csoundReset.bind(this); csoundReset["toString"] = () => reference["toString"](); break; } case "csoundPushMidiMessage": { const midiMessage = async (status = 0, data1 = 0, data2 = 0) => { this.handleMidiInput({ data: [status, data1, data2] }); }; this.exportApi.midiMessage = midiMessage.bind(this); midiMessage["toString"] = () => reference["toString"](); break; } case "fs": { this.exportApi["fs"] = {}; Object.keys(reference).forEach((method) => { const proxyFsCallback = makeProxyCallback( proxyPort, csoundInstance, method, this.currentPlayState, ); proxyFsCallback["toString"] = () => reference[method]["toString"](); this.exportApi["fs"][method] = proxyFsCallback; }); break; } default: { // avoiding deadlock by sending the IPC callback // while thread is unlocked const bufferWrappedCallback = async (...arguments_) => { if ( this.currentPlayState === "realtimePerformanceStarted" || this.currentPlayState === "renderStarted" || this.eventPromises.isWaitingToStart() // startPromiz indicates that startup is in progress // and any events send before it's resolved are swallowed ) { const callbackId = this.callbackId; this.callbackId += 1; const returnPromise = new Promise((resolve, reject) => { const timeout = setTimeout( () => reject( new Error(`Worker timed out so ${csoundApiRename(apiKey)}() wasn't called!`), ), 10000, ); const resolveCallback = (answer) => { clearTimeout(timeout); resolve(answer); }; const callbackBufferDispatch = {}; callbackBufferDispatch["resolveCallback"] = resolveCallback; callbackBufferDispatch["apiKey"] = apiKey; callbackBufferDispatch["argumentz"] = [csoundInstance, ...arguments_]; this.callbackBuffer[callbackId] = callbackBufferDispatch; }); Atomics.compareExchange(audioStatePointer, AUDIO_STATE.HAS_PENDING_CALLBACKS, 0, 1); return await returnPromise; } else { return await proxyCallback.apply(undefined, arguments_); } }; bufferWrappedCallback["toString"] = () => reference["toString"](); this.exportApi[csoundApiRename(apiKey)] = bufferWrappedCallback; break; } } } log(`PUBLIC API Generated and stored`)(); } } export default SharedArrayBufferMainThread;