sema-engine
Version:
Module containing Sema's signal engine and compiler for a live coding language ecosystem
1,572 lines (1,372 loc) • 158 kB
JavaScript
(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':35730/livereload.js?snipver=1'; r.id = 'livereloadscript'; r.crossOrigin='anonymous'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document);
// Send audio interleaved audio frames between threads, wait-free.
// A Single Producer - Single Consumer thread-safe wait-free ring buffer.
//
// The producer and the consumer can be separate thread, but cannot change role,
// except with external synchronization.
class RingBuffer {
static getStorageForCapacity(capacity, type) {
if (!type.BYTES_PER_ELEMENT) {
throw "Pass in a ArrayBuffer subclass";
}
var bytes = 8 + (capacity + 1) * type.BYTES_PER_ELEMENT;
return new SharedArrayBuffer(bytes);
}
// `sab` is a SharedArrayBuffer with a capacity calculated by calling
// `getStorageForCapacity` with the desired capacity.
constructor(sab, type) {
if (!ArrayBuffer.__proto__.isPrototypeOf(type) &&
type.BYTES_PER_ELEMENT !== undefined) {
throw "Pass a concrete typed array class as second argument";
}
// Maximum usable size is 1<<32 - type.BYTES_PER_ELEMENT bytes in the ring
// buffer for this version, easily changeable.
// -4 for the write ptr (uint32_t offsets)
// -4 for the read ptr (uint32_t offsets)
// capacity counts the empty slot to distinguish between full and empty.
this._type = type;
this.capacity = (sab.byteLength - 8) / type.BYTES_PER_ELEMENT;
this.buf = sab;
this.write_ptr = new Uint32Array(this.buf, 0, 1);
this.read_ptr = new Uint32Array(this.buf, 4, 1);
this.storage = new type(this.buf, 8, this.capacity);
}
// Returns the type of the underlying ArrayBuffer for this RingBuffer. This
// allows implementing crude type checking.
type() {
return this._type.name;
}
// Push bytes to the ring buffer. `bytes` is an typed array of the same type
// as passed in the ctor, to be written to the queue.
// Returns the number of elements written to the queue.
push(elements) {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
if ((wr + 1) % this._storage_capacity() == rd) {
// full
return 0;
}
let to_write = Math.min(this._available_write(rd, wr), elements.length);
let first_part = Math.min(this._storage_capacity() - wr, to_write);
let second_part = to_write - first_part;
this._copy(elements, 0, this.storage, wr, first_part);
this._copy(elements, first_part, this.storage, 0, second_part);
// publish the enqueued data to the other side
Atomics.store(
this.write_ptr,
0,
(wr + to_write) % this._storage_capacity()
);
return to_write;
}
// Read `elements.length` elements from the ring buffer. `elements` is a typed
// array of the same type as passed in the ctor.
// Returns the number of elements read from the queue, they are placed at the
// beginning of the array passed as parameter.
pop(elements) {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
if (wr == rd) {
return 0;
}
let to_read = Math.min(this._available_read(rd, wr), elements.length);
let first_part = Math.min(this._storage_capacity() - rd, elements.length);
let second_part = to_read - first_part;
this._copy(this.storage, rd, elements, 0, first_part);
this._copy(this.storage, 0, elements, first_part, second_part);
Atomics.store(this.read_ptr, 0, (rd + to_read) % this._storage_capacity());
return to_read;
}
// True if the ring buffer is empty false otherwise. This can be late on the
// reader side: it can return true even if something has just been pushed.
empty() {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
return wr == rd;
}
// True if the ring buffer is full, false otherwise. This can be late on the
// write side: it can return true when something has just been poped.
full() {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
return (wr + 1) % this.capacity != rd;
}
// The usable capacity for the ring buffer: the number of elements that can be
// stored.
capacity() {
return this.capacity - 1;
}
// Number of elements available for reading. This can be late, and report less
// elements that is actually in the queue, when something has just been
// enqueued.
available_read() {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
return this._available_read(rd, wr);
}
// Number of elements available for writing. This can be late, and report less
// elemtns that is actually available for writing, when something has just
// been dequeued.
available_write() {
var rd = Atomics.load(this.read_ptr, 0);
var wr = Atomics.load(this.write_ptr, 0);
return this._available_write(rd, wr);
}
// private methods //
// Number of elements available for reading, given a read and write pointer..
_available_read(rd, wr) {
if (wr > rd) {
return wr - rd;
} else {
return wr + this._storage_capacity() - rd;
}
}
// Number of elements available from writing, given a read and write pointer.
_available_write(rd, wr) {
let rv = rd - wr - 1;
if (wr >= rd) {
rv += this._storage_capacity();
}
return rv;
}
// The size of the storage for elements not accounting the space for the index.
_storage_capacity() {
return this.capacity;
}
// Copy `size` elements from `input`, starting at offset `offset_input`, to
// `output`, starting at offset `offset_output`.
_copy(input, offset_input, output, offset_output, size) {
for (var i = 0; i < size; i++) {
output[offset_output + i] = input[offset_input + i];
}
}
}
const getBase64 = (str) => {
//check if the string is a data URI
if (str.indexOf(';base64,') !== -1) {
//see where the actual data begins
var dataStart = str.indexOf(';base64,') + 8;
//check if the data is base64-encoded, if yes, return it
// taken from
// http://stackoverflow.com/a/8571649
return str.slice(dataStart).match(/^([A-Za-z0-9+\/]{4})*([A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)$/) ? str.slice(dataStart) : false;
} else return false;
};
const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const removePaddingFromBase64 = (input) => {
var lkey = Module.maxiTools._keyStr.indexOf(input.charAt(input.length - 1));
if (lkey === 64) {
return input.substring(0, input.length - 1);
}
return input;
};
const loadSampleToArray = (audioContext, sampleObjectName, url, audioWorkletNode) => {
var data = [];
//check if url is actually a base64-encoded string
var b64 = getBase64(url);
if (b64) {
//convert to arraybuffer
//modified version of this:
// https://github.com/danguer/blog-examples/blob/master/js/base64-binary.js
var ab_bytes = (b64.length / 4) * 3;
var arrayBuffer = new ArrayBuffer(ab_bytes);
b64 = removePaddingFromBase64(removePaddingFromBase64(b64));
var bytes = parseInt((b64.length / 4) * 3, 10);
var uarray;
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
var j = 0;
uarray = new Uint8Array(arrayBuffer);
b64 = b64.replace(/[^A-Za-z0-9\+\/\=]/g, "");
for (i = 0; i < bytes; i += 3) {
//get the 3 octects in 4 ascii chars
enc1 = _keyStr.indexOf(b64.charAt(j++));
enc2 = _keyStr.indexOf(b64.charAt(j++));
enc3 = _keyStr.indexOf(b64.charAt(j++));
enc4 = _keyStr.indexOf(b64.charAt(j++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
uarray[i] = chr1;
if (enc3 !== 64) {
uarray[i + 1] = chr2;
}
if (enc4 !== 64) {
uarray[i + 2] = chr3;
}
}
// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
// Asynchronously decodes the audio file data contained in the ArrayBuffer.
audioContext.decodeAudioData(
arrayBuffer, // has its content-type determined by sniffing
function (buffer) { // successCallback, argument is an AudioBuffer representing the decoded PCM audio data.
// source.buffer = buffer;
// source.loop = true;
// source.start(0);
let float32ArrayBuffer = buffer.getChannelData(0);
if (data !== undefined && audioWorkletNode !== undefined) {
// console.log('f32array: ' + float32Array);
audioWorkletNode.port.postMessage({
"sample":sampleObjectName,
"buffer": float32ArrayBuffer,
});
}
},
function (buffer) { // errorCallback
console.log("Error decoding source!");
}
);
} else {
// Load asynchronously
// NOTE: This is giving me an error
// Uncaught ReferenceError: XMLHttpRequest is not defined (index):97 MaxiProcessor Error detected: undefined
// NOTE: followed the trail to the wasmmodule.js
// when loading on if (typeof XMLHttpRequest !== 'undefined') {
// throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers.
// Use --embed-file or --preload-file in emcc on the main thread.");
var request = new XMLHttpRequest();
request.addEventListener("load", () =>
console.info(`loading sample '${sampleObjectName}'`)
);
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = function () {
audioContext.decodeAudioData(
request.response,
function (buffer) {
let float32ArrayBuffer = buffer.getChannelData(0);
if (data !== undefined && audioWorkletNode !== undefined) {
// console.log('f32array: ' + float32Array);
audioWorkletNode.port.postMessage({
"sample":sampleObjectName,
"buffer": float32ArrayBuffer,
});
}
},
function (buffer) {
console.log("Error decoding source!");
}
);
};
request.send();
}
return "Loading module";
};
class Event {
constructor(eventName) {
this.eventName = eventName;
this.callbacks = [];
}
registerCallback(callback) {
this.callbacks.push(callback);
}
unregisterCallback(callback) {
const index = this.callbacks.indexOf(callback);
if (index > -1) {
this.callbacks.splice(index, 1);
}
}
emit(data) {
const callbacks = this.callbacks.slice(0);
callbacks.forEach( callback => {
callback(data);
});
}
}
class Dispatcher {
constructor() {
this.events = {};
}
dispatch(eventName, data) {
// console.log("dispatch is getting called.");
const event = this.events[eventName];
//let event = Event()
// console.log("event", this.events, "data", data);
// console.log("data", data, event);
if (event) {
// console.log("data being emitted", data)
event.emit(data);
}
}
addEventListener(eventName, callback) {
let event = this.events[eventName];
if (!event) {
event = new Event(eventName);
this.events[eventName] = event;
}
event.registerCallback(callback);
}
removeEventListener(eventName, callback) {
const event = this.events[eventName];
if (event && event.callbacks.indexOf(callback) > -1) {
event.unregisterCallback(callback);
if (event.callbacks.length === 0) {
delete this.events[eventName];
}
}
}
}
// NOTE: this imports RingBuffer directly from node_modules
// }
/**
* The Engine is a singleton class that encapsulates the AudioContext
* and all WASM and Maximilian -powered Audio Worklet Processor
* @class AudioEngine
* TODO more error handling
* TODO more checking of arguments passed to methods
* TODO optimise performance, especially on analysers which are pumping data continuously
*/
class Engine {
/**
* @constructor
*/
constructor() {
if (Engine.instance) {
return Engine.instance; // Singleton pattern
}
Engine.instance = this;
this.origin = "";
this.learners = {};
// Hash of on-demand analysers (e.g. spectrogram, oscilloscope)
// NOTE: analysers serialized to localStorage are de-serialized
// and loaded from localStorage before user-triggered audioContext init
this.analysers = {};
this.mediaStreamSource = {};
this.mediaStream = {};
// Shared array buffers for sharing client side data to the audio engine- e.g. mouse coords
this.sharedArrayBuffers = {};
// Event emitter that should be subscribed by SAB receivers
this.dispatcher = new Dispatcher();
this.samplesLoaded = false;
this.isHushed = false;
}
/**
* Add learner instance
*/
async addLearner(id, learner) {
if (learner) {
try {
// `this` is the scope that will
await learner.init(this.origin);
this.addEventListener("onSharedBuffer", (e) =>
learner.addSharedBuffer(e)
); // Engine's SAB emissions subscribed by Learner
learner.addEventListener("onSharedBuffer", (e) =>
this.addSharedBuffer(e)
); // Learner's SAB emissions subscribed by Engine
this.learners[id] = learner;
} catch (error) {
console.error("Error adding Learner to Engine: ", error);
}
} else throw new Error("Error adding Learner instance to Engine");
}
removeLearner(id) {
if (id){
if (this.learners && ( id in this.learners ) ) {
let learner = this.learners[id];
learner.removeEventListener("onSharedBuffer", (e) =>
this.addSharedBuffer(e)
);
learner = null;
delete this.learners[id];
}
// else throw new Error("Error removing Learner from Engine: ");
} else throw new Error("Error with learner ID when removing Learner from Engine");
}
/**
* Engine's event subscription
* @addEventListener
* @param {*} event
* @param {*} callback
*/
addEventListener(event, callback) {
if (this.dispatcher && event && callback)
this.dispatcher.addEventListener(event, callback);
else throw new Error("Error adding event listener to Engine");
}
/* #region SharedBuffers */
/**
* Create a shared array buffer for communicating with the audio engine
* @param channelId
* @param ttype
* @param blocksize
*/
createSharedBuffer(channelId, ttype, blocksize) {
let sab = RingBuffer.getStorageForCapacity(32 * blocksize, Float64Array);
let ringbuf = new RingBuffer(sab, Float64Array);
this.audioWorkletNode.port.postMessage({
func: "sab",
value: sab,
ttype: ttype,
channelID: channelId,
blocksize: blocksize,
});
this.sharedArrayBuffers[channelId] = {
sab: sab, // TODO: this is redundant, you can access the sab from the rb,
// TODO change hashmap name it is confusing and induces error
rb: ringbuf,
};
return sab;
}
/**
* Push data to shared array buffer for communicating with the audio engine and ML worker
* @param {*} e
*/
addSharedBuffer(e) {
if (e) {
if (e.value && e.value instanceof SharedArrayBuffer) {
try {
let ringbuf = new RingBuffer(e.value, Float64Array);
this.audioWorkletNode.port.postMessage({
func: "sab",
value: e.value,
ttype: e.ttype,
channelID: e.channelID,
blocksize: e.blocksize,
});
this.sharedArrayBuffers[e.channelID] = {
sab: e.value, // TODO this is redundant, you can access the sab from the rb,
// TODO also change hashmap name it is confusing and induces error
rb: ringbuf,
};
} catch (err) {
console.error("Error pushing SharedBuffer to engine");
}
} else if (e.name && e.data) {
this.audioWorkletNode.port.postMessage({
func: "sendbuf",
name: e.name,
data: e.data,
});
}
} else throw new Error("Error with onSharedBuffer event");
}
/**
* Push data to shared array buffer for communicating with the audio engine and ML worker
* @param {*} e
* @param {*} channelId
*/
pushDataToSharedBuffer(channelId, data) {
if (channelId && data && typeof Array.isArray(data)) {
if (this.sharedArrayBuffers && this.sharedArrayBuffers[channelId]) {
this.sharedArrayBuffers[channelId].rb.push(data);
}
} else throw new Error("Error in function parameters");
}
/* #region Analysers */
/**
* Polls data from connected WAAPI analyser return structured object with data and time data in arrays
* @param {*} analyser
*/
pollAnalyserData(analyser) {
if (analyser !== undefined) {
const timeDataArray = new Uint8Array(analyser.fftSize); // Uint8Array should be the same length as the fftSize
const frequencyDataArray = new Uint8Array(analyser.fftSize);
analyser.getByteTimeDomainData(timeDataArray);
analyser.getByteFrequencyData(frequencyDataArray);
return {
smoothingTimeConstant: analyser.smoothingTimeConstant,
fftSize: analyser.fftSize,
frequencyDataArray: frequencyDataArray,
timeDataArray: timeDataArray,
};
}
}
/**
* Creates a WAAPI analyser node
* @todo configuration object as argumen
* @createAnalyser
*/
createAnalyser(analyserID, callback) {
// If Analyser creation happens after AudioContext intialization, create and connect WAAPI analyser
if (analyserID && callback){
if(this.audioContext && this.audioWorkletNode ) {
let analyser = this.audioContext.createAnalyser();
analyser.smoothingTimeConstant = 0.25;
analyser.fftSize = 256; // default 2048;
analyser.minDecibels = -90; // default
analyser.maxDecibels = -0; // default -10; max 0
this.audioWorkletNode.connect(analyser);
let analyserFrameId = -1,
analyserData = {};
this.analysers[analyserID] = {
analyser,
analyserFrameId,
callback,
};
/**
* Creates requestAnimationFrame loop for polling data and publishing
* Returns Analyser Frame ID for adding to Analysers hash
* and cancelling animation frame
*/
const analyserPollingLoop = () => {
analyserData = this.pollAnalyserData(
this.analysers[analyserID].analyser
);
this.analysers[analyserID].callback(analyserData); // Invoke callback that carries
// This will guarantee feeding poll request at steady animation framerate
this.analysers[analyserID].analyserFrameId = requestAnimationFrame(
analyserPollingLoop
);
return analyserFrameId;
};
analyserPollingLoop();
// Other if AudioContext is NOT created yet (after app load, before splashScreen click)
} else {
this.analysers[analyserID] = { callback };
}
} else throw new Error('Parameters to createAnalyser incorrect')
}
/**
* Connects WAAPI analyser nodes to the main audio worklet for visualisation.
* @connectAnalysers
*/
connectAnalysers() {
Object.keys(this.analysers).map((id) =>
this.createAnalyser(id, this.analysers[id].callback)
);
}
/**
* Removes a WAAPI analyser node, disconnects graph, cancels animation frame, deletes from hash
* @removeAnalyser
*/
removeAnalyser(event) {
if ( this.audioContext && this.audioWorkletNode ) {
let analyser = this.analysers[event.id];
if (analyser !== undefined) {
cancelAnimationFrame(this.analysers[event.id].analyserFrameId);
delete this.analysers[event.id];
// this.audioWorkletNode.disconnect(analyser);
}
}
}
/* #endregion */
/**
* Initialises audio context and sets worklet processor code
* @play
*/
async init(origin) {
if (origin && new URL(origin)) {
let isWorkletProcessorLoaded;
try{
// AudioContext needs lazy loading to workaround the Chrome warning
// Audio Engine first play() call, triggered by user, prevents the warning
// by setting this.audioContext = new AudioContext();
this.audioContext;
this.origin = origin;
this.audioWorkletName = "maxi-processor";
this.audioWorkletUrl = origin + "/" + this.audioWorkletName + ".js";
if (this.audioContext === undefined) {
this.audioContext = new AudioContext({
// create audio context with latency optimally configured for playback
latencyHint: "playback",
// latencyHint: 32/44100, //this doesn't work below 512 on chrome (?)
// sampleRate: 48000
});
}
isWorkletProcessorLoaded = await this.loadWorkletProcessorCode();
}
catch(err){
return false;
}
if (isWorkletProcessorLoaded) {
this.connectWorkletNode();
console.log(
"running %csema-engine v0.1.1",
"font-weight: bold; color: #ffb7c5"
// "font-weight: bold; background: #000; color: #bada55"
);
return true;
} else return false;
// No need to inject the callback here, messaging is built in KuraClock
// this.kuraClock = new kuramotoNetClock((phase, idx) => {
// // console.log( `DEBUG:AudioEngine:sendPeersMyClockPhase:phase:${phase}:id:${idx}`);
// // This requires an initialised audio worklet
// this.audioWorkletNode.port.postMessage({ phase: phase, i: idx });
// });
// if (this.kuraClock.connected()) {
// this.kuraClock.queryPeers(async numClockPeers => {
// console.log(`DEBUG:AudioEngine:init:numClockPeers: ${numClockPeers}`);
// });
// }
} else {
throw new Error("Name and valid URL required for AudioWorklet processor");
}
}
/**
* Initialises audio context and sets worklet processor code
* or re-starts audio playback by stopping and running the latest Audio Worklet Processor code
* @play
*/
play() {
if (this.audioContext !== undefined) {
if (this.audioContext.state === "suspended") {
this.audioContext.resume();
return true;
} else {
this.hush();
// this.stop();
return false;
}
}
}
/**
* Suspends AudioContext (Pause)
* @stop
*/
stop() {
if (this.audioWorkletNode !== undefined) {
this.hush();
// this.audioContext.suspend();
}
}
/**
* Stops audio by disconnecting AudioNode with AudioWorkletProcessor code
* from Web Audio graph TODO Investigate when it is best to just STOP the graph exectution
* @stop
*/
stopAndRelease() {
if (this.audioWorkletNode !== undefined) {
this.audioWorkletNode.disconnect(this.audioContext.destination);
this.audioWorkletNode = undefined;
}
}
setGain(gain) {
if (this.audioWorkletNode !== undefined && gain >= 0 && gain <= 1) {
const gainParam = this.audioWorkletNode.parameters.get("gain");
gainParam.value = gain;
console.log(gainParam.value); // DEBUG
return true;
} else return false;
}
more() {
if (this.audioWorkletNode !== undefined) {
const gainParam = this.audioWorkletNode.parameters.get("gain");
gainParam.value += 0.05;
console.info(gainParam.value); // DEBUG
return gainParam.value;
} else throw new Error("error increasing sound level");
}
less() {
if (this.audioWorkletNode !== undefined) {
const gainParam = this.audioWorkletNode.parameters.get("gain");
gainParam.value -= 0.05;
console.info(gainParam.value); // DEBUG
return gainParam.value;
} else throw new Error("error decreasing sound level");
}
hush() {
if (this.audioWorkletNode !== undefined) {
this.audioWorkletNode.port.postMessage({
hush: 1,
});
this.isHushed = true;
return true;
} else return false;
}
unHush() {
if (this.audioWorkletNode !== undefined) {
this.audioWorkletNode.port.postMessage({
unhush: 1,
});
this.isHushed = false;
return true;
} else return false;
}
eval(dspFunction) {
if (this.audioWorkletNode && this.audioWorkletNode.port) {
if (this.audioContext.state === "suspended") {
this.audioContext.resume();
}
this.audioWorkletNode.port.postMessage({
eval: 1,
setup: dspFunction.setup,
loop: dspFunction.loop,
});
this.isHushed = false;
return true;
} else return false;
}
/**
* Handler of the Pub/Sub message events
* whose topics are subscribed to in the audio engine constructor
* @asyncPostToProcessor
* @param {*} event
*/
asyncPostToProcessor(event) {
if (event && this.audioWorkletNode && this.audioWorkletNode.port) {
// Receive notification from 'model-output-data' topic
console.log("DEBUG:AudioEngine:onMessagingEventHandler:");
console.log(event);
this.audioWorkletNode.port.postMessage(event);
} else throw new Error("Error async posting to processor");
}
sendClockPhase(phase, idx) {
if (this.audioWorkletNode !== undefined) {
this.audioWorkletNode.port.postMessage({
phase: phase,
i: idx,
});
}
}
onAudioInputInit(stream) {
try {
this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);
this.mediaStreamSource.connect(this.audioWorkletNode);
this.mediaStream = stream;
this.mediaStreamSourceConnected = true;
} catch (error) {
console.error(error);
}
}
onAudioInputFail(error) {
this.mediaStreamSourceConnected = false;
console.error(
`ERROR:Engine:AudioInputFail: ${error.message} ${error.name}`
);
}
onAudioInputDisconnect() {
}
/**
* Sets up an AudioIn WAAPI sub-graph
* @connectMediaStreamSourceInput
*/
async connectMediaStream() {
const constraints = ( window.constraints = {
audio: {
latency: 0.02,
echoCancellation: false,
mozNoiseSuppression: false,
mozAutoGainControl: false
},
video: false,
});
// onAudioInputDisconnect();
await navigator.mediaDevices
.getUserMedia(constraints)
.then( s => this.onAudioInputInit(s) )
.catch(this.onAudioInputFail);
return this.mediaStreamSourceConnected;
}
/**
* Breaks up an AudioIn WAAPI sub-graph
* @disconnectMediaStreamSourceInput
*/
async disconnectMediaStream() {
try {
this.mediaStreamSource.disconnect(this.audioWorkletNode);
this.mediaStream.getAudioTracks().forEach((at) => at.stop());
this.mediaStreamSource = null;
this.mediaStreamSourceConnected = false;
} catch (error) {
console.error(error);
}
finally {
return this.mediaStreamSourceConnected;
}
// await navigator.mediaDevices
// .getUserMedia(constraints)
// .then((s) => this.onAudioInputDisconnect(s))
// .catch(this.onAudioInputFail);
}
/**
* Loads audioWorklet processor code into a worklet,
* setups up all handlers (errors, async messaging, etc),
* connects the worklet processor to the WAAPI graph
*/
async loadWorkletProcessorCode() {
if (this.audioContext !== undefined) {
try {
await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
} catch (err) {
console.error(
"ERROR:Engine:loadWorkletProcessorCode: AudioWorklet not supported in this browser: ",
err.message
);
return false;
}
try {
// Custom node constructor with required parameters
// this.audioWorkletNode = new CustomMaxiNode(
this.audioWorkletNode = new AudioWorkletNode(
this.audioContext,
this.audioWorkletName
);
this.audioWorkletNode.channelInterpretation = "discrete";
this.audioWorkletNode.channelCountMode = "explicit";
this.audioWorkletNode.channelCount = this.audioContext.destination.maxChannelCount;
return true;
} catch (err) {
console.error("Error loading worklet processor code: ", err);
return false;
}
} else {
return false;
}
}
/**
* connects all error event handlers and default processor message callback
*/
connectWorkletNode() {
if (this.audioWorkletNode !== undefined) {
try {
this.audioContext.destination.channelInterpretation = "discrete";
this.audioContext.destination.channelCountMode = "explicit";
this.audioContext.destination.channelCount = this.audioContext.destination.maxChannelCount;
// Connect the worklet node to the audio graph
this.audioWorkletNode.connect(this.audioContext.destination);
// All possible error event handlers subscribed
this.audioWorkletNode.onprocessorerror = (e) =>
// Errors from the processor
console.error(`Engine processor error detected`, e);
// Subscribe state changes in the audio worklet processor
this.audioWorkletNode.onprocessorstatechange = (e) =>
console.info(
`Engine processor state change: ` + audioWorkletNode.processorState
);
// Subscribe errors from the processor port
this.audioWorkletNode.port.onmessageerror = (e) =>
console.error(`Engine processor port error: ` + e);
// Default worklet processor message handler
// gets replaced by user callback with 'subscribeAsyncMessage'
this.audioWorkletNode.port.onmessage = (e) =>
this.onProcessorMessageHandler(e);
} catch (err) {
console.error("Error connecting WorkletNode: ", err);
}
}
}
/**
* Default worklet processor message handler
* gets replaced by user-supplied callback through 'subscribeProcessorMessage'
* @param {*} event
*/
onProcessorMessageHandler(event) {
if (event && event.data) {
if (event.data.rq && event.data.rq === "send") {
switch (event.data.ttype) {
case "ML":
// this.messaging.publish("model-input-data", {
// type: "model-input-data",
// value: event.data.value,
// ch: event.data.ch
// });
break;
case "NET":
this.peerNet.send(
event.data.ch[0],
event.data.value,
event.data.ch[1]
);
break;
}
} else if (event.data.rq && event.data.rq === "buf") {
switch (event.data.ttype) {
case "ML":
this.dispatcher.dispatch("onSharedBuffer", {
sab: event.data.value,
channelID: event.data.channelID, //channel ID
blocksize: event.data.blocksize,
});
break;
case "scope":
// this.dispatcher.dispatch("onSharedBuffer", {
// sab: event.data.value,
// channelID: event.data.channelID, //channel ID
// blocksize: event.data.blocksize,
// });
let ringbuf = new RingBuffer(event.data.value, Float64Array);
this.sharedArrayBuffers[event.data.channelID] = {
sab: event.data.value, // TODO: this is redundant, you can access the sab from the rb,
// TODO change hashmap name it is confusing and induces error
rb: ringbuf,
ttype: event.data.ttype,
channelID: event.data.channelID, //channel ID
blocksize: event.data.blocksize,
};
break;
}
} else if (event.data.rq && event.data.rq === "rts") { // ready to suspend
this.audioContext.suspend();
this.isHushed = true;
}
else if (event.data instanceof Error){
// TODO use a logger to inject error
console.error(`On Processor Message ${event.data}`);
}
}
}
/**
* Public method for subscribing async messaging from the Audio Worklet Processor scope
* @param callback
*/
subscribeProcessorMessage(callback) {
if (callback && this.audioWorkletNode)
this.audioWorkletNode.port.onmessage = callback;
else throw new Error("Error subscribing processor message");
}
/**
* Load individual audio sample, assuming an origin URL with which the engine
* is initialised
* @param {*} objectName name of the sample
* @param {*} url relative URL to the origin URL, startgin with `/`
*/
loadSample(objectName, url) {
if (this.audioContext && this.audioWorkletNode) {
if (
url &&
url.length !== 0 &&
this.origin &&
this.origin.length !== 0 &&
new URL(this.origin + url)
) {
try {
loadSampleToArray(
this.audioContext,
objectName,
this.origin + url,
this.audioWorkletNode
);
} catch (error) {
console.error(
`Error loading sample ${objectName} from ${url}: `,
error
);
}
} else throw "Problem with sample relative URL";
} else throw "Engine is not initialised!";
}
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function createCommonjsModule(fn) {
var module = { exports: {} };
return fn(module, module.exports), module.exports;
}
var nearley = createCommonjsModule(function (module) {
(function(root, factory) {
if (module.exports) {
module.exports = factory();
} else {
root.nearley = factory();
}
}(commonjsGlobal, function() {
function Rule(name, symbols, postprocess) {
this.id = ++Rule.highestId;
this.name = name;
this.symbols = symbols; // a list of literal | regex class | nonterminal
this.postprocess = postprocess;
return this;
}
Rule.highestId = 0;
Rule.prototype.toString = function(withCursorAt) {
var symbolSequence = (typeof withCursorAt === "undefined")
? this.symbols.map(getSymbolShortDisplay).join(' ')
: ( this.symbols.slice(0, withCursorAt).map(getSymbolShortDisplay).join(' ')
+ " ● "
+ this.symbols.slice(withCursorAt).map(getSymbolShortDisplay).join(' ') );
return this.name + " → " + symbolSequence;
};
// a State is a rule at a position from a given starting point in the input stream (reference)
function State(rule, dot, reference, wantedBy) {
this.rule = rule;
this.dot = dot;
this.reference = reference;
this.data = [];
this.wantedBy = wantedBy;
this.isComplete = this.dot === rule.symbols.length;
}
State.prototype.toString = function() {
return "{" + this.rule.toString(this.dot) + "}, from: " + (this.reference || 0);
};
State.prototype.nextState = function(child) {
var state = new State(this.rule, this.dot + 1, this.reference, this.wantedBy);
state.left = this;
state.right = child;
if (state.isComplete) {
state.data = state.build();
// Having right set here will prevent the right state and its children
// form being garbage collected
state.right = undefined;
}
return state;
};
State.prototype.build = function() {
var children = [];
var node = this;
do {
children.push(node.right.data);
node = node.left;
} while (node.left);
children.reverse();
return children;
};
State.prototype.finish = function() {
if (this.rule.postprocess) {
this.data = this.rule.postprocess(this.data, this.reference, Parser.fail);
}
};
function Column(grammar, index) {
this.grammar = grammar;
this.index = index;
this.states = [];
this.wants = {}; // states indexed by the non-terminal they expect
this.scannable = []; // list of states that expect a token
this.completed = {}; // states that are nullable
}
Column.prototype.process = function(nextColumn) {
var states = this.states;
var wants = this.wants;
var completed = this.completed;
for (var w = 0; w < states.length; w++) { // nb. we push() during iteration
var state = states[w];
if (state.isComplete) {
state.finish();
if (state.data !== Parser.fail) {
// complete
var wantedBy = state.wantedBy;
for (var i = wantedBy.length; i--; ) { // this line is hot
var left = wantedBy[i];
this.complete(left, state);
}
// special-case nullables
if (state.reference === this.index) {
// make sure future predictors of this rule get completed.
var exp = state.rule.name;
(this.completed[exp] = this.completed[exp] || []).push(state);
}
}
} else {
// queue scannable states
var exp = state.rule.symbols[state.dot];
if (typeof exp !== 'string') {
this.scannable.push(state);
continue;
}
// predict
if (wants[exp]) {
wants[exp].push(state);
if (completed.hasOwnProperty(exp)) {
var nulls = completed[exp];
for (var i = 0; i < nulls.length; i++) {
var right = nulls[i];
this.complete(state, right);
}
}
} else {
wants[exp] = [state];
this.predict(exp);
}
}
}
};
Column.prototype.predict = function(exp) {
var rules = this.grammar.byName[exp] || [];
for (var i = 0; i < rules.length; i++) {
var r = rules[i];
var wantedBy = this.wants[exp];
var s = new State(r, 0, this.index, wantedBy);
this.states.push(s);
}
};
Column.prototype.complete = function(left, right) {
var copy = left.nextState(right);
this.states.push(copy);
};
function Grammar(rules, start) {
this.rules = rules;
this.start = start || this.rules[0].name;
var byName = this.byName = {};
this.rules.forEach(function(rule) {
if (!byName.hasOwnProperty(rule.name)) {
byName[rule.name] = [];
}
byName[rule.name].push(rule);
});
}
// So we can allow passing (rules, start) directly to Parser for backwards compatibility
Grammar.fromCompiled = function(rules, start) {
var lexer = rules.Lexer;
if (rules.ParserStart) {
start = rules.ParserStart;
rules = rules.ParserRules;
}
var rules = rules.map(function (r) { return (new Rule(r.name, r.symbols, r.postprocess)); });
var g = new Grammar(rules, start);
g.lexer = lexer; // nb. storing lexer on Grammar is iffy, but unavoidable
return g;
};
function StreamLexer() {
this.reset("");
}
StreamLexer.prototype.reset = function(data, state) {
this.buffer = data;
this.index = 0;
this.line = state ? state.line : 1;
this.lastLineBreak = state ? -state.col : 0;
};
StreamLexer.prototype.next = function() {
if (this.index < this.buffer.length) {
var ch = this.buffer[this.index++];
if (ch === '\n') {
this.line += 1;
this.lastLineBreak = this.index;
}
return {value: ch};
}
};
StreamLexer.prototype.save = function() {
return {
line: this.line,
col: this.index - this.lastLineBreak,
}
};
StreamLexer.prototype.formatError = function(token, message) {
// nb. this gets called after consuming the offending token,
// so the culprit is index-1
var buffer = this.buffer;
if (typeof buffer === 'string') {
var lines = buffer
.split("\n")
.slice(
Math.max(0, this.line - 5),
this.line
);
buffer.indexOf('\n', this.index);
var col = this.index - this.lastLineBreak;
var lastLineDigits = String(this.line).length;
message += " at line " + this.line + " col " + col + ":\n\n";
message += lines
.map(function(line, i) {
return pad(this.line - lines.length + i + 1, lastLineDigits) + " " + line;
}, this)
.join("\n");
message += "\n" + pad("", lastLineDigits + col) + "^\n";
return message;
} else {
return message + " at index " + (this.index - 1);
}
function pad(n, length) {
var s = String(n);
return Array(length - s.length + 1).join(" ") + s;
}
};
function Parser(rules, start, options) {
if (rules instanceof Grammar) {
var grammar = rules;
var options = start;
} else {
var grammar = Grammar.fromCompiled(rules, start);
}
this.grammar = grammar;
// Read options
this.options = {
keepHistory: false,
lexer: grammar.lexer || new StreamLexer,
};
for (var key in (options || {})) {
this.options[key] = options[key];
}
// Setup lexer
this.lexer = this.options.lexer;
this.lexerState = undefined;
// Setup a table
var column = new Column(grammar, 0);
this.table = [column];
// I could be expecting anything.
column.wants[grammar.start] = [];
column.predict(grammar.start);
// TODO what if start rule is nullable?
column.process();
this.current = 0; // token index
}
// create a reserved token for indicating a parse fail
Parser.fail = {};
Parser.prototype.feed = function(chunk) {
var lexer = this.lexer;
lexer.reset(chunk, this.lexerState);
var token;
while (true) {
try {
token = lexer.next();
if (!token) {
break;
}
} catch (e) {
// Create the next column so that the error reporter
// can display the correctly predicted states.
var nextColumn = new Column(this.grammar, this.current + 1);
this.table.push(nextColumn);
var err = new Error(this.reportLexerError(e));
err.offset = this.current;
err.token = e.token;
throw err;
}
// We add new states to table[current+1]
var column = this.table[this.current];
// GC unused states
if (!this.options.keepHistory) {
delete this.table[this.current - 1];
}
var n = this.current + 1;
var nextColumn = new Column(this.grammar, n);
this.table.push(nextColumn);
// Advance all tokens that expect the symbol
var literal = token.text !== undefined ? token.text : token.value;
var value = lexer.constructor === StreamLexer ? token.value : token;
var scannable = column.scannable;
for (var w = scannable.length; w--; ) {
var state = scannable[w];
var expect = state.rule.symbols[state.dot];
// Try to consume the token
// either regex or literal
if (expect.test ? expect.test(value) :
expect.type ? expect.type === token.type
: expect.literal === literal) {
// Add it
var next = state.nextState({data: value, token: token, isToken: true, reference: n - 1});
nextColumn.states.push(next);
}
}
// Next, for each of the rules, we either
// (a) complete it, and try to see if the reference row expected that
// rule
// (b) predict the next nonterminal it expects by adding that
// nonterminal's start state
// To prevent duplication, we also keep track of rules we have already
// added
nextColumn.process();
// If needed, throw an error:
if (nextColumn.states.length === 0) {
// No states at all! This is not good.
var err = new Error(this.reportError(token));
err.offset = this.current;
err.token = token;
throw err;
}
// maybe save lexer state
if (this.options.keepHistory) {
column.lexerState = lexer.save();
}
this.current++;
}
if (column) {
this.lexerState = lexer.save();
}
// Incrementally keep track of results
this.results = this.finish();
// Allow chaining, for whatever it's worth
return this;
};
Parser.prototype.reportLexerError = function(lexerError) {
var tokenDisplay, lexerMessage;
// Planning to add a token property to moo's thrown error
// even on erroring tokens to be used in error display below
var token = lexerError.token;
if (token) {
tokenDisplay = "input " + JSON.stringify(token.text[0]) + " (lexer error)";
lexerMessage = this.lexer.formatError(token, "Syntax error");
} else {
tokenDisplay = "input (lexer error)";
lexerMessage = lexerError.message;
}
return this.reportErrorCommon(lexerMessage, tokenDisplay);
};
Parser.prototype.reportError = function(token) {
var tokenDisplay = (token.type ? token.type + " token: " : "") + JSON.stringify(token.value !== undefined ? token.value : token);
var lexerMessage = this.lexer.formatError(token, "Syntax error");
return this.reportErrorCommon(lexerMessage, tokenDisplay);
};
Parser.prototype.reportErrorCommon = function(lexerMessage, tokenDisplay) {
var lines = [];
lines.push(lexerMessage);
var lastColumnIndex = this.table.length - 2;
var lastColumn = this.table[lastColumnIndex];
var expectantStates = lastColumn.states
.filter(function(state) {
var nextSymbol = state.rule.symbols[state.dot];
return nextSymbol && typeof nextSymbol !== "string";
});
if (expectantStates.length === 0) {
lines.push('Unexpected ' + tokenDisplay + '. I did not expect any more input. Here is the state of my parse table:\n');
this.displayStateStack(lastColumn.states, lines);
} else {
lines.push('Unexpected ' + tokenDisplay + '. Instead, I was expecting to see one of the following:\n');
// Display a "state stack" for each expectant state
// - which shows you how this state came to be, step by step.
// If there is more than one derivation, we only display the first one.
var stateStacks = expectantStates
.map(function(state) {
return this.buildFirstStateStack(state, []) || [state];
}, this);
// Display each state that is expecting a terminal symbol next.
stateStacks.forEach(function(stateStack) {
var state = stateStack[0];
var nextSymbol = state.rule.symbols[state.dot];
var symbolDisplay = this.getSymbolDisplay(nextSymbol);
lines.push('A ' + symbolDisplay + ' based on:');
this.displayStateStack(stateStack, lines);
}, this);
}
lines.push("");
return lines.join("\n");
};
Parser.prototype.displayStateStack = function(stateStack, lines) {
var lastDisplay;
var sameDisplayCount = 0;
for (var j = 0; j < stateStack.length; j++) {
var state = stateStack[j];
var display = state.rule.toString(state.dot);
if (display === lastDisplay) {
sameDisplayCount++;
} else {
if (sameDisplayCount > 0) {
lines.push(' ^ ' + sameDisplayCount + ' more lines identical to this');
}
sameDisplayCount = 0;
lines.push(' ' + display);
}
lastDisplay = display;
}
};
Parser.prototype.getSymbolDisplay = function(symbol) {
return getSymbolLongDisplay(symbol);
};
/*
Builds a the first state stack. You can think of a state stack as the call stack
of the recursive-descent parser which the Nearley parse algorithm simulates.
A state stack is represented as an array of state objects. Within a
state stack, the first item of the array will be the starting
state, with each successive item in the array going further back into history.
This function needs to be given a starting state and an empty array representing
the visited states, and it returns an single state stack.
*/
Parser.prototype.buildFirstStateStack = function(state, visited) {
if (visited.indexOf(state) !== -1) {
// Found cycle, return null
// to eliminate this path from the results, because
// we don't know how to display it meaningfully
return null;
}
if (state.wantedBy.length === 0) {
return [state];
}
var prevState = state.wantedBy[0];
var childVisited = [state].concat(visited);
var childResult = this.buildFirstStateStack(prevState, childVisited);
if (childResult === null) {
return null;
}
return [state].concat(childResult);
};
Parser.prototype.save = function() {
var column = this.table[this.current];
column.lexerState = this.lexerState;
return column;
};
Parser.prototype.restore = function(column) {
var index = column.index;
this.current = index;
this.table[index] = column;
this.table.splice(index + 1);
this.lexerState = column.lex