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

463 lines (403 loc) 16 kB
/* CsoundScriptProcessor.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 libcsoundFactory from "@root/libcsound"; import loadWasm from "@root/module"; import MessagePortState from "@utils/message-port-state"; import { isEmpty } from "ramda"; import { csoundApiRename, fetchPlugins, makeSingleThreadCallback, stopableStates, } from "@root/utils"; import { messageEventHandler } from "./messages.main"; import { PublicEventAPI } from "@root/events"; import { EventPromises } from "@utils/event-promises"; import { requestMidi } from "@utils/request-midi"; import { initFS, getWorkerFs, rmrfFs, syncWorkerFs } from "@root/filesystem/worker-fs"; import { clearFsLastmods, persistentFilesystem, getModifiedPersistentStorage, syncPersistentStorage, } from "@root/filesystem/persistent-fs"; class ScriptProcessorNodeSingleThread { constructor({ audioContext, inputChannelCount = 1, outputChannelCount = 2 }) { this.publicEvents = new PublicEventAPI(this); this.eventPromises = new EventPromises(); this.audioContext = audioContext; this.onaudioprocess = this.onaudioprocess.bind(this); this.currentPlayState = undefined; this.onPlayStateChange = this.onPlayStateChange.bind(this); this.start = this.start.bind(this); this.stop = this.stop.bind(this); this.pause = this.pause.bind(this); this.resume = this.resume.bind(this); this.wasm = undefined; this.csoundInstance = undefined; this.csoundApi = undefined; this.exportApi = {}; this.spn = audioContext.createScriptProcessor(0, inputChannelCount, outputChannelCount); this.spn.audioContext = audioContext; this.spn.inputChannelCount = inputChannelCount; this.spn.outputChannelCount = outputChannelCount; this.spn.onaudioprocess = this.onaudioprocess; this.node = this.spn; this.exportApi.getNode = async () => this.spn; this.sampleRate = audioContext.sampleRate; // this is the only actual single-thread usecase // so we get away with just forwarding it as if it's form // a message port this.messagePort = new MessagePortState(); this.messagePort.post = (log) => messageEventHandler(this)({ data: { log } }); this.messagePort.ready = true; // imports from original csound-wasm this.running = false; this.started = false; } async terminateInstance() { if (this.spn) { this.spn.disconnect(); delete this.spn; } if (this.audioContext) { if (this.audioContext.state !== "closed") { await this.audioContext.close(); } delete this.audioContext; } if (this.publicEvents) { this.publicEvents.terminateInstance(); delete this.publicEvents; } Object.keys(this.exportApi).forEach((key) => delete this.exportApi[key]); Object.keys(this).forEach((key) => delete this[key]); } async onPlayStateChange(newPlayState) { if (this.currentPlayState === newPlayState) { return; } this.currentPlayState = newPlayState; switch (newPlayState) { case "realtimePerformanceStarted": { this.publicEvents.triggerRealtimePerformanceStarted(this); break; } case "realtimePerformanceEnded": { syncPersistentStorage(getWorkerFs(this.wasmFs)()); clearFsLastmods(); // nuke the "worker" fs to keep same behavior for all this.wasmFs && rmrfFs(this.wasmFs)({}, "/"); this.publicEvents.triggerRealtimePerformanceEnded(this); break; } case "realtimePerformancePaused": { this.publicEvents.triggerRealtimePerformancePaused(this); break; } case "realtimePerformanceResumed": { this.publicEvents.triggerRealtimePerformanceResumed(this); break; } case "renderStarted": { this.publicEvents.triggerRenderStarted(this); break; } case "renderEnded": { syncPersistentStorage(getWorkerFs(this.wasmFs)()); clearFsLastmods(); this.publicEvents.triggerRenderEnded(this); this.wasmFs && rmrfFs(this.wasmFs)({}, "/"); break; } default: { break; } } } async pause() { if (this.started && this.running) { this.running = false; this.onPlayStateChange("realtimePerformancePaused"); } } async resume() { if (this.started && !this.running) { this.running = true; this.onPlayStateChange("realtimePerformanceResumed"); } } async stop() { if (this.started) { this.eventPromises.createStopPromise(); const stopResult = this.csoundApi.csoundStop(this.csoundInstance); await this.eventPromises.waitForStop(); if (this.watcherStdOut) { this.watcherStdOut.close(); delete this.watcherStdOut; } if (this.watcherStdErr) { this.watcherStdErr.close(); delete this.watcherStdErr; } delete this.csoundInputBuffer; delete this.csoundOutputBuffer; delete this.currentPlayState; return stopResult; } } async start() { if (!this.csoundApi) { console.error("starting csound failed because csound instance wasn't created"); return; } if (this.currentPlayState !== "realtimePerformanceStarted") { this.result = 0; this.csoundApi.csoundSetOption(this.csoundInstance, "-odac"); this.csoundApi.csoundSetOption(this.csoundInstance, "-iadc"); this.csoundApi.csoundSetOption(this.csoundInstance, "--sample-rate=" + this.sampleRate); this.nchnls = -1; this.nchnls_i = -1; const ksmps = this.csoundApi.csoundGetKsmps(this.csoundInstance); this.ksmps = ksmps; this.cnt = ksmps; this.nchnls = this.csoundApi.csoundGetNchnls(this.csoundInstance); this.nchnls_i = this.csoundApi.csoundGetNchnlsInput(this.csoundInstance); const outputPointer = this.csoundApi.csoundGetSpout(this.csoundInstance); this.csoundOutputBuffer = new Float64Array( this.wasm.exports.memory.buffer, outputPointer, ksmps * this.nchnls, ); const inputPointer = this.csoundApi.csoundGetSpin(this.csoundInstance); this.csoundInputBuffer = new Float64Array( this.wasm.exports.memory.buffer, inputPointer, ksmps * this.nchnls_i, ); this.zerodBFS = this.csoundApi.csoundGet0dBFS(this.csoundInstance); this.publicEvents.triggerOnAudioNodeCreated(this.spn); this.eventPromises.createStartPromise(); if (!this.watcherStdOut && !this.watcherStdErr) { [this.watcherStdOut, this.watcherStdErr] = initFS(this.wasmFs, this.messagePort); } const startResult = this.csoundApi.csoundStart(this.csoundInstance); if (this.csoundApi._isRequestingRtMidiInput(this.csoundInstance)) { requestMidi({ onMidiMessage: ({ data: event }) => this.csoundApi.csoundPushMidiMessage(this.csoundInstance, event[0], event[1], event[2]), }); } this.running = true; await this.eventPromises.waitForStart(); return startResult; } } async initialize({ wasmDataURI, withPlugins, autoConnect }) { if (!this.plugins && withPlugins && !isEmpty(withPlugins)) { withPlugins = await fetchPlugins(withPlugins); } if (!this.wasm) { [this.wasm, this.wasmFs] = await loadWasm({ wasmDataURI, withPlugins, messagePort: this.messagePort, }); [this.watcherStdOut, this.watcherStdErr] = initFS(this.wasmFs, this.messagePort); } clearFsLastmods(); // libcsound const csoundApi = libcsoundFactory(this.wasm); this.csoundApi = csoundApi; const csoundInstance = await csoundApi.csoundCreate(0); this.csoundInstance = csoundInstance; if (autoConnect) { this.spn.connect(this.audioContext.destination); } this.resetCsound(false); // csoundObj Object.keys(csoundApi).reduce((accumulator, apiName) => { const renamedApiName = csoundApiRename(apiName); accumulator[renamedApiName] = (...arguments_) => { if ( (stopableStates.has(this.currentPlayState) || !this.currentPlayState) && typeof this.wasmFs !== "undefined" ) { const modifiedPersistentStorage = getModifiedPersistentStorage(); syncWorkerFs(this.wasm.exports.memory, this.wasmFs)(undefined, modifiedPersistentStorage); } return makeSingleThreadCallback(csoundInstance, csoundApi[apiName]).apply({}, arguments_); }; accumulator[renamedApiName].toString = csoundApi[apiName].toString; return accumulator; }, this.exportApi); this.exportApi.pause = this.pause.bind(this); this.exportApi.resume = this.resume.bind(this); this.exportApi.start = this.start.bind(this); this.exportApi.stop = this.stop.bind(this); this.exportApi.terminateInstance = this.terminateInstance.bind(this); this.exportApi.getAudioContext = async () => this.audioContext; this.exportApi.name = "Csound: ScriptProcessor Node, Single-threaded"; this.exportApi.fs = persistentFilesystem; this.exportApi = this.publicEvents.decorateAPI(this.exportApi); this.exportApi.reset = () => this.resetCsound(true); // the default message listener this.exportApi.addListener("message", console.log); return this.exportApi; } async resetCsound(callReset) { if ( callReset && this.currentPlayState !== "realtimePerformanceEnded" && this.currentPlayState !== "realtimePerformanceStarted" ) { // reset can't be called until performance has started or ended! return -1; } if (this.currentPlayState === "realtimePerformanceStarted") { this.onPlayStateChange("realtimePerformanceEnded"); } this.running = false; this.started = false; this.result = 0; const cs = this.csoundInstance; const libraryCsound = this.csoundApi; if (callReset) { libraryCsound.csoundReset(cs); } // FIXME: // libraryCsound.csoundSetMidiCallbacks(cs); if (!this.watcherStdOut && !this.watcherStdErr) { [this.watcherStdOut, this.watcherStdErr] = initFS(this.wasmFs, this.messagePort); } libraryCsound.csoundSetOption(cs, "-odac"); libraryCsound.csoundSetOption(cs, "-iadc"); libraryCsound.csoundSetOption(cs, "--sample-rate=" + this.sampleRate); this.nchnls = -1; this.nchnls_i = -1; delete this.csoundOutputBuffer; } onaudioprocess(event) { if (this.csoundOutputBuffer === null || this.running === false) { const output = event.outputBuffer; const bufferLength = output.getChannelData(0).length; for (let index = 0; index < bufferLength; index++) { for (let channel = 0; channel < output.numberOfChannels; channel++) { const outputChannel = output.getChannelData(channel); outputChannel[index] = 0; } } return; } if (this.running && !this.started) { this.started = true; this.onPlayStateChange("realtimePerformanceStarted"); this.eventPromises && this.eventPromises.releaseStartPromises(); } const input = event.inputBuffer; const output = event.outputBuffer; const bufferLength = output.getChannelData(0).length; let csOut = this.csoundOutputBuffer; let csIn = this.csoundInputBuffer; const ksmps = this.ksmps; const zerodBFS = this.zerodBFS; const nchnls = this.nchnls; const nchnlsIn = this.nchnls_i; let cnt = this.cnt || 0; let result = this.result || 0; for (let index = 0; index < bufferLength; index++, cnt++) { if (cnt === ksmps && result === 0) { // if we need more samples from Csound result = this.csoundApi.csoundPerformKsmps(this.csoundInstance); cnt = 0; if (result !== 0) { this.running = false; this.started = false; this.onPlayStateChange("realtimePerformanceEnded"); this.eventPromises && this.eventPromises.releaseStopPromises(); } } /* Check if MEMGROWTH occured from csoundPerformKsmps or otherwise. If so, rest output ant input buffers to new pointer locations. */ if (csOut.length === 0) { csOut = this.csoundOutputBuffer = new Float64Array( this.wasm.exports.memory.buffer, this.csoundApi.csoundGetSpout(this.csoundInstance), ksmps * nchnls, ); } if (csIn.length === 0) { csIn = this.csoundInputBuffer = new Float64Array( this.wasm.exports.memory.buffer, this.csoundApi.csoundGetSpin(this.csoundInstance), 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.numberOfChannels); for (let channel = 0; channel < inputChanMax; channel++) { const inputChannel = input.getChannelData(channel); csIn[cnt * nchnlsIn + channel] = inputChannel[index] * zerodBFS; } // Output 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.numberOfChannels) { for (let channel = 0; channel < output.numberOfChannels; channel++) { const outputChannel = output.getChannelData(channel); if (result === 0) outputChannel[index] = csOut[cnt * nchnls + channel] / zerodBFS; else outputChannel[index] = 0; } } else if (this.nchnls === 2 && output.numberOfChannels === 1) { const outputChannel = output.getChannelData(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.numberOfChannels === 2) { const outChan0 = output.getChannelData(0); const outChan1 = output.getChannelData(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 } // for (let channel = 0; channel < input.numberOfChannels; channel++) { // const inputChannel = input.getChannelData(channel); // csIn[cnt * nchnls_i + channel] = inputChannel[i] * zerodBFS; // } // for (let channel = 0; channel < output.numberOfChannels; channel++) { // const outputChannel = output.getChannelData(channel); // if (result == 0) outputChannel[i] = csOut[cnt * nchnls + channel] / zerodBFS; // else outputChannel[i] = 0; // } } this.cnt = cnt; this.result = result; } } export default ScriptProcessorNodeSingleThread;