sema-engine
Version:
Module containing Sema's signal engine and compiler for a live coding language ecosystem
1,501 lines (1,306 loc) • 166 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);
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['sema-engine'] = {}));
}(this, (function (exports) { 'use strict';
// 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);