Comparing version 0.2.23 to 0.3.0
export default (t, r) => { | ||
const TextParameterReader = t; | ||
const RingBuffer = r; | ||
class GlicolEngine extends AudioWorkletProcessor { | ||
@@ -12,8 +13,13 @@ static get parameterDescriptors() { | ||
this._paramArray = new Uint8Array(2048); | ||
const { codeQueue, paramQueue } = options.processorOptions; | ||
const isLiveCoding = options.isLiveCoding; | ||
const isLiveCoding = options.processorOptions.isLiveCoding; | ||
// console.log("options.isLiveCoding", options.processorOptions.isLiveCoding); | ||
this.useSAB = options.processorOptions.useSAB; | ||
if (this.useSAB) { | ||
// console.log(this.useSAB) | ||
this._code_reader = new TextParameterReader( | ||
new RingBuffer(options.processorOptions.codeQueue, Uint8Array)); | ||
this._param_reader = new TextParameterReader( | ||
new RingBuffer(options.processorOptions.paramQueue, Uint8Array)); | ||
} | ||
this._code_reader = new TextParameterReader(new RingBuffer(codeQueue, Uint8Array)); | ||
this._param_reader = new TextParameterReader(new RingBuffer(paramQueue, Uint8Array)); | ||
this.port.onmessage = async e => { | ||
@@ -95,2 +101,10 @@ if (e.data.type === "load") { | ||
this._wasm.exports.update(codeUint8ArrayPtr, size) | ||
} else if (e.data.type === "msg") { | ||
let msg = e.data.value | ||
let size = msg.byteLength | ||
let msgUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size); | ||
let msgUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, msgUint8ArrayPtr, size); | ||
msgUint8Array.set(msg.slice(0, size)); | ||
this._wasm.exports.send_msg(msgUint8ArrayPtr, size) | ||
} else if (e.data.type === "bpm") { | ||
@@ -102,6 +116,2 @@ this._wasm.exports.set_bpm(e.data.value); | ||
this._wasm.exports.set_track_amp(e.data.value); | ||
// } else if (e.data.type === "sab") { | ||
// } else if (e.data.type === "result") { | ||
// this._result_reader = new TextParameterReader(new RingBuffer(e.data.data, Uint8Array)); | ||
} else { | ||
@@ -118,29 +128,19 @@ throw "unexpected."; | ||
let size = this._code_reader.dequeue(this._codeArray) | ||
if (size) { | ||
let codeUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size); | ||
// console.log("codeUint8ArrayPtr", codeUint8ArrayPtr) | ||
let codeUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, codeUint8ArrayPtr, size); | ||
// console.log(this._codeArray.slice(0, size)) | ||
codeUint8Array.set(this._codeArray.slice(0, size), "this._codeArray.slice(0, size)"); | ||
this._wasm.exports.update(codeUint8ArrayPtr, size) | ||
if (this.useSAB) { | ||
let size = this._code_reader.dequeue(this._codeArray) | ||
if (size) { | ||
let codeUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size); | ||
let codeUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, codeUint8ArrayPtr, size); | ||
codeUint8Array.set(this._codeArray.slice(0, size), "this._codeArray.slice(0, size)"); | ||
this._wasm.exports.update(codeUint8ArrayPtr, size) | ||
} | ||
let size2 = this._param_reader.dequeue(this._paramArray) | ||
if (size2) { | ||
let paramUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size2); | ||
let paramUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, paramUint8ArrayPtr, size2); | ||
paramUint8Array.set(this._paramArray.slice(0, size2)); | ||
this._wasm.exports.send_msg(paramUint8ArrayPtr, size2) | ||
} | ||
} | ||
let size2 = this._param_reader.dequeue(this._paramArray) | ||
if (size2) { | ||
let paramUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size2); | ||
// console.log("paramUint8ArrayPtr", paramUint8ArrayPtr) | ||
let paramUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, paramUint8ArrayPtr, size2); | ||
// console.log(this._paramArray.slice(0, size2), "this._paramArray.slice(0, size2)") | ||
paramUint8Array.set(this._paramArray.slice(0, size2)); | ||
// console.log("paramUint8Array",paramUint8Array) | ||
this._wasm.exports.send_msg(paramUint8ArrayPtr, size2) | ||
} | ||
// if (midiSize) { | ||
// let codeUint8ArrayPtr = this._wasm.exports.alloc_uint8array(size); | ||
// let codeUint8Array = new Uint8Array(this._wasm.exports.memory.buffer, codeUint8ArrayPtr, size); | ||
// codeUint8Array.set(this._codeArray.slice(0, size)); | ||
if (inputs[0][0]) { // TODO: support stereo or multi-chan | ||
@@ -183,27 +183,2 @@ this._inPtr = this._wasm.exports.alloc(128) | ||
registerProcessor('glicol-engine', GlicolEngine) | ||
} | ||
// https://gist.github.com/littledan/f7c1d1abf0e51ad4b526a8eadb2da43b | ||
// register processor in AudioWorkletGlobalScope | ||
// function registerProcessor(name, processorCtor) { | ||
// return `${processorCtor};\nregisterProcessor('${name}', ${processorCtor.name});`; | ||
// } | ||
// const worklet = (URL.createObjectURL( | ||
// new Blob( | ||
// [ | ||
// registerProcessor( | ||
// 'glicol-engine', | ||
// GlicolEngine | ||
// ), | ||
// ], | ||
// { type: 'text/javascript' } | ||
// ) | ||
// )); | ||
// export {worklet} | ||
// export default whateverWorker.a.b | ||
// registerProcessor('glicol-engine', GlicolEngine) | ||
// ` | ||
} |
219
index.js
import worklet from './glicol-engine' | ||
import wasm from './glicol_wasm.wasm' | ||
import nosab from './nosab' | ||
import { detectBrowser } from './detect' | ||
import {TextParameterReader, TextParameterWriter, RingBuffer} from './ringbuf' | ||
let text = `( ${String(worklet)} )(${TextParameterReader}, ${RingBuffer})`; | ||
// console.log(text) | ||
var isSharedArrayBufferSupported = false; | ||
try { | ||
var sab = new SharedArrayBuffer(1); | ||
var {name, _} = detectBrowser(); | ||
if (sab && !name.includes('Safari') ) { | ||
isSharedArrayBufferSupported = true | ||
} | ||
} catch(e){ | ||
console.warn(nosab) | ||
} | ||
class Engine { | ||
constructor(isLiveCoding) { | ||
constructor({ | ||
audioContext = new AudioContext(), | ||
isLiveCoding = false, | ||
connectTo, | ||
onLoaded = () => {} | ||
}={}) { | ||
// console.log("audioContext", audioContext); | ||
// console.log("connectTo", connectTo, "!connectTo", !connectTo); | ||
(async () => { | ||
// isLiveCoding = true | ||
this.encoder = new TextEncoder('utf-8'); | ||
this.decoder = new TextDecoder('utf-8'); | ||
this.audioContext = new AudioContext() | ||
this.audioContext = audioContext; | ||
this.audioContext.suspend() | ||
@@ -20,18 +41,29 @@ | ||
let sab = RingBuffer.getStorageForCapacity(2048, Uint8Array); | ||
let rb = new RingBuffer(sab, Uint8Array); | ||
this.codeWriter = new TextParameterWriter(rb); | ||
let sab2 = RingBuffer.getStorageForCapacity(2048, Uint8Array); | ||
let rb2 = new RingBuffer(sab2, Uint8Array); | ||
this.paramWriter = new TextParameterWriter(rb2); | ||
this.node = new AudioWorkletNode(this.audioContext, 'glicol-engine', { | ||
if (isSharedArrayBufferSupported) { | ||
let sab = RingBuffer.getStorageForCapacity(2048, Uint8Array); | ||
let rb = new RingBuffer(sab, Uint8Array); | ||
this.codeWriter = new TextParameterWriter(rb); | ||
let sab2 = RingBuffer.getStorageForCapacity(2048, Uint8Array); | ||
let rb2 = new RingBuffer(sab2, Uint8Array); | ||
this.paramWriter = new TextParameterWriter(rb2); | ||
this.node = new AudioWorkletNode(this.audioContext, 'glicol-engine', { | ||
outputChannelCount: [2], | ||
processorOptions: { | ||
codeQueue: sab, | ||
paramQueue: sab2, | ||
useSAB: true, | ||
isLiveCoding: isLiveCoding, | ||
}, | ||
}) | ||
} else { | ||
this.node = new AudioWorkletNode(this.audioContext, 'glicol-engine', { | ||
outputChannelCount: [2], | ||
processorOptions: { | ||
codeQueue: sab, | ||
paramQueue: sab2 | ||
}, | ||
isLiveCoding: isLiveCoding === true ? true: false | ||
}) | ||
useSAB: false, | ||
isLiveCoding: isLiveCoding, | ||
} | ||
}) | ||
} | ||
@@ -41,4 +73,5 @@ this.sampleBuffers = {} | ||
this.node.port.onmessage = async e => { | ||
this.log("%c GLICOL loaded. ", "background:#3b82f6; color:white; font-weight: bold; font-family: Courier") | ||
if (e.data.type === 'ready') { | ||
if (Object.keys(this.sampleBuffers).length !== 0) { | ||
@@ -69,2 +102,3 @@ for (let key in this.sampleBuffers) { | ||
} | ||
onLoaded() | ||
} else if (e.data.type === 'e') { | ||
@@ -99,25 +133,42 @@ // let decoder = new TextDecoder("utf-8") | ||
} | ||
this.node.connect(this.audioContext.destination) | ||
if (!connectTo) { | ||
this.node.connect(this.audioContext.destination) | ||
} else { | ||
this.node.connect(connectTo) | ||
} | ||
// wasm({env:{now:Date.now}}).then(res=>window._wasm=res); | ||
// console.log("wasm func; we don't call it, just want the url",wasm) | ||
// this.log("the imported wasm:", wasm) | ||
// this.log("the imported wasm as str:", String(wasm)) | ||
let url = String(wasm).replaceAll(' ', '') | ||
// console.log("wasm url remove all spaces:",url) | ||
url = url.split(",\"/")[1]; | ||
// console.log("wasm url trim prefix:",url) | ||
url = url.split("\")")[0] | ||
// console.log("wasm url trim end:",url) | ||
// console.log("url",url) | ||
fetch(url) | ||
.then(response => response.arrayBuffer()) | ||
let urlSplit = url.split("/"); | ||
urlSplit.shift() | ||
let urlNoHead = "/"+urlSplit.join("/") | ||
let finalUrl = urlNoHead.split(".wasm")[0] + ".wasm" | ||
// console.log(finalUrl) | ||
fetch(finalUrl).then(response => response.arrayBuffer()) | ||
.then(arrayBuffer => { | ||
this.node.port.postMessage({ | ||
type: "load", obj: arrayBuffer | ||
type: "load", | ||
obj: arrayBuffer | ||
}) | ||
}) | ||
.catch(e=>{ | ||
console.log(e) | ||
console.error("fail to load the wasm module. please report it here: https://github.com/chaosprint/glicol") | ||
}) | ||
})(); | ||
} | ||
run(code) { | ||
this.audioContext.resume() | ||
if (this.codeWriter.available_write()) { | ||
this.codeWriter.enqueue(this.encoder.encode(code)) | ||
if (isSharedArrayBufferSupported) { | ||
// console.log("isSharedArrayBufferSupported", isSharedArrayBufferSupported); | ||
if (this.codeWriter.available_write()) { | ||
this.codeWriter.enqueue(this.encoder.encode(code)) | ||
} | ||
} else { | ||
this.node.port.postMessage({ | ||
type: "run", | ||
value: this.encoder.encode(code) | ||
}) | ||
} | ||
@@ -128,5 +179,13 @@ } | ||
let str; | ||
str = msg.slice(-1) === ";"? msg : msg+";" | ||
if (this.paramWriter.available_write()) { | ||
this.paramWriter.enqueue(this.encoder.encode(str)) | ||
str = msg.slice(-1) === ";"? msg : msg+";" // todo: not robust | ||
if (isSharedArrayBufferSupported) { | ||
if (this.paramWriter.available_write()) { | ||
this.paramWriter.enqueue(this.encoder.encode(str)) | ||
} | ||
} else { | ||
this.node.port.postMessage({ | ||
type: "msg", | ||
value: this.encoder.encode(str) | ||
}) | ||
} | ||
@@ -147,2 +206,6 @@ } | ||
connect(target) { | ||
this.node.connect(target) | ||
} | ||
reset() { | ||
@@ -164,5 +227,83 @@ // this.node | ||
showAllSamples() { | ||
window.table(Object.keys(this.sampleBuffers)) | ||
console.table(Object.keys(this.sampleBuffers)) | ||
return `` | ||
} | ||
async addSampleFiles(name, url) { | ||
if (url === undefined) { | ||
var input = document.createElement('input'); | ||
input.type = 'file'; | ||
input.multiple = true | ||
input.onchange = e => { | ||
var files = e.target.files; | ||
// log(files) | ||
for (var i = 0; i < files.length; i++) { | ||
((file) => { | ||
var reader = new FileReader(); | ||
reader.onload = async (e) => { | ||
let name = file.name.toLowerCase().replace(".wav", "").replace(".mp3", "").replaceAll("-","_").replaceAll(" ","_").replaceAll("#","_sharp_") | ||
await this.audioContext.decodeAudioData(e.target.result, buffer => { | ||
this.sampleBuffers[name] = buffer | ||
var sample; | ||
if (buffer.numberOfChannels === 1) { | ||
sample = buffer.getChannelData(0); | ||
} else if (buffer.numberOfChannels === 2) { | ||
sample = new Float32Array( buffer.length * 2); | ||
sample.set(buffer.getChannelData(0), 0); | ||
sample.set(buffer.getChannelData(1), buffer.length); | ||
} else { | ||
throw(Error("Only support mono or stereo samples.")) | ||
} | ||
console.log("loading sample: ", name) | ||
this.node.port.postMessage({ | ||
type: "loadsample", | ||
sample: sample, | ||
channels: buffer.numberOfChannels, | ||
length: buffer.length, | ||
name: this.encoder.encode("\\"+ name), | ||
sr: buffer.sampleRate | ||
}) | ||
}) | ||
// log(`Sample %c${key.replace(".wav", "")} %cloaded`, "color: green; font-weight: bold", "") | ||
}; | ||
reader.readAsArrayBuffer(file); | ||
})(files[i]); | ||
} | ||
} | ||
input.click(); | ||
} else { | ||
this.audioContext.suspend() | ||
let myRequest = new Request(url); | ||
await fetch(myRequest).then(response => response.arrayBuffer()) | ||
.then(arrayBuffer => { | ||
this.audioContext.decodeAudioData(arrayBuffer, buffer => { | ||
// log(new Int16Array(buffer.getChannelData(0).buffer)) | ||
// let name = file.name.toLowerCase().replace(".wav", "").replace(".mp3", "").replace("-","_").replace(" ","_") | ||
this.sampleBuffers[name] = buffer | ||
var sample; | ||
if (buffer.numberOfChannels === 1) { | ||
sample = buffer.getChannelData(0); | ||
} else if (buffer.numberOfChannels === 2) { | ||
sample = new Float32Array( buffer.length * 2); | ||
sample.set(buffer.getChannelData(0), 0); | ||
sample.set(buffer.getChannelData(1), buffer.length); | ||
} else { | ||
throw(Error("Only support mono or stereo samples.")) | ||
} | ||
this.node.port.postMessage({ | ||
type: "loadsample", | ||
sample: sample, | ||
channels: buffer.numberOfChannels, | ||
length: buffer.length, | ||
name: this.encoder.encode("\\"+ name), | ||
sr: buffer.sampleRate | ||
}) | ||
}, function(e){ console.log("Error with decoding audio data" + e.err); }) | ||
}); | ||
this.audioContext.resume() | ||
} | ||
} | ||
async loadSamples() { | ||
@@ -203,7 +344,11 @@ | ||
}) | ||
// log(window.showAllSamples()) | ||
// log(this.showAllSamples()) | ||
}) | ||
// window.actx.suspend() | ||
// this.audioContext.suspend() | ||
// ['bd0000', 'clav', "pandrum", "panfx", "cb"] | ||
} | ||
log(...params) { | ||
setTimeout(console.log.bind(console, ...params)); | ||
} | ||
} | ||
@@ -210,0 +355,0 @@ |
{ | ||
"name": "glicol", | ||
"version": "0.2.23", | ||
"version": "0.3.0", | ||
"description": "An light-weight, garbage-collection-free, and easy-to-use audio engine for browser-based music apps.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
## What's this? | ||
This is a light-weight, garbage-collection free, memory-safe and easy-to-use audio library for browsers. It's written in Rust and ported to JS via WebAssembly and runs in AudioWorklet. The communication is realised with SharedArrayBuffer. | ||
This is a light-weight, garbage-collection free, memory-safe and easy-to-use audio library for browsers. It's written in Rust and ported to JS via WebAssembly and runs in AudioWorklet. The communication is realised with SharedArrayBuffer*. | ||
> Note that you need to have `cross-origin isolation` enabled on the web server (both the dev server and the one you deploy your web app) to use this package. For `vite` dev server, you can use my plugin [here](https://github.com/chaosprint/vite-plugin-cross-origin-isolation). For deployment on `Netlify` or `Firebase`, check their docs for editing the header files. If you use a customised server, you have to figure it out yourself. | ||
> *Without SAB, you can still use Glicol. However, to get the best audio performance, you need to have `cross-origin isolation` enabled on the web server (both the dev server and the one you deploy your web app) to use this package. For `vite` dev server, you can use my plugin [here](https://github.com/chaosprint/vite-plugin-cross-origin-isolation). For deployment on `Netlify` or `Firebase`, check their docs for editing the header files. If you use a customised server, you have to figure it out yourself. | ||
@@ -26,16 +26,8 @@ ## Why `glicol.js`? | ||
Rust is also famous for its error handling. `glicol.js` has taken advantage of that and offers a robust error handling mechanism. | ||
Rust is also famous for its error handling. `glicol.js` has taken advantage of that and offers a robust error handling mechanism. The principle is "Musique Non-Stop", i.e. when there is an error, it will be reported in the console while the music will continue as before. | ||
> WIP: the error report is coming soon. | ||
### Easy to use | ||
With the top-level audio performance in the browser, Glicol is yet easy to use. The balance between minimalism and readability/ergonomics is consistent in the API designing. | ||
With the top-level audio performance in the browser, Glicol is yet easy to use. The balance between minimalism and readability/ergonomics is consistent in the API designing. After you `npm i glicol`, you can just write: | ||
> As this is not a stable version yet, the APIs may significantly change in the future. If you wish to test or use it in a project, please contact me. | ||
## Usage | ||
After you `npm i glicol`, you can just write: | ||
```js | ||
@@ -46,3 +38,3 @@ import Glicol from "glicol" | ||
You can also write the graph in this way: | ||
Then write the graph in this way: | ||
@@ -56,4 +48,6 @@ ```js | ||
Simple as that. No need to create a node, and then connect it everywhere. | ||
Simple as that. | ||
No need to create a node, and then connect it everywhere. | ||
Note that there are two `chains` here, one is called `o` and the other is `~am`. | ||
@@ -83,11 +77,10 @@ | ||
```js | ||
glicol.send_msg(`o, 0, 0, 110`) | ||
// chain "o", node_index 0, param 0, set to 110 | ||
glicol.sendMsg(`o, 0, 0, 110`) | ||
``` | ||
This will send message to set: | ||
- chain: `o` | ||
- node_index: 0, | ||
- para_index: 0, | ||
- set_to_number: 110 | ||
## API reference (coming soon...) | ||
> As this is not a stable version yet, the APIs may significantly change in the future. If you wish to test or use it in a project, please contact me. | ||
## Alternative usage - Glicol DSL | ||
@@ -116,4 +109,4 @@ | ||
```js | ||
// track "o", node_index 0, param 0, set to 110 | ||
glicol.send_msg(`o, 0, 0, 110`); | ||
// chain "o", node_index 0, param 0, set to 110 | ||
glicol.sendMsg(`o, 0, 0, 110`); | ||
``` | ||
@@ -128,5 +121,23 @@ | ||
```js | ||
glicol.send_msg(`o, 0, 0, 110; o, 1, 0, 500; o, 1, 1, 0.8`); | ||
glicol.sendMsg(`o, 0, 0, 110; o, 1, 0, 500; o, 1, 1, 0.8`); | ||
``` | ||
## Extension | ||
You can provide an `audioContext` to glicol and use the output of glicol to another node from that `audioContext`: | ||
```js | ||
import Glicol from 'glicol' | ||
const myAudioContext = new AudioContext() | ||
const gainNode = myAudioContext.createGain(); | ||
gainNode.gain.value = 0.1 | ||
gainNode.connect(myAudioContext.destination) | ||
const glicol = new Glicol({ | ||
audioContext: myAudioContext, | ||
connectTo: gainNode | ||
}) | ||
``` | ||
## Feedback | ||
@@ -133,0 +144,0 @@ |
@@ -64,8 +64,8 @@ // from: https://github.com/padenot/ringbuf.js | ||
static getStorageForCapacity(capacity, type) { | ||
if (!type.BYTES_PER_ELEMENT) { | ||
throw "Pass in a ArrayBuffer subclass"; | ||
if (!type.BYTES_PER_ELEMENT) { | ||
throw "Pass in a ArrayBuffer subclass"; | ||
} | ||
var bytes = 8 + (capacity + 1) * type.BYTES_PER_ELEMENT; | ||
return new SharedArrayBuffer(bytes); | ||
} | ||
var bytes = 8 + (capacity + 1) * type.BYTES_PER_ELEMENT; | ||
return new SharedArrayBuffer(bytes); | ||
} | ||
constructor(sab, type) { | ||
@@ -72,0 +72,0 @@ if (!ArrayBuffer.__proto__.isPrototypeOf(type) && |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
2139118
9
851
148
4