UNPKG

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