UNPKG

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
(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