csound-wasm
Version:
[](https://badge.fury.io/js/csound-wasm)
238 lines (207 loc) • 8.14 kB
JavaScript
import * as Comlink from 'comlink';
import { copyToFs, lsFs, llFs, readFromFs, rmrfFs, workerMessagePort } from '@root/filesystem';
import { logVAN } from '@root/logger';
import { MAX_HARDWARE_BUFFER_SIZE } from '@root/constants.js';
import { handleCsoundStart, instantiateAudioPacket } from '@root/workers/common.utils';
import libcsoundFactory from '@root/libcsound';
import loadWasm from '@root/module';
import { assoc, pipe } from 'ramda';
let wasm, combined, libraryCsound;
let audioProcessCallback = () => {};
const audioInputs = {
availableFrames: 0,
buffers: [],
inputReadIndex: 0,
inputWriteIndex: 0,
port: undefined,
};
let csoundWorkerFrameRequestPort;
let rtmidiPort;
let rtmidiQueue = [];
const createAudioInputBuffers = (inputsCount) => {
for (let channelIndex = 0; channelIndex < inputsCount; ++channelIndex) {
audioInputs.buffers.push(new Float64Array(MAX_HARDWARE_BUFFER_SIZE));
}
};
const generateAudioFrames = (arguments_) => {
if (workerMessagePort.vanillaWorkerState !== 'realtimePerformanceEnded') {
return audioProcessCallback(arguments_);
}
};
const createRealtimeAudioThread = ({ csound }) => {
if (!wasm || !libraryCsound) {
workerMessagePort.post("error: csound wasn't initialized before starting");
return -1;
}
// The actual realtime start
// doing this early to detect errors
// derive options and attributes for the performance
const startError = libraryCsound.csoundStart(csound);
if (startError !== 0) {
workerMessagePort.post(
'error: csoundStart failed in realtime-performance,' +
' look out for errors in options and syntax',
);
return -1;
}
// Prompt for midi-input on demand
// const isRequestingRtMidiInput = libraryCsound._isRequestingRtMidiInput(csound);
// Prompt for microphone only on demand!
const isExpectingInput = libraryCsound.csoundGetInputName(csound).includes('adc');
// Store Csound AudioParams for upcoming performance
const nchnls = libraryCsound.csoundGetNchnls(csound);
const nchnlsInput = isExpectingInput ? libraryCsound.csoundGetNchnlsInput(csound) : 0;
const zeroDecibelFullScale = libraryCsound.csoundGet0dBFS(csound);
workerMessagePort.broadcastPlayState('realtimePerformanceStarted');
const { buffer } = wasm.exports.memory;
const inputBufferPtr = libraryCsound.csoundGetSpin(csound);
const outputBufferPtr = libraryCsound.csoundGetSpout(csound);
const ksmps = libraryCsound.csoundGetKsmps(csound);
let csoundInputBuffer = new Float64Array(buffer, inputBufferPtr, ksmps * nchnlsInput);
let csoundOutputBuffer = new Float64Array(buffer, outputBufferPtr, ksmps * nchnls);
let lastPerformance = 0;
audioProcessCallback = ({ readIndex, numFrames }) => {
// MEMGROW KILLS REFERENCES!
// https://github.com/emscripten-core/emscripten/issues/6747#issuecomment-400081465
if (csoundInputBuffer.length === 0) {
csoundInputBuffer = new Float64Array(
wasm.exports.memory.buffer,
libraryCsound.csoundGetSpin(csound),
ksmps * nchnlsInput,
);
}
if (csoundOutputBuffer.length === 0) {
csoundOutputBuffer = new Float64Array(
wasm.exports.memory.buffer,
libraryCsound.csoundGetSpout(csound),
ksmps * nchnls,
);
}
const outputAudioPacket = instantiateAudioPacket(nchnls, numFrames);
const hasInput = audioInputs.buffers.length > 0 && audioInputs.availableFrames >= numFrames;
if (rtmidiQueue.length > 0) {
rtmidiQueue.forEach((event) => {
try {
libraryCsound.csoundPushMidiMessage(csound, event[0], event[1], event[2]);
} catch (error) {
console.error(error);
}
});
rtmidiQueue = [];
}
for (let i = 0; i < numFrames; i++) {
const currentCsoundBufferPos = i % ksmps;
if (currentCsoundBufferPos === 0 && lastPerformance === 0) {
lastPerformance = libraryCsound.csoundPerformKsmps(csound);
if (lastPerformance !== 0) {
workerMessagePort.broadcastPlayState('realtimePerformanceEnded');
audioProcessCallback = () => {};
rtmidiQueue = [];
rtmidiPort = undefined;
audioInputs.port = undefined;
csoundWorkerFrameRequestPort = undefined;
return { framesLeft: i };
}
}
outputAudioPacket.forEach((channel, channelIndex) => {
if (csoundOutputBuffer.length > 0) {
channel[i] =
(csoundOutputBuffer[currentCsoundBufferPos * nchnls + channelIndex] || 0) /
zeroDecibelFullScale;
}
});
if (hasInput) {
for (let ii = 0; ii < nchnlsInput; ii++) {
csoundInputBuffer[currentCsoundBufferPos * nchnlsInput + ii] =
(audioInputs.buffers[ii][i + (audioInputs.inputReadIndex % MAX_HARDWARE_BUFFER_SIZE)] ||
0) * zeroDecibelFullScale;
}
}
}
if (hasInput) {
audioInputs.availableFrames -= numFrames;
audioInputs.inputReadIndex += numFrames % MAX_HARDWARE_BUFFER_SIZE;
}
return { audioPacket: outputAudioPacket, framesLeft: 0 };
};
};
const callUncloned = async (k, arguments_) => {
const caller = combined.get(k);
return caller && caller.apply({}, arguments_ || []);
};
addEventListener('message', (event) => {
if (event.data.msg === 'initMessagePort') {
logVAN(`initMessagePort`);
const port = event.ports[0];
workerMessagePort.post = (log) => port.postMessage({ log });
workerMessagePort.broadcastPlayState = (playStateChange) => {
workerMessagePort.vanillaWorkerState = playStateChange;
port.postMessage({ playStateChange });
};
workerMessagePort.ready = true;
} else if (event.data.msg === 'initRequestPort') {
logVAN(`initRequestPort`);
csoundWorkerFrameRequestPort = event.ports[0];
csoundWorkerFrameRequestPort.addEventListener('message', (requestEvent) => {
const { framesLeft = 0, audioPacket } = generateAudioFrames(requestEvent.data) || {};
csoundWorkerFrameRequestPort &&
csoundWorkerFrameRequestPort.postMessage({
numFrames: requestEvent.data.numFrames - framesLeft,
audioPacket,
...requestEvent.data,
});
});
csoundWorkerFrameRequestPort.start();
} else if (event.data.msg === 'initAudioInputPort') {
logVAN(`initAudioInputPort`);
audioInputs.port = event.ports[0];
audioInputs.port.addEventListener('message', ({ data: pkgs }) => {
if (audioInputs.buffers.length === 0) {
createAudioInputBuffers(pkgs.length);
}
audioInputs.buffers.forEach((buf, i) => {
buf.set(pkgs[i], audioInputs.inputWriteIndex);
});
audioInputs.inputWriteIndex += pkgs[0].length;
audioInputs.availableFrames += pkgs[0].length;
if (audioInputs.inputWriteIndex >= MAX_HARDWARE_BUFFER_SIZE) {
audioInputs.inputWriteIndex = 0;
}
});
audioInputs.port.start();
} else if (event.data.msg === 'initRtMidiEventPort') {
logVAN(`initRtMidiEventPort`);
rtmidiPort = event.ports[0];
rtmidiPort.addEventListener('message', ({ data: payload }) => {
rtmidiQueue.push(payload);
});
rtmidiPort.start();
} else if (event.data.playStateChange) {
logVAN(`playStateChange`, event.data.playStateChange.playStateChange);
workerMessagePort.vanillaWorkerState = event.data.playStateChange.playStateChange;
}
});
const initialize = async (wasmDataURI) => {
logVAN(`initializing wasm and exposing csoundAPI functions from worker to main`);
wasm = wasm || (await loadWasm(wasmDataURI));
libraryCsound = libraryCsound || libcsoundFactory(wasm);
const startHandler = handleCsoundStart(
workerMessagePort,
libraryCsound,
createRealtimeAudioThread,
);
const allAPI = pipe(
assoc('copyToFs', copyToFs),
assoc('readFromFs', readFromFs),
assoc('lsFs', lsFs),
assoc('llFs', llFs),
assoc('rmrfFs', rmrfFs),
assoc('csoundStart', startHandler),
assoc('wasm', wasm),
)(libraryCsound);
combined = new Map(Object.entries(allAPI));
};
Comlink.expose({
initialize,
callUncloned,
});