UNPKG

sema-engine

Version:

Module containing Sema's signal engine and compiler for a live coding language ecosystem

1 lines 314 kB
{"version":3,"file":"index.mjs","sources":["node_modules/ringbuf.js/dist/index.es.js","src/engine/maximilian.util.js","src/common/event.js","src/common/dispatcher.js","src/engine/engine.js","node_modules/nearley/lib/nearley.js","node_modules/moo/moo.js","node_modules/nearley/lib/nearley-language-bootstrapped.js","src/compiler/compiler-low-level.js","node_modules/nearley/lib/generate.js","node_modules/nearley/lib/lint.js","src/compiler/IR.js","src/compiler/sema.js","src/compiler/compiler.js","src/common/logger.js","src/learner/learner.js","src/common/blockTracker.js"],"sourcesContent":["// Send audio interleaved audio frames between threads, wait-free.\n//\n// Those classes allow communicating between a non-real time thread (browser\n// main thread or worker) and a real-time thread (in an AudioWorkletProcessor).\n// Write and Reader cannot change role after setup, unless externally\n// synchronized.\n//\n// GC _can_ happen during the initial construction of this object when hopefully\n// no audio is being output. This depends on how implementations schedule GC\n// passes. After the setup phase no GC is triggered on either side of the queue..\n\n// Interleaved -> Planar audio buffer conversion\n//\n// `input` is an array of n*128 frames arrays, interleaved, where n is the\n// channel count.\n// output is an array of 128-frames arrays.\n//\n// This is useful to get data from a codec, the network, or anything that is\n// interleaved, into planar format, for example a Web Audio API AudioBuffer or\n// the output parameter of an AudioWorkletProcessor.\nfunction deinterleave(input, output) {\n var channel_count = input.length / 256;\n if (output.length != channel_count) {\n throw \"not enough space in output arrays\";\n }\n for (var i = 0; i < channelCount; i++) {\n let out_channel = output[i];\n let interleaved_idx = i;\n for (var j = 0; j < 128; ++j) {\n out_channel[j] = input[interleaved_idx];\n interleaved_idx += channel_count;\n }\n }\n}\n// Planar -> Interleaved audio buffer conversion\n//\n// Input is an array of `n` 128 frames Float32Array that hold the audio data.\n// output is a Float32Array that is n*128 elements long. This function is useful\n// to get data from the Web Audio API (that does planar audio), into something\n// that codec or network streaming library expect.\nfunction interleave(input, output) {\n if (input.length * 128 != output.length) {\n throw \"input and output of incompatible sizes\";\n }\n var out_idx = 0;\n for (var i = 0; i < 128; i++) {\n for (var channel = 0; j < output.length; j++) {\n output[out_idx] = input[channel][i];\n out_idx++;\n }\n }\n}\n\nclass AudioWriter {\n // From a RingBuffer, build an object that can enqueue enqueue audio in a ring\n // buffer.\n constructor(ringbuf) {\n if (ringbuf.type() != \"Float32Array\") {\n throw \"This class requires a ring buffer of Float32Array\";\n }\n this.ringbuf = ringbuf;\n }\n // Enqueue a buffer of interleaved audio into the ring buffer.\n // Returns the number of samples that have been successfuly written to the\n // queue. `buf` is not written to during this call, so the samples that\n // haven't been written to the queue are still available.\n enqueue(buf) {\n return this.ringbuf.push(buf);\n }\n // Query the free space in the ring buffer. This is the amount of samples that\n // can be queued, with a guarantee of success.\n available_write() {\n return this.ringbuf.available_write();\n }\n}\n\nclass AudioReader {\n constructor(ringbuf) {\n if (ringbuf.type() != \"Float32Array\") {\n throw \"This class requires a ring buffer of Float32Array\";\n }\n this.ringbuf = ringbuf;\n }\n // Attempt to dequeue at most `buf.length` samples from the queue. This\n // returns the number of samples dequeued. If greater than 0, the samples are\n // at the beginning of `buf`\n dequeue(buf) {\n if (this.ringbuf.empty()) {\n return 0;\n }\n return this.ringbuf.pop(buf);\n }\n // Query the occupied space in the queue. This is the amount of samples that\n // can be read with a guarantee of success.\n available_read() {\n return this.ringbuf.available_read();\n }\n}\n\n// Communicate parameter changes, lock free, no gc.\n//\n// between a UI thread (browser main thread or worker) and a real-time thread\n// (in an AudioWorkletProcessor). Write and Reader cannot change role after\n// setup, unless externally synchronized.\n//\n// GC can happen during the initial construction of this object when hopefully\n// no audio is being output. This depends on the implementation.\n//\n// Parameter changes are like in the VST framework: an index and a float value\n// (no restriction on the value).\n//\n// This class supports up to 256 parameters, but this is easy to extend if\n// needed.\n//\n// An element is a index, that is an unsigned byte, and a float32, which is 4\n// bytes.\n\nclass ParameterWriter {\n // From a RingBuffer, build an object that can enqueue a parameter change in\n // the queue.\n constructor(ringbuf) {\n if (ringbuf.type() != \"Uint8Array\") {\n throw \"This class requires a ring buffer of Uint8Array\";\n }\n const SIZE_ELEMENT = 5;\n this.ringbuf = ringbuf;\n this.mem = new ArrayBuffer(SIZE_ELEMENT);\n this.array = new Uint8Array(this.mem);\n this.view = new DataView(this.mem);\n }\n // Enqueue a parameter change for parameter of index `index`, with a new value\n // of `value`.\n // Returns true if enqueuing suceeded, false otherwise.\n enqueue_change(index, value) {\n const SIZE_ELEMENT = 5;\n this.view.setUint8(0, index);\n this.view.setFloat32(1, value);\n if (this.ringbuf.available_write() < SIZE_ELEMENT) {\n return false;\n }\n return this.ringbuf.push(this.array) == SIZE_ELEMENT;\n }\n}\n\nclass ParameterReader {\n constructor(ringbuf) {\n const SIZE_ELEMENT = 5;\n this.ringbuf = ringbuf;\n this.mem = new ArrayBuffer(SIZE_ELEMENT);\n this.array = new Uint8Array(this.mem);\n this.view = new DataView(this.mem);\n }\n dequeue_change(o) {\n if (this.ringbuf.empty()) {\n return false;\n }\n var rv = this.ringbuf.pop(this.array);\n o.index = this.view.getUint8(0);\n o.value = this.view.getFloat32(1);\n\n return true;\n }\n}\n\n// A Single Producer - Single Consumer thread-safe wait-free ring buffer.\n//\n// The producer and the consumer can be separate thread, but cannot change role,\n// except with external synchronization.\n\nclass RingBuffer {\n static getStorageForCapacity(capacity, type) {\n if (!type.BYTES_PER_ELEMENT) {\n throw \"Pass in a ArrayBuffer subclass\";\n }\n var bytes = 8 + (capacity + 1) * type.BYTES_PER_ELEMENT;\n return new SharedArrayBuffer(bytes);\n }\n // `sab` is a SharedArrayBuffer with a capacity calculated by calling\n // `getStorageForCapacity` with the desired capacity.\n constructor(sab, type) {\n if (!ArrayBuffer.__proto__.isPrototypeOf(type) &&\n type.BYTES_PER_ELEMENT !== undefined) {\n throw \"Pass a concrete typed array class as second argument\";\n }\n\n // Maximum usable size is 1<<32 - type.BYTES_PER_ELEMENT bytes in the ring\n // buffer for this version, easily changeable.\n // -4 for the write ptr (uint32_t offsets)\n // -4 for the read ptr (uint32_t offsets)\n // capacity counts the empty slot to distinguish between full and empty.\n this._type = type;\n this.capacity = (sab.byteLength - 8) / type.BYTES_PER_ELEMENT;\n this.buf = sab;\n this.write_ptr = new Uint32Array(this.buf, 0, 1);\n this.read_ptr = new Uint32Array(this.buf, 4, 1);\n this.storage = new type(this.buf, 8, this.capacity);\n }\n // Returns the type of the underlying ArrayBuffer for this RingBuffer. This\n // allows implementing crude type checking.\n type() {\n return this._type.name;\n }\n // Push bytes to the ring buffer. `bytes` is an typed array of the same type\n // as passed in the ctor, to be written to the queue.\n // Returns the number of elements written to the queue.\n push(elements) {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n\n if ((wr + 1) % this._storage_capacity() == rd) {\n // full\n return 0;\n }\n\n let to_write = Math.min(this._available_write(rd, wr), elements.length);\n let first_part = Math.min(this._storage_capacity() - wr, to_write);\n let second_part = to_write - first_part;\n\n this._copy(elements, 0, this.storage, wr, first_part);\n this._copy(elements, first_part, this.storage, 0, second_part);\n\n // publish the enqueued data to the other side\n Atomics.store(\n this.write_ptr,\n 0,\n (wr + to_write) % this._storage_capacity()\n );\n\n return to_write;\n }\n // Read `elements.length` elements from the ring buffer. `elements` is a typed\n // array of the same type as passed in the ctor.\n // Returns the number of elements read from the queue, they are placed at the\n // beginning of the array passed as parameter.\n pop(elements) {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n\n if (wr == rd) {\n return 0;\n }\n\n let to_read = Math.min(this._available_read(rd, wr), elements.length);\n\n let first_part = Math.min(this._storage_capacity() - rd, elements.length);\n let second_part = to_read - first_part;\n\n this._copy(this.storage, rd, elements, 0, first_part);\n this._copy(this.storage, 0, elements, first_part, second_part);\n\n Atomics.store(this.read_ptr, 0, (rd + to_read) % this._storage_capacity());\n\n return to_read;\n }\n\n // True if the ring buffer is empty false otherwise. This can be late on the\n // reader side: it can return true even if something has just been pushed.\n empty() {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n\n return wr == rd;\n }\n\n // True if the ring buffer is full, false otherwise. This can be late on the\n // write side: it can return true when something has just been poped.\n full() {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n\n return (wr + 1) % this.capacity != rd;\n }\n\n // The usable capacity for the ring buffer: the number of elements that can be\n // stored.\n capacity() {\n return this.capacity - 1;\n }\n\n // Number of elements available for reading. This can be late, and report less\n // elements that is actually in the queue, when something has just been\n // enqueued.\n available_read() {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n return this._available_read(rd, wr);\n }\n\n // Number of elements available for writing. This can be late, and report less\n // elemtns that is actually available for writing, when something has just\n // been dequeued.\n available_write() {\n var rd = Atomics.load(this.read_ptr, 0);\n var wr = Atomics.load(this.write_ptr, 0);\n return this._available_write(rd, wr);\n }\n\n // private methods //\n\n // Number of elements available for reading, given a read and write pointer..\n _available_read(rd, wr) {\n if (wr > rd) {\n return wr - rd;\n } else {\n return wr + this._storage_capacity() - rd;\n }\n }\n\n // Number of elements available from writing, given a read and write pointer.\n _available_write(rd, wr) {\n let rv = rd - wr - 1;\n if (wr >= rd) {\n rv += this._storage_capacity();\n }\n return rv;\n }\n\n // The size of the storage for elements not accounting the space for the index.\n _storage_capacity() {\n return this.capacity;\n }\n\n // Copy `size` elements from `input`, starting at offset `offset_input`, to\n // `output`, starting at offset `offset_output`.\n _copy(input, offset_input, output, offset_output, size) {\n for (var i = 0; i < size; i++) {\n output[offset_output + i] = input[offset_input + i];\n }\n }\n}\n\nexport { AudioReader, AudioWriter, ParameterReader, ParameterWriter, RingBuffer, deinterleave, interleave };\n//# sourceMappingURL=index.es.js.map\n","export const getArrayAsVectorDbl = (arrayIn) => {\n var vecOut = new exports.VectorDouble();\n for (var i = 0; i < arrayIn.length; i++) {\n vecOut.push_back(arrayIn[i]);\n }\n return vecOut;\n};\n\nexport const getBase64 = (str) => {\n //check if the string is a data URI\n if (str.indexOf(';base64,') !== -1) {\n //see where the actual data begins\n var dataStart = str.indexOf(';base64,') + 8;\n //check if the data is base64-encoded, if yes, return it\n // taken from\n // http://stackoverflow.com/a/8571649\n 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;\n } else return false;\n};\n\nexport const _keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n\nexport const removePaddingFromBase64 = (input) => {\n var lkey = Module.maxiTools._keyStr.indexOf(input.charAt(input.length - 1));\n if (lkey === 64) {\n return input.substring(0, input.length - 1);\n }\n return input;\n};\n\n\nexport const loadSampleToArray = (audioContext, sampleObjectName, url, audioWorkletNode) => {\n var data = [];\n\n var context = audioContext;\n\n //check if url is actually a base64-encoded string\n var b64 = getBase64(url);\n if (b64) {\n //convert to arraybuffer\n //modified version of this:\n // https://github.com/danguer/blog-examples/blob/master/js/base64-binary.js\n var ab_bytes = (b64.length / 4) * 3;\n var arrayBuffer = new ArrayBuffer(ab_bytes);\n\n b64 = removePaddingFromBase64(removePaddingFromBase64(b64));\n\n var bytes = parseInt((b64.length / 4) * 3, 10);\n\n var uarray;\n var chr1, chr2, chr3;\n var enc1, enc2, enc3, enc4;\n var i = 0;\n var j = 0;\n\n uarray = new Uint8Array(arrayBuffer);\n\n b64 = b64.replace(/[^A-Za-z0-9\\+\\/\\=]/g, \"\");\n\n for (i = 0; i < bytes; i += 3) {\n //get the 3 octects in 4 ascii chars\n enc1 = _keyStr.indexOf(b64.charAt(j++));\n enc2 = _keyStr.indexOf(b64.charAt(j++));\n enc3 = _keyStr.indexOf(b64.charAt(j++));\n enc4 = _keyStr.indexOf(b64.charAt(j++));\n\n chr1 = (enc1 << 2) | (enc2 >> 4);\n chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);\n chr3 = ((enc3 & 3) << 6) | enc4;\n\n uarray[i] = chr1;\n if (enc3 !== 64) {\n uarray[i + 1] = chr2;\n }\n if (enc4 !== 64) {\n uarray[i + 2] = chr3;\n }\n }\n\n // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata\n // Asynchronously decodes the audio file data contained in the ArrayBuffer.\n audioContext.decodeAudioData(\n arrayBuffer, // has its content-type determined by sniffing\n function (buffer) { // successCallback, argument is an AudioBuffer representing the decoded PCM audio data.\n // source.buffer = buffer;\n // source.loop = true;\n // source.start(0);\n let float32ArrayBuffer = buffer.getChannelData(0);\n if (data !== undefined && audioWorkletNode !== undefined) {\n // console.log('f32array: ' + float32Array);\n audioWorkletNode.port.postMessage({\n \"sample\":sampleObjectName,\n \"buffer\": float32ArrayBuffer,\n });\n }\n },\n function (buffer) { // errorCallback\n console.log(\"Error decoding source!\");\n }\n );\n } else {\n // Load asynchronously\n // NOTE: This is giving me an error\n // Uncaught ReferenceError: XMLHttpRequest is not defined (index):97 MaxiProcessor Error detected: undefined\n // NOTE: followed the trail to the wasmmodule.js\n // when loading on if (typeof XMLHttpRequest !== 'undefined') {\n // throw new Error(\"Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers.\n // Use --embed-file or --preload-file in emcc on the main thread.\");\n var request = new XMLHttpRequest();\n request.addEventListener(\"load\", () =>\n\t\t\tconsole.info(`loading sample '${sampleObjectName}'`)\n\t\t);\n request.open(\"GET\", url, true);\n request.responseType = \"arraybuffer\";\n request.onload = function () {\n audioContext.decodeAudioData(\n request.response,\n function (buffer) {\n let float32ArrayBuffer = buffer.getChannelData(0);\n if (data !== undefined && audioWorkletNode !== undefined) {\n // console.log('f32array: ' + float32Array);\n audioWorkletNode.port.postMessage({\n \"sample\":sampleObjectName,\n \"buffer\": float32ArrayBuffer,\n });\n }\n },\n function (buffer) {\n console.log(\"Error decoding source!\");\n }\n );\n };\n request.send();\n }\n return \"Loading module\";\n};\n\n/**\n * @buildWorkletStringForBlob\n */\nexport const buildWorkletStringForBlob = (userDefinedFunction) => {\n\t// let = \"\";\n\t// switch (expression % 2) {\n\t// \tcase 0:\n\t// \t\tuserDefinedFunction = `Math.random() * 2`;\n\t// \t\tbreak;\n\t// \tcase 1:\n\t// \t\tuserDefinedFunction = `(Math.sin(400) + 0.4)`;\n\t// \t\tbreak;\n\t// \tdefault:\n\t// \t\tuserDefinedFunction = `(Math.sin(440) + 0.4)`;\n\t// }\n\n\t// We get an \"Error on loading worklet: DOMException\" with the following import:\n\t// import Module from './maximilian.wasmmodule.js';\n\treturn `\n import Module from './maximilian.wasmmodule.js';\n cwlass CustomProcessor extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [{\n name: 'gain',\n defaultValue: 0.1\n }];\n }\n constructor() {\n super();\n this.sampleRate = 44100;\n\n this.port.onmessage = (event) => {\n console.log(event.data);\n };\n\n }\n process(inputs, outputs, parameters) {\n\n const outputsLength = outputs.length;\n for (let outputId = 0; outputId < outputsLength; ++outputId) {\n let output = outputs[outputId];\n const channelLenght = output.length;\n\n for (let channelId = 0; channelId < channelLenght; ++channelId) {\n const gain = parameters.gain;\n const isConstant = gain.length === 1\n let outputChannel = output[channelId];\n\n for (let i = 0; i < outputChannel.length; ++i) {\n const amp = isConstant ? gain[0] : gain[i]\n outputChannel[i] = ${userDefinedFunction} * amp;\n }\n }\n }\n return true;\n }\n }`;\n};\n\n/**\n * @createAndRegisterCustomProcessorCode\n */\nexport const createAndRegisterCustomProcessorCode = (il2pCode, processorName) => {\n\n return `${il2pCode}\n\n registerProcessor(\"${processorName}\", CustomProcessor);`;\n}\n\n/**\n * @buildWorkletStringForBlob\n */\nexport const buildWorkletFromBlob = () => {\n // console.log('processorCount: ' + this.processorCount);\n // // const userCode = editor.getDoc().getValue();\n // const processorName = `processor-${this.processorCount}`;\n\n // this.il2pCode = this.translateIntermediateLanguageToProcessorCode(this.processorCount);\n\n // const code = this.createAndRegisterCustomProcessorCode(this.il2pCode, processorName);\n\n // console.log(code);\n\n // const blob = new Blob([code], {\n // type: \"application/javascript; charset=utf-8\",\n // });\n\n return blob;\n}\n\n/**\n * TODO: Check for memory leaks\n * @runProcessorCode\n */\n// export const runProcessorCode = () => {\n// // TODO: Check for memory leaks\n// // URL.revokeObjectURL()\n// const workletUrl = window.URL.createObjectURL(blob);\n\n// // Set custom processor in audio worklet\n// this.audioContext.audioWorklet.addModule(workletUrl).then(() => {\n// this.stop();\n// this.customNode = new CustomAudioNode(this.audioContext, processorName);\n// this.customNode.port.onmessage = (event) => {\n// // data from the processor.\n// console.log(\"from processor: \" + event.data);\n// };\n// this.customNode.connect(this.audioContext.destination);\n// }).catch(e => console.log(\"Error on loading worklet: \", e));\n// }\n\n\nexport const generateNoiseBuffer = (length) => {\n var bufferData = new Module.VectorDouble();\n for (var n = 0; n < length; n++) {\n bufferData.push_back(Math.random(1));\n }\n return bufferData;\n}\n\n\nexport const translateBlobToBuffer = (blob) => {\n\n let arrayBuffer = null;\n let float32Array = null;\n var fileReader = new FileReader();\n fileReader.onload = function (event) {\n arrayBuffer = event.target.result;\n float32Array = new Float32Array(arrayBuffer);\n };\n fileReader.readAsArrayBuffer(blob);\n let audioFloat32Array = fileReader.result;\n var maxiSampleBufferData = new Module.VectorDouble();\n for (var i = 0; i < audioFloat32Array.length; i++) {\n maxiSampleBufferData.push_back(audioFloat32Array[i]);\n }\n return maxiSampleBufferData;\n}\n","export default class Event {\n\tconstructor(eventName) {\n\t\tthis.eventName = eventName;\n\t\tthis.callbacks = [];\n\t}\n\n\tregisterCallback(callback) {\n this.callbacks.push(callback);\n\t}\n\n\tunregisterCallback(callback) {\n const index = this.callbacks.indexOf(callback);\n\t\tif (index > -1) {\n\t\t\tthis.callbacks.splice(index, 1);\n\t\t}\n\t}\n\n\temit(data) {\n const callbacks = this.callbacks.slice(0);\n\t\tcallbacks.forEach( callback => {\n\t\t\tcallback(data);\n\t\t});\n\t}\n}\n","import Event from './event.js';\n\nexport default class Dispatcher {\n\n constructor() {\n\t\tthis.events = {};\n\t}\n\n\tdispatch(eventName, data) {\n\t\t// console.log(\"dispatch is getting called.\");\n\t\tconst event = this.events[eventName];\n\t\t//let event = Event()\n\n\t\t// console.log(\"event\", this.events, \"data\", data);\n\t\t// console.log(\"data\", data, event);\n\t\tif (event) {\n\t\t\t// console.log(\"data being emitted\", data)\n\t\t\tevent.emit(data);\n\t\t}\n\t}\n\n\taddEventListener(eventName, callback) {\n\t\tlet event = this.events[eventName];\n\t\tif (!event) {\n\t\t\tevent = new Event(eventName);\n\t\t\tthis.events[eventName] = event;\n\t\t}\n\t\tevent.registerCallback(callback);\n\t}\n\n\tremoveEventListener(eventName, callback) {\n\t\tconst event = this.events[eventName];\n\t\tif (event && event.callbacks.indexOf(callback) > -1) {\n\t\t\tevent.unregisterCallback(callback);\n\t\t\tif (event.callbacks.length === 0) {\n\t\t\t\tdelete this.events[eventName];\n\t\t\t}\n\t\t}\n\t}\n}\n","// NOTE: this imports RingBuffer directly from node_modules\n// which is different from maxi-processor that\n// dynamically loads from the adjacent ringBuf.js file\nimport { RingBuffer } from 'ringbuf.js'; //thanks padenot\n\nimport {\n loadSampleToArray\n} from './maximilian.util.js';\n\nimport Dispatcher from '../common/dispatcher.js';\n// import { isThisTypeNode } from 'typescript';\n// import {\n// kuramotoNetClock\n// } from './interfaces/clockInterface.js';\n// import {\n// PeerStreaming\n// } from '../interfaces/peerStreaming.js';\n\n/**\n * The CustomMaxiNode is a class that extends AudioWorkletNode\n * to hold an Custom Audio Worklet Processor and connect to Web Audio graph\n * @class CustomMaxiNode\n * @extends AudioWorkletNode\n */\n// if(true){\nclass CustomMaxiNode extends AudioWorkletNode {\n constructor(audioContext, processorName) {\n // super(audioContext, processorName);\n // console.log();\n let options = {\n numberOfInputs: 1,\n numberOfOutputs: 1,\n outputChannelCount: [audioContext.destination.maxChannelCount]\n };\n super(audioContext, processorName, options);\n }\n}\n// }\n\n/**\n * The Engine is a singleton class that encapsulates the AudioContext\n * and all WASM and Maximilian -powered Audio Worklet Processor\n * @class AudioEngine\n * TODO more error handling\n * TODO more checking of arguments passed to methods\n * TODO optimise performance, especially on analysers which are pumping data continuously\n */\nexport class Engine {\n\t/**\n\t * @constructor\n\t */\n\tconstructor() {\n\t\tif (Engine.instance) {\n\t\t\treturn Engine.instance; // Singleton pattern\n\t\t}\n\t\tEngine.instance = this;\n\n\t\tthis.origin = \"\";\n\n\t\tthis.learners = {};\n\n\t\t// Hash of on-demand analysers (e.g. spectrogram, oscilloscope)\n\t\t// NOTE: analysers serialized to localStorage are de-serialized\n\t\t// and loaded from localStorage before user-triggered audioContext init\n\t\tthis.analysers = {};\n\n\t\tthis.mediaStreamSource = {};\n\t\tthis.mediaStream = {};\n\n\t\t// Shared array buffers for sharing client side data to the audio engine- e.g. mouse coords\n\t\tthis.sharedArrayBuffers = {};\n\n\t\t// Event emitter that should be subscribed by SAB receivers\n\t\tthis.dispatcher = new Dispatcher();\n\n\t\tthis.samplesLoaded = false;\n this.isHushed = false;\n\t}\n\n\t/**\n\t * Add learner instance\n\t */\n\tasync addLearner(id, learner) {\n\t\tif (learner) {\n\t\t\ttry {\n\t\t\t\t// `this` is the scope that will\n\t\t\t\tawait learner.init(this.origin);\n\n\t\t\t\tthis.addEventListener(\"onSharedBuffer\", (e) =>\n\t\t\t\t\tlearner.addSharedBuffer(e)\n\t\t\t\t); // Engine's SAB emissions subscribed by Learner\n\n\t\t\t\tlearner.addEventListener(\"onSharedBuffer\", (e) =>\n\t\t\t\t\tthis.addSharedBuffer(e)\n\t\t\t\t); // Learner's SAB emissions subscribed by Engine\n\n\t\t\t\tthis.learners[id] = learner;\n\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Error adding Learner to Engine: \", error);\n\t\t\t}\n\t\t} else throw new Error(\"Error adding Learner instance to Engine\");\n\t}\n\n\tremoveLearner(id) {\n\t\tif (id){\n\t\t\tif (this.learners && ( id in this.learners ) ) {\n\t\t\t\tlet learner = this.learners[id];\n\t\t\t\tlearner.removeEventListener(\"onSharedBuffer\", (e) =>\n\t\t\t\t\tthis.addSharedBuffer(e)\n\t\t\t\t);\n\t\t\t\tlearner = null;\n\t\t\t\tdelete this.learners[id];\n\t\t\t}\n\t\t\t// else throw new Error(\"Error removing Learner from Engine: \");\n\t\t} else throw new Error(\"Error with learner ID when removing Learner from Engine\");\n\t}\n\n\t/**\n\t * Engine's event subscription\n\t * @addEventListener\n\t * @param {*} event\n\t * @param {*} callback\n\t */\n\taddEventListener(event, callback) {\n\t\tif (this.dispatcher && event && callback)\n\t\t\tthis.dispatcher.addEventListener(event, callback);\n\t\telse throw new Error(\"Error adding event listener to Engine\");\n\t}\n\n\t/* #region SharedBuffers */\n\n\t/**\n\t * Create a shared array buffer for communicating with the audio engine\n\t * @param channelId\n\t * @param ttype\n\t * @param blocksize\n\t */\n\tcreateSharedBuffer(channelId, ttype, blocksize) {\n\t\tlet sab = RingBuffer.getStorageForCapacity(32 * blocksize, Float64Array);\n\t\tlet ringbuf = new RingBuffer(sab, Float64Array);\n\n\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\tfunc: \"sab\",\n\t\t\tvalue: sab,\n\t\t\tttype: ttype,\n\t\t\tchannelID: channelId,\n\t\t\tblocksize: blocksize,\n\t\t});\n\n\t\tthis.sharedArrayBuffers[channelId] = {\n\t\t\tsab: sab, // TODO: this is redundant, you can access the sab from the rb,\n\t\t\t// TODO change hashmap name it is confusing and induces error\n\t\t\trb: ringbuf,\n\t\t};\n\n\t\treturn sab;\n\t}\n\n\t/**\n\t * Push data to shared array buffer for communicating with the audio engine and ML worker\n\t * @param {*} e\n\t */\n\taddSharedBuffer(e) {\n\t\tif (e) {\n\t\t\tif (e.value && e.value instanceof SharedArrayBuffer) {\n\t\t\t\ttry {\n\t\t\t\t\tlet ringbuf = new RingBuffer(e.value, Float64Array);\n\t\t\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\t\t\tfunc: \"sab\",\n\t\t\t\t\t\tvalue: e.value,\n\t\t\t\t\t\tttype: e.ttype,\n\t\t\t\t\t\tchannelID: e.channelID,\n\t\t\t\t\t\tblocksize: e.blocksize,\n\t\t\t\t\t});\n\n\t\t\t\t\tthis.sharedArrayBuffers[e.channelID] = {\n\t\t\t\t\t\tsab: e.value, // TODO this is redundant, you can access the sab from the rb,\n\t\t\t\t\t\t// TODO also change hashmap name it is confusing and induces error\n\t\t\t\t\t\trb: ringbuf,\n\t\t\t\t\t};\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"Error pushing SharedBuffer to engine\");\n\t\t\t\t}\n\t\t\t} else if (e.name && e.data) {\n\t\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\t\tfunc: \"sendbuf\",\n\t\t\t\t\tname: e.name,\n\t\t\t\t\tdata: e.data,\n\t\t\t\t});\n\t\t\t}\n\t\t} else throw new Error(\"Error with onSharedBuffer event\");\n\t}\n\n\t/**\n\t * Push data to shared array buffer for communicating with the audio engine and ML worker\n\t * @param {*} e\n\t * @param {*} channelId\n\t */\n\tpushDataToSharedBuffer(channelId, data) {\n\t\tif (channelId && data && typeof Array.isArray(data)) {\n\t\t\tif (this.sharedArrayBuffers && this.sharedArrayBuffers[channelId]) {\n\t\t\t\tthis.sharedArrayBuffers[channelId].rb.push(data);\n\t\t\t}\n\t\t} else throw new Error(\"Error in function parameters\");\n\t}\n\n\t/* #region Analysers */\n\n\t/**\n\t * Polls data from connected WAAPI analyser return structured object with data and time data in arrays\n\t * @param {*} analyser\n\t */\n\tpollAnalyserData(analyser) {\n\t\tif (analyser !== undefined) {\n\t\t\tconst timeDataArray = new Uint8Array(analyser.fftSize); // Uint8Array should be the same length as the fftSize\n\t\t\tconst frequencyDataArray = new Uint8Array(analyser.fftSize);\n\n\t\t\tanalyser.getByteTimeDomainData(timeDataArray);\n\t\t\tanalyser.getByteFrequencyData(frequencyDataArray);\n\n\t\t\treturn {\n\t\t\t\tsmoothingTimeConstant: analyser.smoothingTimeConstant,\n\t\t\t\tfftSize: analyser.fftSize,\n\t\t\t\tfrequencyDataArray: frequencyDataArray,\n\t\t\t\ttimeDataArray: timeDataArray,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Creates a WAAPI analyser node\n\t * @todo configuration object as argumen\n\t * @createAnalyser\n\t */\n\tcreateAnalyser(analyserID, callback) {\n\t\t// If Analyser creation happens after AudioContext intialization, create and connect WAAPI analyser\n\t\tif (analyserID && callback){\n if(this.audioContext && this.audioWorkletNode ) {\n\n let analyser = this.audioContext.createAnalyser();\n analyser.smoothingTimeConstant = 0.25;\n analyser.fftSize = 256; // default 2048;\n analyser.minDecibels = -90; // default\n analyser.maxDecibels = -0; // default -10; max 0\n this.audioWorkletNode.connect(analyser);\n\n let analyserFrameId = -1,\n analyserData = {};\n\n this.analysers[analyserID] = {\n analyser,\n analyserFrameId,\n callback,\n };\n\n /**\n * Creates requestAnimationFrame loop for polling data and publishing\n * Returns Analyser Frame ID for adding to Analysers hash\n * and cancelling animation frame\n */\n const analyserPollingLoop = () => {\n analyserData = this.pollAnalyserData(\n this.analysers[analyserID].analyser\n );\n this.analysers[analyserID].callback(analyserData); // Invoke callback that carries\n // This will guarantee feeding poll request at steady animation framerate\n this.analysers[analyserID].analyserFrameId = requestAnimationFrame(\n analyserPollingLoop\n );\n return analyserFrameId;\n };\n\n analyserPollingLoop();\n\n\t\t\t// Other if AudioContext is NOT created yet (after app load, before splashScreen click)\n } else {\n this.analysers[analyserID] = { callback };\n }\n } else throw new Error('Parameters to createAnalyser incorrect')\n\t}\n\n\t/**\n\t * Connects WAAPI analyser nodes to the main audio worklet for visualisation.\n\t * @connectAnalysers\n\t */\n\tconnectAnalysers() {\n\t\tObject.keys(this.analysers).map((id) =>\n\t\t\tthis.createAnalyser(id, this.analysers[id].callback)\n\t\t);\n\t}\n\n\t/**\n\t * Removes a WAAPI analyser node, disconnects graph, cancels animation frame, deletes from hash\n\t * @removeAnalyser\n\t */\n\tremoveAnalyser(event) {\n\t\tif ( this.audioContext && this.audioWorkletNode ) {\n\t\t\tlet analyser = this.analysers[event.id];\n\t\t\tif (analyser !== undefined) {\n\t\t\t\tcancelAnimationFrame(this.analysers[event.id].analyserFrameId);\n\t\t\t\tdelete this.analysers[event.id];\n\t\t\t\t// this.audioWorkletNode.disconnect(analyser);\n\t\t\t}\n\t\t}\n\t}\n\n\t/* #endregion */\n\n\t/**\n\t * Initialises audio context and sets worklet processor code\n\t * @play\n\t */\n\tasync init(origin) {\n\t\tif (origin && new URL(origin)) {\n\n let isWorkletProcessorLoaded;\n\n try{\n // AudioContext needs lazy loading to workaround the Chrome warning\n // Audio Engine first play() call, triggered by user, prevents the warning\n // by setting this.audioContext = new AudioContext();\n this.audioContext;\n this.origin = origin;\n this.audioWorkletName = \"maxi-processor\";\n this.audioWorkletUrl = origin + \"/\" + this.audioWorkletName + \".js\";\n\n if (this.audioContext === undefined) {\n this.audioContext = new AudioContext({\n // create audio context with latency optimally configured for playback\n latencyHint: \"playback\",\n // latencyHint: 32/44100, //this doesn't work below 512 on chrome (?)\n // sampleRate: 48000\n });\n }\n\n isWorkletProcessorLoaded = await this.loadWorkletProcessorCode();\n }\n catch(err){\n return false;\n }\n\n\t\t\tif (isWorkletProcessorLoaded) {\n\n\t\t\t\tthis.connectWorkletNode();\n\n\t\t\t\tconsole.log(\n\t\t\t\t\t\"running %csema-engine v0.1.1\",\n\t\t\t\t\t\"font-weight: bold; color: #ffb7c5\"\n\t\t\t\t\t// \"font-weight: bold; background: #000; color: #bada55\"\n\t\t\t\t);\n\n\t\t\t\treturn true;\n\t\t\t} else return false;\n\n\t\t\t// No need to inject the callback here, messaging is built in KuraClock\n\t\t\t// this.kuraClock = new kuramotoNetClock((phase, idx) => {\n\t\t\t// // console.log( `DEBUG:AudioEngine:sendPeersMyClockPhase:phase:${phase}:id:${idx}`);\n\t\t\t// // This requires an initialised audio worklet\n\t\t\t// this.audioWorkletNode.port.postMessage({ phase: phase, i: idx });\n\t\t\t// });\n\t\t\t// if (this.kuraClock.connected()) {\n\t\t\t// \tthis.kuraClock.queryPeers(async numClockPeers => {\n\t\t\t// \t\tconsole.log(`DEBUG:AudioEngine:init:numClockPeers: ${numClockPeers}`);\n\t\t\t// \t});\n\t\t\t// }\n\t\t} else {\n\t\t\tthrow new Error(\"Name and valid URL required for AudioWorklet processor\");\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Initialises audio context and sets worklet processor code\n\t * or re-starts audio playback by stopping and running the latest Audio Worklet Processor code\n\t * @play\n\t */\n\tplay() {\n\t\tif (this.audioContext !== undefined) {\n\t\t\tif (this.audioContext.state === \"suspended\") {\n\t\t\t\tthis.audioContext.resume();\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\tthis.hush();\n\t\t\t\t// this.stop();\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Suspends AudioContext (Pause)\n\t * @stop\n\t */\n\tstop() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tthis.hush();\n\t\t\t// this.audioContext.suspend();\n\t\t}\n\t}\n\n\t/**\n\t * Stops audio by disconnecting AudioNode with AudioWorkletProcessor code\n\t * from Web Audio graph TODO Investigate when it is best to just STOP the graph exectution\n\t * @stop\n\t */\n\tstopAndRelease() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tthis.audioWorkletNode.disconnect(this.audioContext.destination);\n\t\t\tthis.audioWorkletNode = undefined;\n\t\t}\n\t}\n\n\tsetGain(gain) {\n\t\tif (this.audioWorkletNode !== undefined && gain >= 0 && gain <= 1) {\n\t\t\tconst gainParam = this.audioWorkletNode.parameters.get(\"gain\");\n\t\t\tgainParam.value = gain;\n\t\t\tconsole.log(gainParam.value); // DEBUG\n\t\t\treturn true;\n\t\t} else return false;\n\t}\n\n\tmore() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tconst gainParam = this.audioWorkletNode.parameters.get(\"gain\");\n\t\t\tgainParam.value += 0.05;\n\t\t\tconsole.info(gainParam.value); // DEBUG\n\t\t\treturn gainParam.value;\n\t\t} else throw new Error(\"error increasing sound level\");\n\t}\n\n\tless() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tconst gainParam = this.audioWorkletNode.parameters.get(\"gain\");\n\t\t\tgainParam.value -= 0.05;\n\t\t\tconsole.info(gainParam.value); // DEBUG\n\t\t\treturn gainParam.value;\n\t\t} else throw new Error(\"error decreasing sound level\");\n\t}\n\n\thush() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\thush: 1,\n\t\t\t});\n this.isHushed = true;\n\t\t\treturn true;\n\t\t} else return false;\n\t}\n\n\tunHush() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\tunhush: 1,\n\t\t\t});\n this.isHushed = false;\n\t\t\treturn true;\n\t\t} else return false;\n\t}\n\n\teval(dspFunction) {\n\t\tif (this.audioWorkletNode && this.audioWorkletNode.port) {\n\t\t\tif (this.audioContext.state === \"suspended\") {\n\t\t\t\tthis.audioContext.resume();\n\t\t\t}\n\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\teval: 1,\n\t\t\t\tsetup: dspFunction.setup,\n\t\t\t\tloop: dspFunction.loop,\n\t\t\t});\n this.isHushed = false;\n\t\t\treturn true;\n\t\t} else return false;\n\t}\n\n\t/**\n\t * Handler of the Pub/Sub message events\n\t * whose topics are subscribed to in the audio engine constructor\n\t * @asyncPostToProcessor\n\t * @param {*} event\n\t */\n\tasyncPostToProcessor(event) {\n\t\tif (event && this.audioWorkletNode && this.audioWorkletNode.port) {\n\t\t\t// Receive notification from 'model-output-data' topic\n\t\t\tconsole.log(\"DEBUG:AudioEngine:onMessagingEventHandler:\");\n\t\t\tconsole.log(event);\n\t\t\tthis.audioWorkletNode.port.postMessage(event);\n\t\t} else throw new Error(\"Error async posting to processor\");\n\t}\n\n\tsendClockPhase(phase, idx) {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\tthis.audioWorkletNode.port.postMessage({\n\t\t\t\tphase: phase,\n\t\t\t\ti: idx,\n\t\t\t});\n\t\t}\n\t}\n\n\tonAudioInputInit(stream) {\n try {\n this.mediaStreamSource = this.audioContext.createMediaStreamSource(stream);\n this.mediaStreamSource.connect(this.audioWorkletNode);\n this.mediaStream = stream;\n this.mediaStreamSourceConnected = true;\n } catch (error) {\n console.error(error);\n }\n\t}\n\n\tonAudioInputFail(error) {\n\t\tthis.mediaStreamSourceConnected = false;\n\t\tconsole.error(\n\t\t\t`ERROR:Engine:AudioInputFail: ${error.message} ${error.name}`\n\t\t);\n\t}\n\n\tonAudioInputDisconnect() {\n\n\t}\n\n\t/**\n\t * Sets up an AudioIn WAAPI sub-graph\n\t * @connectMediaStreamSourceInput\n\t */\n\tasync connectMediaStream() {\n\t\tconst constraints = ( window.constraints = {\n audio: {\n latency: 0.02,\n echoCancellation: false,\n mozNoiseSuppression: false,\n mozAutoGainControl: false\n },\n\t\t\tvideo: false,\n\t\t});\n // onAudioInputDisconnect();\n\t\tawait navigator.mediaDevices\n\t\t\t.getUserMedia(constraints)\n\t\t\t.then( s => this.onAudioInputInit(s) )\n\t\t\t.catch(this.onAudioInputFail);\n\n\t\treturn this.mediaStreamSourceConnected;\n\t}\n\n\t/**\n\t * Breaks up an AudioIn WAAPI sub-graph\n\t * @disconnectMediaStreamSourceInput\n\t */\n\tasync disconnectMediaStream() {\n\n try {\n\t\t\tthis.mediaStreamSource.disconnect(this.audioWorkletNode);\n\t\t\tthis.mediaStream.getAudioTracks().forEach((at) => at.stop());\n\t\t\tthis.mediaStreamSource = null;\n\t\t\tthis.mediaStreamSourceConnected = false;\n\t\t} catch (error) {\n\t\t\tconsole.error(error);\n\t\t}\n finally {\n return this.mediaStreamSourceConnected;\n }\n\t\t// await navigator.mediaDevices\n\t\t// \t.getUserMedia(constraints)\n\t\t// \t.then((s) => this.onAudioInputDisconnect(s))\n\t\t// \t.catch(this.onAudioInputFail);\n\n\n\t}\n\n\t/**\n\t * Loads audioWorklet processor code into a worklet,\n\t * setups up all handlers (errors, async messaging, etc),\n\t * connects the worklet processor to the WAAPI graph\n\t */\n\tasync loadWorkletProcessorCode() {\n\t\tif (this.audioContext !== undefined) {\n\t\t\ttry {\n\t\t\t\tawait this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t\"ERROR:Engine:loadWorkletProcessorCode: AudioWorklet not supported in this browser: \",\n\t\t\t\t\terr.message\n\t\t\t\t);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// Custom node constructor with required parameters\n\t\t\t\t// this.audioWorkletNode = new CustomMaxiNode(\n\t\t\t\tthis.audioWorkletNode = new AudioWorkletNode(\n\t\t\t\t\tthis.audioContext,\n\t\t\t\t\tthis.audioWorkletName\n\t\t\t\t);\n\n\t\t\t\tthis.audioWorkletNode.channelInterpretation = \"discrete\";\n\t\t\t\tthis.audioWorkletNode.channelCountMode = \"explicit\";\n\t\t\t\tthis.audioWorkletNode.channelCount = this.audioContext.destination.maxChannelCount;\n\n\t\t\t\treturn true;\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"Error loading worklet processor code: \", err);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} else {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * connects all error event handlers and default processor message callback\n\t */\n\tconnectWorkletNode() {\n\t\tif (this.audioWorkletNode !== undefined) {\n\t\t\ttry {\n\t\t\t\tthis.audioContext.destination.channelInterpretation = \"discrete\";\n\t\t\t\tthis.audioContext.destination.channelCountMode = \"explicit\";\n\t\t\t\tthis.audioContext.destination.channelCount = this.audioContext.destination.maxChannelCount;\n\n\t\t\t\t// Connect the worklet node to the audio graph\n\t\t\t\tthis.audioWorkletNode.connect(this.audioContext.destination);\n\n\t\t\t\t// All possible error event handlers subscribed\n\t\t\t\tthis.audioWorkletNode.onprocessorerror = (e) =>\n\t\t\t\t\t// Errors from the processor\n\t\t\t\t\tconsole.error(`Engine processor error detected`, e);\n\n\t\t\t\t// Subscribe state changes in the audio worklet processor\n\t\t\t\tthis.audioWorkletNode.onprocessorstatechange = (e) =>\n\t\t\t\t\tconsole.info(\n\t\t\t\t\t\t`Engine processor state change: ` + audioWorkletNode.processorState\n\t\t\t\t\t);\n\n\t\t\t\t// Subscribe errors from the processor port\n\t\t\t\tthis.audioWorkletNode.port.onmessageerror = (e) =>\n\t\t\t\t\tconsole.error(`Engine processor port error: ` + e);\n\n\t\t\t\t// Default worklet processor message handler\n\t\t\t\t// gets replaced by user callback with 'subscribeAsyncMessage'\n\t\t\t\tthis.audioWorkletNode.port.onmessage = (e) =>\n\t\t\t\t\tthis.onProcessorMessageHandler(e);\n\n\t\t\t} catch (err) {\n\t\t\t\tconsole.error(\"Error connecting WorkletNode: \", err);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Default worklet processor message handler\n\t * gets replaced by user-supplied callback through 'subscribeProcessorMessage'\n\t * @param {*} event\n\t */\n\tonProcessorMessageHandler(event) {\n\t\tif (event && event.data) {\n\t\t\tif (event.data.rq && event.data.rq === \"send\") {\n\t\t\t\tswitch (event.data.ttype) {\n\t\t\t\t\tcase \"ML\":\n\t\t\t\t\t\t// this.messaging.publish(\"model-input-data\", {\n\t\t\t\t\t\t// type: \"model-input-data\",\n\t\t\t\t\t\t// value: event.data.value,\n\t\t\t\t\t\t// ch: event.data.ch\n\t\t\t\t\t\t// });\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"NET\":\n\t\t\t\t\t\tthis.peerNet.send(\n\t\t\t\t\t\t\tevent.data.ch[0],\n\t\t\t\t\t\t\tevent.data.value,\n\t\t\t\t\t\t\tevent.data.ch[1]\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else if (event.data.rq && event.data.rq === \"buf\") {\n\t\t\t\tswitch (event.data.ttype) {\n\t\t\t\t\tcase \"ML\":\n\t\t\t\t\t\tthis.dispatcher.dispatch(\"onSharedBuffer\", {\n\t\t\t\t\t\t\tsab: event.data.value,\n\t\t\t\t\t\t\tchannelID: event.data.channelID, //channel ID\n\t\t\t\t\t\t\tblocksize: event.data.blocksize,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"scope\":\n\t\t\t\t\t\t// this.dispatcher.dispatch(\"onSharedBuffer\", {\n\t\t\t\t\t\t// \tsab: event.data.value,\n\t\t\t\t\t\t// \tchannelID: event.data.channelID, //channel ID\n\t\t\t\t\t\t// \tblocksize: event.data.blocksize,\n\t\t\t\t\t\t// });\n\n\t\t\t\t\t\tlet ringbuf = new RingBuffer(event.data.value, Float64Array);\n\n\t\t\t\t\t\tthis.sharedArrayBuffers[event.data.channelID] = {\n\t\t\t\t\t\t\tsab: event.data.value, // TODO: this is redundant, you can access the sab from the rb,\n\t\t\t\t\t\t\t// TODO change hashmap name it is confusing and induces error\n\t\t\t\t\t\t\trb: ringbuf,\n\t\t\t\t\t\t\tttype: event.data.ttype,\n\t\t\t\t\t\t\tchannelID: event.data.channelID, //channel ID\n\t\t\t\t\t\t\tblocksize: event.data.blocksize,\n\t\t\t\t\t\t};\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else if (event.data.rq && event.data.rq === \"rts\") { // ready to suspend\n\t\t \tthis.audioContext.suspend();\n this.isHushed = true;\n\t\t }\n else if (event.data instanceof Error){\n // TODO use a logger to inject error\n console.error(`On Processor Message ${event.data}`);\n }\n\t\t}\n\t}\n\n\t/**\n\t * Public method for subscribing async messaging from the Audio Worklet Processor scope\n\t * @param callback\n\t */\n\tsubscribeProcessorMessage(callback) {\n\t\tif (callback && this.audioWorkletNode)\n\t\t\tthis.audioWorkletNode.port.onmessage = callback;\n\t\telse throw new Error(\"Error subscribing processor message\");\n\t}\n\n\t/**\n\t * Load individual audio sample, assuming an origin URL with which the engine\n\t * is initialised\n\t * @param {*} objectName name of the sample\n\t * @param {*} url relative URL to the origin URL, startgin with `/`\n\t */\n\tloadSample(objectName, url) {\n\t\tif (this.audioContext && this.audioWorkletNode) {\n\t\t\tif (\n\t\t\t\turl &&\n\t\t\t\turl.length !== 0 &&\n\t\t\t\tthis.origin &&\n\t\t\t\tthis.origin.length !== 0 &&\n\t\t\t\tnew URL(this.origin + url)\n\t\t\t) {\n\t\t\t\ttry {\n\t\t\t\t\tloadSampleToArray(\n\t\t\t\t\t\tthis.audioContext,\n\t\t\t\t\t\tobjectName,\n\t\t\t\t\t\tthis.origin + url,\n\t\t\t\t\t\tthis.audioWorkletNode\n\t\t\t\t\t);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`Error loading sample ${objectName} from ${url}: `,\n\t\t\t\t\t\terror\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else throw \"Problem with sample relative URL\";\n\t\t} else throw \"Engine is not initialised!\";\n\t}\n}\n","(function(root, factory) {\n if (typeof module === 'object' && module.exports) {\n module.exports = factory();\n } else {\n root.nearley = factory();\n }\n}(this, function() {\n\n function Rule(name, symbols, postprocess) {\n this.id = ++Rule.highestId;\n this.name = name;\n this.symbols = symbols; // a list of literal | regex class | nonterminal\n this.postprocess = postprocess;\n return this;\n }\n Rule.highestId = 0;\n\n Rule.prototype.toString = function(withCursorAt) {\n var symbolSequence = (typeof withCursorAt === \"undefined\")\n ? this.symbols.map(getSymbolShortDisplay).join(' ')\n : ( this.symbols.slice(0, withCursorAt).map(getSymbolShortDisplay).join(' ')\n + \" ● \"\n + this.symbols.slice(withCursorAt).map(getSymbolShortDisplay).join(' ') );\n return this.name + \" → \" + symbolSequence;\n }\n\n\n // a State is a rule at a position from a given starting point in the input stream (reference)\n function State(rule, dot, reference, wantedBy) {\n this.rule = rule;\n this.dot = dot;\n this.reference = reference;\n this.data = [];\n this.wantedBy = wantedBy;\n this.isComplete = this.dot === rule.symbols.length;\n }\n\n State.prototype.toString = function() {\n return \"{\" + this.rule.toString(this.dot) + \"}, from: \" + (this.reference || 0);\n };\n\n State.prototype.nextState = function(child) {\n var state = new State(this.rule, this.dot + 1, this.reference, this.wantedBy);\n state.left = this;\n state.right = child;\n if (state.isComplete) {\n state.data = state.build();\n // Having right set here will prevent the right state and its children\n // form being garbage collected\n state.right = undefined;\n }\n return state;\n };\n\n State.prototype.build = function() {\n var children = [];\n var node = this;\n do {\n children.push(node.right.data);\n node = node.left;\n } while (node.left);\n children.reverse();\n return children;\n };\n\n State.prototype.finish = function() {\n if (this.rule.postprocess) {\n this.data = this.rule.postprocess(this.data, this.reference, Parser.fail);\n }\n };\n\n\n function Column(grammar, index) {\n this.grammar = grammar;\n this.index = index;\n this.states = [];\n this.wants = {}; // states indexed by the non-terminal they expect\n this.scannable = []; // list of states that expect a token\n this.completed = {}; // states that are nullable\n }\n\n\n Column.prototype.process = function(nextColumn) {\n var states = this.states;\n var wants = this.wants;\n var completed = this.completed;\n\n for (var w = 0; w < states.length; w++) { // nb. we push() during iteration\n var state = states[w];\n\n if (state.isComplete) {\n state.finish();\n if (state.data !== Parser.fail) {\n // complet