diff --git a/dist/Superpowered.js b/dist/Superpowered.js index 79e9341..3f59111 100644 --- a/dist/Superpowered.js +++ b/dist/Superpowered.js @@ -3,7 +3,7 @@ class SuperpoweredGlue { - static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.7/dist/superpowered-npm.wasm" + static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.8/dist/superpowered-npm.wasm" niceSize(bytes) { if (bytes == 0) return '0 byte'; else if (bytes == 1) return '1 byte'; diff --git a/dist/superpowered-npm.wasm b/dist/superpowered-npm.wasm index 03832bd..4c72914 100755 Binary files a/dist/superpowered-npm.wasm and b/dist/superpowered-npm.wasm differ diff --git a/dist/superpowered.wasm b/dist/superpowered.wasm index 993ce29..7b00ccd 100755 Binary files a/dist/superpowered.wasm and b/dist/superpowered.wasm differ diff --git a/examples/example_effects/Superpowered.js b/examples/example_effects/Superpowered.js index 79e9341..3f59111 100644 --- a/examples/example_effects/Superpowered.js +++ b/examples/example_effects/Superpowered.js @@ -3,7 +3,7 @@ class SuperpoweredGlue { - static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.7/dist/superpowered-npm.wasm" + static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.8/dist/superpowered-npm.wasm" niceSize(bytes) { if (bytes == 0) return '0 byte'; else if (bytes == 1) return '1 byte'; diff --git a/examples/example_guitardistortion/Superpowered.js b/examples/example_guitardistortion/Superpowered.js index 79e9341..3f59111 100644 --- a/examples/example_guitardistortion/Superpowered.js +++ b/examples/example_guitardistortion/Superpowered.js @@ -3,7 +3,7 @@ class SuperpoweredGlue { - static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.7/dist/superpowered-npm.wasm" + static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.8/dist/superpowered-npm.wasm" niceSize(bytes) { if (bytes == 0) return '0 byte'; else if (bytes == 1) return '1 byte'; diff --git a/examples/example_pitchbend/Superpowered.js b/examples/example_pitchbend/Superpowered.js new file mode 100644 index 0000000..3f59111 --- /dev/null +++ b/examples/example_pitchbend/Superpowered.js @@ -0,0 +1,869 @@ +/* eslint-disable */ +// @ts-nocheck + +class SuperpoweredGlue { + + static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.8/dist/superpowered-npm.wasm" + + niceSize(bytes) { + if (bytes == 0) return '0 byte'; else if (bytes == 1) return '1 byte'; + const postfix = [ ' bytes', ' kb', ' mb', ' gb', ' tb' ], n = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); + return Math.round(bytes / Math.pow(1024, n), 2) + postfix[n]; + } + + createFloatArray(length) { + return this.createViewFromType(9, this.malloc(length * 4), length); + } + + static async Instantiate(licenseKey, wasmUrl = SuperpoweredGlue.wasmCDNUrl) { + SuperpoweredGlue.wasmCDNUrl = wasmUrl; + const obj = new SuperpoweredGlue(); + await fetch(wasmUrl).then(response => response.arrayBuffer() ).then(bytes => obj.loadFromArrayBuffer(bytes) ); + obj.Initialize(licenseKey); + return obj; + } + + constructor() { + this.id = Math.floor(Math.random() * Date.now()); + this.linearMemory = null; + this.__lastObject__ = null; + this.__classUnderConstruction__ = null; + this.__functions__ = {}; + this.__classes__ = {}; + this.__exportsToWasm__ = {}; + this.__views__ = new Set(); + this.trackLoaderReceivers = []; + + const glue = this; + this.Uint8Buffer = class { constructor(length) { return glue.createViewFromType(1, glue.malloc(length), length); } } + this.Int8Buffer = class { constructor(length) { return glue.createViewFromType(2, glue.malloc(length), length); } } + this.Uint16Buffer = class { constructor(length) { return glue.createViewFromType(3, glue.malloc(length * 2), length); } } + this.Int16Buffer = class { constructor(length) { return glue.createViewFromType(4, glue.malloc(length * 2), length); } } + this.Uint32Buffer = class { constructor(length) { return glue.createViewFromType(5, glue.malloc(length * 4), length); } } + this.Int32Buffer = class { constructor(length) { return glue.createViewFromType(6, glue.malloc(length * 4), length); } } + this.BigUint64Buffer = class { constructor(length) { return glue.createViewFromType(7, glue.malloc(length * 8), length); } } + this.BigInt64Buffer = class { constructor(length) { return glue.createViewFromType(8, glue.malloc(length * 8), length); } } + this.Float32Buffer = class { constructor(length) { return glue.createViewFromType(9, glue.malloc(length * 4), length); } } + this.Float64Buffer = class { constructor(length) { return glue.createViewFromType(10, glue.malloc(length * 8), length); } } + + this.__exportsToWasm__.consolelog = this.consolelog.bind(this); + this.__exportsToWasm__.emscripten_notify_memory_growth = this.onMemoryGrowth.bind(this); + + this.__exportsToWasm__.__createClass__ = this.createClass.bind(this); + this.__exportsToWasm__.__createStaticProperty__ = this.createStaticProperty.bind(this); + this.__exportsToWasm__.__createStaticMethod__ = this.createStaticMethod.bind(this); + this.__exportsToWasm__.__createConstructor__ = this.createConstructor.bind(this); + this.__exportsToWasm__.__createDestructor__ = this.createDestructor.bind(this); + this.__exportsToWasm__.__createProperty__ = this.createProperty.bind(this); + this.__exportsToWasm__.__createMethod__ = this.createMethod.bind(this); + this.__exportsToWasm__.__createFunction__ = this.createFunction.bind(this); + this.__exportsToWasm__.__createClassConstant__ = this.createClassConstant.bind(this); + this.__exportsToWasm__.__createConstant__ = this.createConstant.bind(this); + this.__exportsToWasm__.__runjs__ = function(pointer) { + return eval(this.toString(pointer)); + }.bind(this); + + this.__exportsToWasm__.abs = function(value) { return Math.abs(value); } + this.__exportsToWasm__.round = function(value) { return Math.round(value); } + this.__exportsToWasm__.roundf = function(value) { return Math.fround(value); } + + this.wasi = { + proc_exit: function() { console.log('abort'); }, + }; + } + + updateBuffer(buffer, arraybuffer) { + buffer.__arraybuffer__ = arraybuffer; + switch (buffer.__type__) { + case 1: buffer.array = new Uint8Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) : buffer.length); break; + case 2: buffer.array = new Int8Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) : buffer.length); break; + case 3: buffer.array = new Uint16Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 2 : buffer.length); break; + case 4: buffer.array = new Int16Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 2 : buffer.length); break; + case 5: buffer.array = new Uint32Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 4 : buffer.length); break; + case 6: buffer.array = new Int32Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 4 : buffer.length); break; + case 7: buffer.array = new BigUint64Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 8 : buffer.length); break; + case 8: buffer.array = new BigInt64Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 8 : buffer.length); break; + case 9: buffer.array = new Float32Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 4 : buffer.length); break; + case 10: buffer.array = new Float64Array(buffer.__arraybuffer__, buffer.pointer, (buffer.length < 0) ? (buffer.__arraybuffer__.byteLength - buffer.pointer) / 8 : buffer.length); break; + } + } + + createViewFromType(type, pointer, length) { + const buffer = { + pointer: pointer, + length: length, + __arraybuffer__: this.linearMemory, + __type__: type, + __glue__: this, + free() { + this.__glue__.free(this.pointer); + Object.getOwnPropertyNames(this).forEach((property) => delete this[property] ); + Object.setPrototypeOf(this, null); + } + }; + this.updateBuffer(buffer, this.linearMemory); + this.__views__.add(buffer); + return buffer; + } + + returnPointerToView(r, type) { + if ((type > 0) && (typeof r !== 'undefined')) { + const length = this.__functions__.__lastarraylength__(); + r = this.createViewFromType(type, r, length > 0 ? length : -1); + } + return r; + } + + invokeMethod() { + if ((arguments.length == 2) && (typeof arguments[1] == 'object')) { + const obj = arguments[1]; let n = 1; + for (const m in obj) arguments[n++] = obj[m]; + arguments.length = n; + } + const strings = []; + for (let index = arguments.length - 1; index > 0; index--) { + if (arguments[index].array != undefined) arguments[index] = arguments[index].array.byteOffset; + else if (arguments[index].__pointer__ != undefined) arguments[index] = arguments[index].__pointer__; + else if (typeof arguments[index] == 'string') { + arguments[index] = this.__glue__.toWASMString(arguments[index]); + strings.push(arguments[index]); + } + } + const info = arguments[0]; + arguments[0] = this.__pointer__; + let r = info.function.apply(this, arguments); + for (const string of strings) this.__glue__.free(string); + r = this.__glue__.returnPointerToView(r, info.returnPointerType); + return r; + } + + invokeFunction() { + if ((arguments.length == 1) && (typeof arguments[0] == 'object')) { + const obj = arguments[0]; let n = 0; + for (const m in obj) arguments[n++] = obj[m]; + arguments.length = n; + } + const strings = []; + for (let index = arguments.length - 1; index >= 0; index--) { + if (arguments[index].array != undefined) arguments[index] = arguments[index].array.byteOffset; + else if (arguments[index].__pointer__ != undefined) arguments[index] = arguments[index].__pointer__; + else if (typeof arguments[index] == 'string') { + arguments[index] = this.glue.toWASMString(arguments[index]); + strings.push(arguments[index]); + } + } + let r = this.apply(this, arguments); + for (const string of strings) this.glue.free(string); + r = this.glue.returnPointerToView(r, this.returnPointerType); + return r; + } + + invokeExportedFunction() { + let r = this.apply(this, arguments); + if (r.array !== undefined) r = r.array.byteOffset; + return r; + } + + createClass(classnamePointer, classnameLen, sizeofClass) { + const glue = this, classname = glue.toString(classnamePointer, classnameLen); + const WASM = class { + constructor() { + const meta = Object.getPrototypeOf(this).constructor.__meta__; + if (!meta.hasConstructor) throw meta.name + ' has no constructor'; + + this.__class__ = meta.name; + this.__prev__ = glue.__lastObject__; + if (glue.__lastObject__ != null) glue.__lastObject__.__next__ = this; + this.__next__ = null; + this.__glue__ = glue; + glue.__lastObject__ = this; + + const args = [].slice.call(arguments); + args.unshift(glue.malloc(meta.size)); + this.__pointer__ = glue[meta.name + '::' + meta.name].apply(null, args); + + for (const property of meta.properties) glue.createPropertyFromDescriptor(this, property); + for (const method of meta.methods) this[method.name] = glue.invokeMethod.bind(this, { function: glue[method.wasmFunction], returnPointerType: method.returnPointerType }); + } + destruct() { + const meta = Object.getPrototypeOf(this).constructor.__meta__; + if (meta.hasDestructor) glue[meta.name + '::~' + meta.name](this.__pointer__); + glue.free(this.__pointer__); + if (this.__prev__ != null) this.__prev__.__next__ = this.__next__; + if (this.__next__ != null) this.__next__.__prev__ = this.__prev__; + Object.getOwnPropertyNames(this).forEach((property) => delete this[property] ); + Object.setPrototypeOf(this, null); + } + } + glue.__classUnderConstruction__ = glue.__classes__[classname] = glue[classname] = WASM; + glue.__classUnderConstruction__.__meta__ = { + name: classname, + size: sizeofClass, + hasConstructor: false, + hasDestructor: false, + properties: [], + methods: [], + staticProperties: [] + } + delete glue.__functionsWithNamespace__[classname]; + } + + createConstructor() { + this.__classUnderConstruction__.__meta__.hasConstructor = true; + } + + createDestructor() { + this.__classUnderConstruction__.__meta__.hasDestructor = this.__classUnderConstruction__.__meta__.hasConstructor; + } + + createClassConstant(nameptr, namelen, value) { + this.__classUnderConstruction__[this.toString(nameptr, namelen)] = value; + } + + createConstant(nameptr, namelen, value) { + this[this.toString(nameptr, namelen)] = value; + } + + createPropertyFromDescriptor(object, descriptor) { + const buffer = this.createViewFromType(descriptor.viewType, object.__pointer__ + descriptor.offset, descriptor.viewLength); + if (descriptor.viewLength > 1) Object.defineProperty(object, descriptor.name, { + get: function() { return buffer.array; }, + set: function(value) { buffer.array[index] = value; }, + configurable: true, + enumerable: true + }); else Object.defineProperty(object, descriptor.name, { + get: function() { return buffer.array[0]; }, + set: function(value) { buffer.array[0] = value; }, + configurable: true, + enumerable: true + }); + } + + createProperty(propertynamePointer, propertynameLen, offset, viewType, viewLength) { + this.__classUnderConstruction__.__meta__.properties.push({ + name: this.toString(propertynamePointer, propertynameLen), + offset: offset, + viewType: viewType, + viewLength: viewLength + }); + } + + createStaticPropertyFromDescriptor(wasmClass, descriptor) { + const buffer = this.createViewFromType(descriptor.viewType, descriptor.pointer, descriptor.viewLength); + if (descriptor.viewLength > 1) Object.defineProperty(wasmClass, descriptor.name, { + get: function() { return buffer.array; }, + set: function(value) { buffer.array[index] = value; }, + configurable: true, + enumerable: true + }); else Object.defineProperty(wasmClass, descriptor.name, { + get: function() { return buffer.array[0]; }, + set: function(value) { buffer.array[0] = value; }, + configurable: true, + enumerable: true + }); + } + + createStaticProperty(propertynamePointer, propertynameLen, pointer, viewType, viewLength) { + const descriptor = { + name: this.toString(propertynamePointer, propertynameLen), + pointer: pointer, + viewType: viewType, + viewLength: viewLength + }; + this.__classUnderConstruction__.__meta__.staticProperties.push(descriptor); + this.createStaticPropertyFromDescriptor(this.__classUnderConstruction__, descriptor); + } + + createMethod(methodnamePointer, methodnameLen, returnPointerType) { + const methodname = this.toString(methodnamePointer, methodnameLen); + this.__classUnderConstruction__.__meta__.methods.push({ + name: methodname, + wasmFunction: this.__classUnderConstruction__.__meta__.name + '::' + methodname, + returnPointerType: returnPointerType + }); + } + + createStaticMethod(methodnamePointer, methodnameLen, returnPointerType) { + const methodname = this.toString(methodnamePointer, methodnameLen), wasmMethodname = this.__classUnderConstruction__.__meta__.name + '::' + methodname; + this[wasmMethodname].returnPointerType = returnPointerType; + this[wasmMethodname].glue = this; + this.__classUnderConstruction__[methodname] = this.invokeFunction.bind(this[wasmMethodname]); + } + + createFunction(methodnamePointer, methodnameLen, returnPointerType) { + const methodname = this.toString(methodnamePointer, methodnameLen); + if (!this[methodname]) { // maybe this function is in a namespace + for (const namespace in this.__functionsWithNamespace__) { + if (this.__functionsWithNamespace__[namespace][methodname]) { + this[methodname] = this.__functionsWithNamespace__[namespace][methodname]; + delete this.__functionsWithNamespace__[namespace][methodname]; + break; + } + } + if (!this[methodname]) return; + } + this[methodname].returnPointerType = returnPointerType; + this[methodname].glue = this; + this[methodname] = this.invokeFunction.bind(this[methodname]); + } + + exportToWasm(functionName, f) { + this.__exportsToWasm__[functionName] = this.invokeExportedFunction.bind(f); + } + + onMemoryGrowth(n) { + this.linearMemory = this.wasmInstance.exports.memory.buffer; + if (this.__memorygrowview__.buffer.byteLength < 1) this.updateMemoryViews(); + this.logMemory(); + } + + consolelog(pointer, strlen) { + console.log(this.toString(pointer, strlen)); + } + + setInstance(wasmInstance) { + this.wasmInstance = wasmInstance; + this.wasmInstance.exports._initialize(); + + this.__functions__ = this.wasmInstance.exports; + this.linearMemory = this.wasmInstance.exports.memory.buffer; + this.__memorygrowpointer__ = this.__functions__.__malloc__(16); + this.__memorygrowview__ = new Uint8Array(this.linearMemory, this.__memorygrowpointer__, 16); + this.__functionsWithNamespace__ = {}; + + const outputBuffer = this.__functions__.__malloc__(1024), stringview = new Uint8Array(this.linearMemory, this.__functions__.__malloc__(1024), 1024); + for (const f in this.__functions__) if (f != '__demangle__') { + const length = this.__functions__.__demangle__(this.toWASMString(f, stringview), outputBuffer); + if (length > 0) { + let name = this.toString(outputBuffer, length); + const par = name.indexOf('('); + if (par > 0) name = name.substring(0, par); + + let namespace = name.lastIndexOf('::'); + if (namespace > 0) { + namespace = name.lastIndexOf('::', namespace - 1); + if (namespace > 0) name = name.substr(namespace + 2); + } + + // class members have namespaces removed from this point, but functions not + const split = name.split('::', 2); + if (split.length == 2) { + if (!this.__functionsWithNamespace__[split[0]]) this.__functionsWithNamespace__[split[0]] = {}; + this.__functionsWithNamespace__[split[0]][split[1]] = this.__functions__[f]; + } + + this[name] = this.__functions__[f]; + } else this[f] = this.__functions__[f]; + } + this.free(outputBuffer); + this.free(stringview.byteOffset); + + this.__functions__.__initialize__(); + delete this.__functionsWithNamespace__; + this.logMemory(); + this.__classUnderConstruction__ = null; + } + + async loadFromArrayBuffer(wasmCode, afterWASMLoaded = null) { + this.wasmCode = wasmCode; + await WebAssembly.instantiate(wasmCode, { + wasi_snapshot_preview1: this.wasi, + env: this.__exportsToWasm__, + }).then((_module) => { + this.setInstance(_module.instance); + if (afterWASMLoaded != null) afterWASMLoaded.afterWASMLoaded(); + }); + } + + async loadFromModule(module) { + await WebAssembly.instantiate(module, { + wasi_snapshot_preview1: this.wasi, + env: this.__exportsToWasm__, + }).then((instance) => { + this.setInstance(instance); + }); + } + + async loadFromURL(url, storeCode = true) { + if (WebAssembly.instantiateStreaming) { + await WebAssembly.instantiateStreaming(fetch(url), { + wasi_snapshot_preview1: this.wasi, + env: this.__exportsToWasm__, + }).then((_module) => { + this.setInstance(_module.instance); + }); + if (storeCode) { + const wasmResponse = await fetch(url); + this.wasmCode = await wasmResponse.arrayBuffer(); + } + } + else { + const response = await fetch(url); + const wasmCode = await response.arrayBuffer(); + if (storeCode) { + this.wasmCode = wasmCode; + } + await WebAssembly.instantiate(wasmCode, { + wasi_snapshot_preview1: this.wasi, + env: this.__exportsToWasm__, + }).then((_module) => { + this.setInstance(_module.instance); + }); + } + } + + toString(pointer, strlen = 0) { + let view = null; + if (strlen < 1) { + const viewLength = Math.min(16384, this.linearMemory.byteLength - pointer); + view = new Uint8Array(this.linearMemory, pointer, viewLength); + for (strlen = 0; strlen < viewLength; strlen++) if (view[strlen] == 0) break; + } else view = new Uint8Array(this.linearMemory, pointer, strlen); + + let str = '', i = 0, bytesNeeded, codePoint; + while (i < strlen) { + const octet = view[i]; + bytesNeeded = codePoint = 0; + + if (octet <= 0x7f) { + bytesNeeded = 0; + codePoint = octet & 0xff; + } else if (octet <= 0xdf) { + bytesNeeded = 1; + codePoint = octet & 0x1f; + } else if (octet <= 0xef) { + bytesNeeded = 2; + codePoint = octet & 0x0f; + } else if (octet <= 0xf4) { + bytesNeeded = 3; + codePoint = octet & 0x07; + } + + if (strlen - i - bytesNeeded > 0) { + for (let k = 0; k < bytesNeeded; k++) codePoint = (codePoint << 6) | (view[i + k + 1] & 0x3f); + } else { + codePoint = 0xfffd; + bytesNeeded = strlen - i; + } + + str += String.fromCodePoint(codePoint); + i += bytesNeeded + 1; + } + return str; + } + + toWASMString(str, view = null) { + const length = str.length, maxBytes = length * 4 + 1; + let i = 0, c, bits, destination = 0; + if (view == null) { + const pointer = this.malloc(maxBytes); + view = new Uint8Array(this.linearMemory, pointer, maxBytes); + } + while (i < length) { + const codePoint = str.codePointAt(i); + c = bits = 0; + + if (codePoint <= 0x0000007f) { + c = 0; + bits = 0x00; + } else if (codePoint <= 0x000007ff) { + c = 6; + bits = 0xc0; + } else if (codePoint <= 0x0000ffff) { + c = 12; + bits = 0xe0; + } else if (codePoint <= 0x001fffff) { + c = 18; + bits = 0xf0; + } + + view[destination++] = bits | (codePoint >> c); + c -= 6; + while (c >= 0) { + view[destination++] = 0x80 | ((codePoint >> c) & 0x3f); + c -= 6; + } + i += (codePoint >= 0x10000) ? 2 : 1; + } + + view[destination] = 0; + return view.byteOffset; + } + + logMemory() { + console.log('WASM memory ' + this.id + ': ' + this.niceSize(this.__functions__.__stacksize__()) + ' stack, ' + this.niceSize(this.linearMemory.byteLength - this.__functions__.__heapbase__()) + ' heap, ' + this.niceSize(this.linearMemory.byteLength) + ' total.'); + } + + malloc(bytes) { + const pointer = this.__functions__.__malloc__(bytes); + if (this.__memorygrowview__.buffer.byteLength < 1) this.updateMemoryViews(); + return pointer; + } + + updateMemoryViews() { + for (const buffer of this.__views__) this.updateBuffer(buffer, this.linearMemory); + this.__memorygrowview__ = new Uint8Array(this.linearMemory, this.__memorygrowpointer__, 16); + } + + free(pointer) { + this.__functions__.__free__(pointer); + } + + setInt64(pointer, index, value) { + this.__functions__.__setint64__(pointer, index, value); + } + + bufferToWASM(buffer, input, index) { + let inBufferL = null, inBufferR = null; + if (index === undefined) index = 0; + if (typeof input.getChannelData === 'function') { + inBufferL = input.getChannelData(0); + inBufferR = input.getChannelData(1); + } else { + inBufferL = input[index][0]; + inBufferR = input[index][1]; + } + const arr = (buffer.constructor === Array) ? buffer[index].array : buffer.array, to = arr.length; + for (let n = 0, i = 0; n < to; n++, i++) { + arr[n++] = inBufferL[i]; + arr[n] = inBufferR[i]; + } + } + + bufferToJS(buffer, output, index) { + let outBufferL = null, outBufferR = null; + if (index === undefined) index = 0; + if (typeof output.getChannelData === 'function') { + outBufferL = output.getChannelData(0); + outBufferR = output.getChannelData(1); + } else { + outBufferL = output[index][0]; + outBufferR = output[index][1]; + } + const arr = (buffer.constructor === Array) ? buffer[index].array : buffer.array, to = arr.length; + for (let n = 0, i = 0; n < to; n++, i++) { + outBufferL[i] = arr[n++]; + outBufferR[i] = arr[n]; + } + } + + arrayBufferToWASM(arrayBuffer, offset = 0) { + const pointer = this.malloc(arrayBuffer.byteLength + offset); + new Uint8Array(this.linearMemory).set(new Uint8Array(arrayBuffer, 0, arrayBuffer.byteLength), pointer + offset); + return pointer; + } + + copyWASMToArrayBuffer(pointer, lengthBytes) { + const arrayBuffer = new ArrayBuffer(lengthBytes); + new Uint8Array(arrayBuffer, 0, lengthBytes).set(new Uint8Array(this.linearMemory, pointer, lengthBytes)); + return arrayBuffer; + } + + moveWASMToArrayBuffer(pointer, lengthBytes) { + const arrayBuffer = this.copyWASMToArrayBuffer(pointer, lengthBytes); + this.free(pointer); + return arrayBuffer; + } + + static async loaderWorkerMain(url) { + SuperpoweredGlue.__uint_max__sp__ = 255; + const Superpowered = await SuperpoweredGlue.Instantiate(''); + + await fetch(url).then(response => + response.arrayBuffer() + ).then(audiofileArrayBuffer => { + // Copy the ArrayBuffer to WebAssembly Linear Memory. + const audiofileInWASMHeap = Superpowered.arrayBufferToWASM(audiofileArrayBuffer); + + // Decode the entire file into the Audio In Memory format. + const audioInMemoryFormat = Superpowered.Decoder.decodeToAudioInMemory(audiofileInWASMHeap, audiofileArrayBuffer.byteLength); + + // Copy from the WebAssembly heap into a regular ArrayBuffer that can be transfered. + // Size calculation: 48 bytes (main table is six 64-bit numbers), plus number of audio frames (.getSize) multiplied by four (16-bit stereo is 4 bytes). + const arrayBuffer = Superpowered.moveWASMToArrayBuffer(audioInMemoryFormat, 48 + Superpowered.AudioInMemory.getSize(audioInMemoryFormat) * 4); + + // Transfer the ArrayBuffer. + if (typeof self.transfer !== 'undefined') self.transfer(url, arrayBuffer); + else postMessage({ '__transfer__': arrayBuffer, }, [ arrayBuffer ]); + }); + } + + static loaderWorkerOnmessage(message) { + if (typeof message.data.load === 'string') SuperpoweredGlue.loaderWorkerMain(message.data.load); + } + + registerTrackLoader(receiver) { + if (typeof receiver.terminate !== 'undefined') receiver.addEventListener('message', this.handleTrackLoaderMessage); // Worker + return this.trackLoaderReceivers.push((typeof receiver.port !== 'undefined') ? receiver.port : receiver) - 1; + } + + handleTrackLoaderMessage(message) { + if (typeof message.data.SuperpoweredLoad !== 'string') return false; + this.loadTrackInWorker(message.data.SuperpoweredLoad, message.data.trackLoaderID); + return true; + } + + async loadTrackInWorker(url, trackLoaderID) { + let source = SuperpoweredGlue.toString(); + + const trackLoaderWorker = new Worker(URL.createObjectURL(new Blob([ source + "\r\n\r\nonmessage = SuperpoweredGlue.loaderWorkerOnmessage;" + `\r\n\r\nSuperpoweredGlue.wasmCDNUrl = "${SuperpoweredGlue.wasmCDNUrl}";` ], { type: 'application/javascript' }))); + trackLoaderWorker.__url__ = url; + trackLoaderWorker.trackLoaderID = trackLoaderID; + + trackLoaderWorker.ontransfer = function(message) { + this.transferLoadedTrack(message.transfer, trackLoaderWorker); + }.bind(this); + + trackLoaderWorker.onmessage = function(message) { + this.transferLoadedTrack(message.data.__transfer__, trackLoaderWorker); + }.bind(this); + + if ((typeof window !== 'undefined') && (typeof window.location !== 'undefined') && (typeof window.location.origin !== 'undefined')) url = new URL(url, window.location.origin).toString(); + trackLoaderWorker.postMessage({ load: url }); + } + + transferLoadedTrack(arrayBuffer, trackLoaderWorker) { + const receiver = this.trackLoaderReceivers[trackLoaderWorker.trackLoaderID]; + if (receiver == null) return; + if (typeof receiver.postMessage === 'function') receiver.postMessage({ SuperpoweredLoaded: { buffer: arrayBuffer, url: trackLoaderWorker.__url__ }}, [ arrayBuffer ]); + else receiver({ SuperpoweredLoaded: { buffer: arrayBuffer, url: trackLoaderWorker.__url__ }}); + trackLoaderWorker.terminate(); + } + + downloadAndDecode(url, obj) { + if (obj.trackLoaderID === undefined) return; + if ((typeof obj.onMessageFromMainScope === 'function') && (typeof obj.sendMessageToMainScope === 'function')) obj.sendMessageToMainScope({ SuperpoweredLoad: url, trackLoaderID: obj.trackLoaderID }); + else this.loadTrackInWorker(url, obj.trackLoaderID); + } +} + +class SuperpoweredWebAudio { + static AudioWorkletHasBrokenModuleImplementation = false; + + constructor(minimumSamplerate, superpowered) { + //SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation = (navigator.userAgent.indexOf('AppleWebKit') > -1) || (navigator.userAgent.indexOf('Firefox') > -1); + //SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation = (navigator.userAgent.indexOf('Firefox') > -1); + //if (SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation && (navigator.userAgent.indexOf('Chrome') > -1)) SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation = false; + this.Superpowered = superpowered; + this.audioContext = null; + const AudioContext = window.AudioContext || window.webkitAudioContext || false; + let c = new AudioContext(); + if (c.sampleRate < minimumSamplerate) { + c.close(); + c = new AudioContext({ sampleRate: minimumSamplerate }); + } + this.audioContext = c; + } + + getUserMediaForAudio(constraints, onPermissionGranted, onPermissionDenied) { + let finalConstraints = {}; + + if (navigator.mediaDevices) { + const supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); + for (const constraint in supportedConstraints) if (supportedConstraints.hasOwnProperty(constraint) && (constraints[constraint] !== undefined)) finalConstraints[constraint] = constraints[constraint]; + } + + finalConstraints.audio = true; + finalConstraints.video = false; + + navigator.fastAndTransparentAudio = constraints.hasOwnProperty('fastAndTransparentAudio') && (constraints.fastAndTransparentAudio === true); + if (navigator.fastAndTransparentAudio) { + finalConstraints.echoCancellation = false; + finalConstraints.disableLocalEcho = false; + finalConstraints.autoGainControl = false; + finalConstraints.audio = { mandatory: { googAutoGainControl: false, googAutoGainControl2: false, googEchoCancellation: false, googNoiseSuppression: false, googHighpassFilter: false, googEchoCancellation2: false, googNoiseSuppression2: false, googDAEchoCancellation: false, googNoiseReduction: false } }; + }; + + navigator.getUserMediaMethod = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mediaDevices.mozGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + if (navigator.getUserMediaMethod) navigator.getUserMediaMethod(finalConstraints, onPermissionGranted, onPermissionDenied); + else { + let userMedia = null; + try { + userMedia = navigator.mediaDevices.getUserMedia; + } catch(error) { + if ((location.protocol.toLowerCase() != 'https') && (location.hostname.toLowerCase() != 'localhost')) onPermissionDenied("Web Audio requires a secure context (HTTPS or localhost)."); + else onPermissionDenied(error); + userMedia = null; + } + + if (userMedia != null) { + if (userMedia) navigator.mediaDevices.getUserMedia(finalConstraints).then(onPermissionGranted).catch(onPermissionDenied); + else onPermissionDenied("Can't access getUserMedia."); + } + } + } + + async getUserMediaForAudioAsync(constraints) { + return new Promise((resolve, reject) => { + this.getUserMediaForAudio(constraints, function(stream) { + if (navigator.fastAndTransparentAudio) { + const audioTracks = stream.getAudioTracks(); + for (const audioTrack of audioTracks) audioTrack.applyConstraints({ autoGainControl: false, echoCancellation: false, noiseSuppression: false }); + } + resolve(stream); + }, reject); + }); + } + + async createAudioNodeAsync(url, className, onMessageFromAudioScope, numInputs, numOutputs) { + if (numInputs === undefined) numInputs = 1; + if (numOutputs === undefined) numOutputs = 1; + return new Promise((resolve, reject) => this.createAudioNode(url, className, resolve, onMessageFromAudioScope, numInputs, numOutputs) ); + } + + createAudioNode(url, className, callback, onMessageFromAudioScope, numInputs, numOutputs) { + if (!SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation && (typeof AudioWorkletNode === 'function')) { + if (numInputs === undefined) numInputs = 1; + if (numOutputs === undefined) numOutputs = 1; + + this.audioContext.audioWorklet.addModule(url).then(() => { + const node = new AudioWorkletNode(this.audioContext, className, { + processorOptions: { + wasmCode: this.Superpowered.wasmCode, + samplerate: this.audioContext.sampleRate, + maxChannels: this.Superpowered.__maxChannels__, + numberOfInputs: numInputs, + numberOfOutputs: numOutputs, + trackLoaderID: this.Superpowered.trackLoaderReceivers.length + }, + numberOfInputs: numInputs, + numberOfOutputs: numOutputs, + outputChannelCount: Array(numOutputs).fill(2) + }); + node.superpoweredWASMUrl = SuperpoweredGlue.wasmCDNUrl; + node.trackLoaderID = this.Superpowered.registerTrackLoader(node); + node.Superpowered = this.Superpowered; + node.onReadyCallback = callback; + node.onMessageFromAudioScope = onMessageFromAudioScope; + node.destruct = function() { + node.Superpowered.trackLoaderReceivers[node.trackLoaderID] = null; + node.port.postMessage('___superpowered___destruct___'); + } + node.sendMessageToAudioScope = function(message, transfer = []) { node.port.postMessage(message, transfer); } + node.port.onmessage = function(event) { + if (node.Superpowered.handleTrackLoaderMessage(event)) return; + if (event.data == '___superpowered___onready___') { + node.state = 1; + node.onReadyCallback(node); + } else node.onMessageFromAudioScope(event.data); + }.bind(node); + }); + } else { + import(/* webpackIgnore: true */ /* viteIgnore: true */ url).then((processorModule) => { + const node = this.audioContext.createScriptProcessor(1024, 2, 2); + node.trackLoaderID = this.Superpowered.registerTrackLoader(node); + node.samplerate = this.audioContext.sampleRate; + node.inputBuffer = this.Superpowered.createFloatArray(1024 * 2); + node.outputBuffer = this.Superpowered.createFloatArray(1024 * 2); + node.processor = new processorModule.default(this.Superpowered, onMessageFromAudioScope, node.samplerate); + node.sendMessageToAudioScope = function(message, transfer = 0) { node.processor.onMessageFromMainScope(message); } + node.destruct = function() { + node.processor.Superpowered.trackLoaderReceivers[node.trackLoaderID] = null; + node.processor.state = -1; + node.processor.onDestruct(); + } + node.onaudioprocess = function(e) { + node.processor.Superpowered.bufferToWASM(node.inputBuffer, e.inputBuffer); + if (node.processor.state > 0) node.processor.processAudio(node.inputBuffer, node.outputBuffer, node.inputBuffer.array.length / 2); + node.processor.Superpowered.bufferToJS(node.outputBuffer, e.outputBuffer); + }; + node.processor.state = 1; + callback(node); + }); + } + } +} + +if (!SuperpoweredWebAudio.AudioWorkletHasBrokenModuleImplementation && (typeof AudioWorkletProcessor === 'function')) { + class SuperpoweredAudioWorkletProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + SuperpoweredGlue.__uint_max__sp__ = options.processorOptions.maxChannels; + this.trackLoaderID = options.processorOptions.trackLoaderID; + this.state = 0; + this.port.onmessage = (event) => { + if (event.data == '___superpowered___destruct___') { + this.state = -1; + this.onDestruct(); + } else this.onMessageFromMainScope(event.data); + }; + this.samplerate = options.processorOptions.samplerate; + this.Superpowered = new SuperpoweredGlue(); + this.Superpowered.loadFromArrayBuffer(options.processorOptions.wasmCode, this); + this.numberOfInputs = options.processorOptions.numberOfInputs; + this.numberOfOutputs = options.processorOptions.numberOfOutputs; + } + afterWASMLoaded() { + // Add the user's WASM URL to the SuperpoweredGlue class + if (this.superpoweredWASMUrl) { + SuperpoweredGlue.wasmCDNUrl = this.superpoweredWASMUrl; + } + + this.Superpowered.Initialize(); + + this.inputBuffers = []; + for (let n = this.numberOfInputs; n > 0; n--) this.inputBuffers.push(this.Superpowered.createFloatArray(128 * 2)); + + this.outputBuffers = []; + for (let n = this.numberOfOutputs; n > 0; n--) this.outputBuffers.push(this.Superpowered.createFloatArray(128 * 2)); + + this.onReady(); + this.port.postMessage('___superpowered___onready___'); + this.state = 1; + } + onReady() {} + onDestruct() {} + onMessageFromMainScope(message) {} + sendMessageToMainScope(message) { this.port.postMessage(message); } + processAudio(buffer, parameters) {} + process(inputs, outputs, parameters) { + if (this.state < 0) return false; + if (this.state == 1) { + for (let n = this.numberOfInputs - 1; n >= 0; n--) { + if (inputs[n].length > 1) this.Superpowered.bufferToWASM(this.inputBuffers, inputs, n); + else this.Superpowered.memorySet(this.inputBuffers[n].pointer, 0, 128 * 8); + } + this.processAudio( + (this.numberOfInputs == 1) ? this.inputBuffers[0] : this.inputBuffers, + (this.numberOfOutputs == 1) ? this.outputBuffers[0] : this.outputBuffers, + 128, + parameters + ); + for (let n = this.numberOfOutputs - 1; n >= 0; n--) { + if (outputs[n].length > 1) this.Superpowered.bufferToJS(this.outputBuffers, outputs, n); + } + } + return true; + } + } + SuperpoweredWebAudio.AudioWorkletProcessor = SuperpoweredAudioWorkletProcessor; +} else { + class SuperpoweredAudioWorkletProcessor { + constructor(sp, oma, sr) { + this.Superpowered = sp; + this.samplerate = sr; + this.onMessageFromAudioScope = oma; + this.state = 0; + this.onReady(); + } + onMessageFromAudioScope = null; + onReady() {} + onDestruct() {} + onMessageFromMainScope(message) {} + sendMessageToMainScope(message) { if (!this.Superpowered.handleTrackLoaderMessage({ data: message })) this.onMessageFromAudioScope(message); } + postMessage(message, transfer = []) { this.onMessageFromMainScope(message); } + processAudio(buffer, parameters) {} + } + SuperpoweredWebAudio.AudioWorkletProcessor = SuperpoweredAudioWorkletProcessor; +} + +if (typeof exports === "object" && typeof module === "object") + module.exports = { SuperpoweredGlue, SuperpoweredWebAudio }; +else if (typeof exports === "object") { + exports["SuperpoweredGlue"] = SuperpoweredGlue; + exports["SuperpoweredWebAudio"] = SuperpoweredWebAudio; +} +if (typeof globalThis !== "undefined") { + globalThis.SuperpoweredGlue = SuperpoweredGlue; + globalThis.SuperpoweredWebAudio = SuperpoweredWebAudio; +} diff --git a/examples/example_timestretching/Superpowered.js b/examples/example_timestretching/Superpowered.js index 79e9341..3f59111 100644 --- a/examples/example_timestretching/Superpowered.js +++ b/examples/example_timestretching/Superpowered.js @@ -3,7 +3,7 @@ class SuperpoweredGlue { - static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.7/dist/superpowered-npm.wasm" + static wasmCDNUrl = "https://cdn.jsdelivr.net/npm/@superpoweredsdk/web@2.6.8/dist/superpowered-npm.wasm" niceSize(bytes) { if (bytes == 0) return '0 byte'; else if (bytes == 1) return '1 byte';