diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..5bd1b92fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:4321", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/packages/superdough/remove.html b/packages/superdough/remove.html new file mode 100644 index 000000000..59476043c --- /dev/null +++ b/packages/superdough/remove.html @@ -0,0 +1,295 @@ +🌱 audio worklets +In the last post, we've been filling fixed length audio buffers. While this is very simple to implement, it's a bit annoying when the audio is always stopping. To calculate samples infinitely, we can use an AudioWorklet. Now we've arrived at a typical bytebeat/floatbeat editor: + +💡 hit ctrl+enter to update the code & ctrl+. to stop +((( // by stimmer (2011-10-03) +t*(4|t>>13&3)>>(~t>>11&1)&128|t*(t>>11&t>>13)*(~t>>9&3)&127 +) & 255) / 127.5 - 1)/4 + play | stop +The AudioWorklet is the lowest abstaction we get with the Web Audio API. It essentially gives us a function that fills a buffer of 128 samples over and over again, also running isolated from the rest of the page. At a sample rate of 44100/s, this means we fill a buffer every ~3ms (44100/128). + +Here are some examples to try: + +hello sine +slow sine (inaudible) +just intonation triad +hello saw +hello pulse +wandering sine +bytebeat 1 +bytebeat 2 +gladyouask - chirps +aks - TR-808-like hihat +show source (271 loc) + + + + 🌱 audio worklets + + + +

🌱 audio worklets

+

+ In the last post, we've been filling + fixed length audio buffers. While this is very simple to implement, it's a + bit annoying when the audio is always stopping. To calculate samples + infinitely, we can use an AudioWorklet. Now we've arrived at a typical + bytebeat/floatbeat editor: +

+
+ 💡 hit ctrl+enter to update the code & ctrl+. to stop +
+ + play | stop + +

+ The AudioWorklet is the lowest abstaction we get with the Web Audio API. + It essentially gives us a function that fills a buffer of 128 samples over + and over again, also running isolated from the rest of the page. At a + sample rate of 44100/s, this means we fill a buffer every ~3ms + (44100/128). +

+

Here are some examples to try:

+ + +
+ show page source +

+    
+

+ back to garten.salat +

+ + +back to garten.salat \ No newline at end of file diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 0afbb3d67..07ab08054 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -446,7 +446,7 @@ function mapChannelNumbers(channels) { return (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1); } -export const superdough = async (value, t, hapDuration) => { +export const superdough = async (value, t, hapDuration, cps) => { const ac = getAudioContext(); t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t; let { stretch } = value; @@ -580,7 +580,7 @@ export const superdough = async (value, t, hapDuration) => { // get source AudioNode let sourceNode; if (source) { - sourceNode = source(t, value, hapDuration); + sourceNode = source(t, value, hapDuration,cps); } else if (getSound(s)) { const { onTrigger } = getSound(s); const onEnded = () => { diff --git a/packages/superdough/synth.mjs b/packages/superdough/synth.mjs index dd2c92451..554bb1e9a 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -158,10 +158,13 @@ export function registerSynthSounds() { ); const holdend = begin + duration; const end = holdend + release + 0.01; + const frequency = getFrequencyFromValue(value); + let o = getWorklet( ac, 'byte-beat-processor', { + frequency, begin, end, }, diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index b4923d5aa..3d1bbccbe 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -295,7 +295,7 @@ class LadderProcessor extends AudioWorkletProcessor { cutoff = (cutoff * 2 * _PI) / sampleRate; cutoff = cutoff > 1 ? 1 : cutoff; - const k = Math.min(8, resonance * 0.4); + const k = Math.min(8, resonance * 0.13); // drive makeup * resonance volume loss makeup let makeupgain = (1 / drive) * Math.min(1.75, 1 + k); @@ -762,13 +762,29 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor { registerProcessor('pulse-oscillator', PulseOscillatorProcessor); + +function frequencyToDigit(freq, baseFreq) { + const d = Math.log2(freq / baseFreq); + return Math.max(d, 0) + Math.pow(2, Math.min(d, 0)); +} + class ByteBeatProcessor extends AudioWorkletProcessor { constructor() { super(); - this.bb = '0'; + this.codeText = '0'; this.port.onmessage = (event) => { - this.bb = event.data; + + this.codeText = event.data.trim().replace( + /^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/, + (match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%'))); + }; + this.virtualRate = 32000; // target sample rate + // this.nativeRate = sampleRate; // actual context rate, e.g., 48000 + // this.ratio = this.nativeRate / this.virtualRate; + this.t = null; + this.framebuffer = new Float32Array(Math.floor(sampleRate/60)); + this.func = null; } static get parameterDescriptors() { @@ -779,7 +795,17 @@ class ByteBeatProcessor extends AudioWorkletProcessor { max: Number.POSITIVE_INFINITY, min: 0, }, - + { + name: 'frequency', + defaultValue: 440, + min: Number.EPSILON, + }, + { + name: 'detune', + defaultValue: 0, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + }, { name: 'end', defaultValue: 0, @@ -799,24 +825,432 @@ class ByteBeatProcessor extends AudioWorkletProcessor { if (currentTime >= params.end[0]) { return false; } - const bb = this.bb; + if (this.t == null) { + this.t = params.begin[0] * this.virtualRate + } + + + let codeText = this.codeText; + + // const f = Function('t', 'return ' + codeText); + + + // const chyx = { + // /*bit*/ "bitC": function (x, y, z) { return x & y ? z : 0 }, + // /*bit reverse*/"br": function (x, size = 8) { + // if(size > 32) { throw new Error("br() Size cannot be greater than 32") } else { + // let result = 0; + // for (let idx = 0; idx < (size - 0); idx++) { + // result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1))) + // } + // return result + // } + // }, + // /*sin that loops every 128 "steps", instead of every pi steps*/"sinf": function (x) { return Math.sin(x / (128 / Math.PI)) }, + // /*cos that loops every 128 "steps", instead of every pi steps*/"cosf": function (x) { return Math.cos(x / (128 / Math.PI)) }, + // /*tan that loops every 128 "steps", instead of every pi steps*/"tanf": function (x) { return Math.tan(x / (128 / Math.PI)) }, + // /*converts t into a string composed of it's bits, regex's that*/"regG": function (t, X) { return X.test(t.toString(2)) } + // /*corrupt sound"crpt": function(x,y=8) {return chyx.br(chyx.br(x,y)+t,y)^chyx.br(t,y)}, + // decorrupt sound"decrpt": function(x,y=8) {return chyx.br(chyx.br(x^chyx.br(t,y),y)-t,y)},*/ + // } + // // Create shortened Math functions + // const mathParams = Object.getOwnPropertyNames(Math); + // const values = mathParams.map(k => Math[k]); + // const chyxNames = Object.getOwnPropertyNames(chyx); + // const chyxFuncs = chyxNames.map(k => chyx[k]); + // mathParams.push('int', 'window', ...chyxNames); + // values.push(Math.floor, globalThis, ...chyxFuncs); + + + + + // Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%"))) + // this.func = new Function(...mathParams, 't', `return 0,\n${ codeText || 0 };`).bind(globalThis, ...values); + this.func = Function('t', 'return ' + codeText); - const f = Function('t', 'return ' + bb); const output = outputs[0]; + const ct = params.begin[0] + const tIncrement = this.virtualRate / sampleRate + - for (let i = 0; i < (output[0].length ?? 0); i++) { - let t = currentTime; - t *= sampleRate; - const signal = ((f(t) & 255) / 127.5 - 1) / 4; - for (let o = 0; o < output.length; o++) { - // Combination of both oscillators with envelope applied - output[o][i] = signal * .4; + // console.info(ct, currentTime) + for (let i = 0; i < output[0].length; i++) { + let t = Math.floor(this.t) + const detune = getParamValue(i, params.detune); + const freq = applySemitoneDetuneToFrequency(getParamValue(i, params.frequency), detune / 100); + // t = t * (Math.log(freq / 440)) / Math.LN2; + + t = t * frequencyToDigit(freq, 440) + // console.info(t) + // let t = (currentTime * sampleRate) + i + // let t = this.t + + // const index = Math.floor(this.t) + const funcValue = this.func(t) + let signal = (funcValue & 255) / 127.5 - 1 + const out = signal; + for (let c = 0; c < output.length; c++) { + output[c][i] = out; } + this.t = this.t + tIncrement + // if (i % 8 === 0){ + // this.t = this.t + tIncrement + + // } + // this.framebuffer[this.t%this.framebuffer.length] = out; + // if(this.t % this.framebuffer.length === 0 && this.t > 0) { + // this.port.postMessage(this.framebuffer) + // } } + + + // for (let i = 0; i < (output[0].length ?? 0); i++) { + // let t = currentTime; + // t *= sampleRate; + // const signal = this.func(t) + // // const signal = ((this.func(t) + 128) & 255) / 127.5 - 1 + // // const signal = ((this.func(t) & 255) / 127.5 - 1) / 4; + // for (let o = 0; o < output.length; o++) { + // // Combination of both oscillators with envelope applied + // output[o][i] = signal * .4; + // } + // } return true; // keep the audio processing going } } registerProcessor('byte-beat-processor', ByteBeatProcessor); + + + +// class ByteBeatProcessor extends AudioWorkletProcessor { +// constructor(...args) { +// super(...args); +// this.audioSample = 0; +// this.byteSample = 0; +// // this.drawMode = 'Points'; +// // this.errorDisplayed = true; +// this.func = null; +// this.getValues = null; +// this.isFuncbeat = false; +// this.isPlaying = true; +// this.playbackSpeed = 1; +// this.divisorStorage = 0; +// this.lastTime = -1; +// this.lastFuncValue = [null, null]; +// this.lastByteValue = [0, 0]; +// this.outValue = [0, 0]; +// // this.sampleRate = 8000; +// // this.sampleRatio = 1; +// this.sampleDivisor/*PRO*/ = 1; +// this.soundMode = 'Bytebeat'; +// // Object.seal(this); +// // ByteBeatProcessor.deleteGlobals(); +// // ByteBeatProcessor.freezeGlobals(); +// this.port.addEventListener('message', e => this.receiveData(e.data)); +// this.port.start(); +// } + +// static get parameterDescriptors() { +// return [ +// { +// name: 'begin', +// defaultValue: 0, +// max: Number.POSITIVE_INFINITY, +// min: 0, +// }, + +// { +// name: 'end', +// defaultValue: 0, +// max: Number.POSITIVE_INFINITY, +// min: 0, +// }, +// ]; +// } +// // static deleteGlobals() { +// // // Delete single letter variables to prevent persistent variable errors (covers a good enough range) +// // for(let i = 0; i < 26; ++i) { +// // delete globalThis[String.fromCharCode(65 + i)]; +// // delete globalThis[String.fromCharCode(97 + i)]; +// // } +// // // Delete global variables +// // for(const name in globalThis) { +// // if(Object.prototype.hasOwnProperty.call(globalThis, name)) { +// // delete globalThis[name]; +// // } +// // } +// // } +// // static freezeGlobals() { +// // Object.getOwnPropertyNames(globalThis).forEach(name => { +// // const prop = globalThis[name]; +// // const type = typeof prop; +// // if((type === 'object' || type === 'function') && name !== 'globalThis') { +// // Object.freeze(prop); +// // } +// // if(type === 'function' && Object.prototype.hasOwnProperty.call(prop, 'prototype')) { +// // Object.freeze(prop.prototype); +// // } +// // Object.defineProperty(globalThis, name, { writable: false, configurable: false }); +// // }); +// // } + +// process(inputs, [outputData], params) { +// if (currentTime <= params.begin[0]) { +// return true; +// } +// if (currentTime >= params.end[0]) { +// return false; +// } + + + +// const chDataLen = outputData[0].length; +// if(!chDataLen || !this.isPlaying) { +// return true; +// } +// let time = currentTime; +// let { byteSample } = this; +// const drawBuffer = []; +// const isDiagram = this.drawMode === 'Combined' || this.drawMode === 'Diagram' || this.drawMode === 'Spectrogram'; + +// for(let i = 0; i < chDataLen; ++i) { +// time += this.sampleRatio; +// const CT = Math.floor(time); +// if(this.lastTime !== CT) { +// let funcValue; +// const currentSample = Math.floor(byteSample); + +// // long cascade of null handlers +// const inputs0 = inputs[0] ?? [ ]; +// const inputs00 = inputs0[0] ?? [ ]; +// const inputs01 = inputs0[1] ?? inputs00; +// const inputs00i = inputs00[i] ?? 0; +// const inputs01i = inputs01[i] ?? 0; +// const micSample = [inputs00i, inputs01i, inputs00i / 2 + inputs01i / 2]; + +// if(this.isFuncbeat) { +// funcValue = this.func(currentSample / this.sampleRate, this.sampleRate, +// currentSample, micSample); +// } else { +// funcValue = this.func(currentSample, micSample); +// } + +// funcValue = Array.isArray(funcValue) ? [funcValue[0], funcValue[1]] : [funcValue, funcValue]; +// let hasValue = false; +// let ch = 2; +// while(ch--) { +// try { +// funcValue[ch] = +funcValue[ch]; +// } catch(err) { +// funcValue[ch] = NaN; +// } +// if(isDiagram) { +// if(!isNaN(funcValue[ch])) { +// this.outValue[ch] = this.getValues(funcValue[ch], ch); +// } else { +// this.lastByteValue[ch] = NaN; +// } +// hasValue = true; +// continue; +// } +// if(funcValue[ch] === this.lastFuncValue[ch]) { +// continue; +// } else if(!isNaN(funcValue[ch])) { +// this.outValue[ch] = this.getValues(funcValue[ch], ch); +// hasValue = true; +// } else if(!isNaN(this.lastFuncValue[ch])) { +// this.lastByteValue[ch] = NaN; +// hasValue = true; +// } +// } +// if(hasValue) { +// drawBuffer.push({ t: currentSample, value: [...this.lastByteValue] }); +// } +// byteSample += CT - this.lastTime; +// this.lastFuncValue = funcValue; +// this.lastTime = CT; +// } +// outputData[0][i] = this.outValue[0]; +// outputData[1][i] = this.outValue[1]; +// } +// if(Math.abs(byteSample) > Number.MAX_SAFE_INTEGER) { +// this.resetTime(); +// return true; +// } +// this.audioSample += chDataLen; +// let isSend = false; +// const data = {}; +// if(byteSample !== this.byteSample) { +// isSend = true; +// data.byteSample = this.byteSample = byteSample; +// } +// if(drawBuffer.length) { +// isSend = true; +// data.drawBuffer = drawBuffer; +// } +// if(isSend) { +// this.sendData(data); +// } +// return true; +// } +// receiveData(data) { +// if(data.byteSample !== undefined) { +// this.byteSample = +data.byteSample || 0; +// this.resetValues(); +// } + + +// if(data.playbackSpeed !== undefined) { +// const sampleRatio = this.sampleRatio / this.playbackSpeed; +// this.playbackSpeed = data.playbackSpeed; +// this.setSampleRatio(sampleRatio); +// } +// if(data.mode !== undefined) { +// this.isFuncbeat = data.mode === 'Funcbeat'; +// switch (data.mode) { +// case 'Bytebeat': +// this.getValues = (funcValue, ch) => (this.lastByteValue[ch] = funcValue & 255) / 127.5 - 1; +// break; +// case 'Signed Bytebeat': +// this.getValues = (funcValue, ch) => +// (this.lastByteValue[ch] = (funcValue + 128) & 255) / 127.5 - 1; +// break; +// case 'Floatbeat': +// case 'Funcbeat': +// this.getValues = (funcValue, ch) => { +// const limited = Math.max(Math.min(funcValue, 1), -1); +// this.lastByteValue[ch] = limited * 127.5 + 127.5 | 0 +// return limited; +// }; +// break; +// case 'Bitbeat': +// this.getValues = (funcValue, ch) => { +// this.lastByteValue[ch] = funcValue & 1 ? 255 : 0; +// return (funcValue & 1) - 0.5; +// }; +// break; +// case '2048': +// this.getValues = (funcValue, ch) => { +// this.lastByteValue[ch] = funcValue / 8 & 255; +// return (funcValue & 2047) / 1023.5 - 1 +// }; +// break; +// case 'logmode': +// this.getValues = (funcValue, ch) => (this.lastByteValue[ch] = (Math.log2(funcValue) * 32) & 255) / 127.5 - 1; +// break; +// case 'logHack': +// this.getValues = (funcValue, ch) => { +// const neg = (funcValue < 0) ? -32 : 32; +// return (this.lastByteValue[ch] = (Math.log2(Math.abs(funcValue)) * neg) & 255) / 127.5 - 1; +// }; +// break; +// case 'logHack2': +// this.getValues = (funcValue, ch) => { +// const neg = funcValue < 0 +// return funcValue == 0 ? 0 : ((this.lastByteValue[ch] = ((Math.log2(Math.abs(funcValue)) * (neg ? -16 : 16)) + (neg ? -127 : 128)) & 255) / 127.5 - 1); +// }; +// break; + +// default: this.getValues = (_funcValue) => NaN; +// } +// } +// if(data.setFunction !== undefined) { +// this.setFunction(data.setFunction); +// } +// if(data.resetTime === true) { +// this.resetTime(); +// } +// if(data.sampleRate !== undefined) { +// this.sampleRate = data.sampleRate; +// } +// if(data.sampleRatio !== undefined) { +// this.setSampleRatio(data.sampleRatio); +// } +// if(data.divisor !== undefined) { +// this.sampleDivisor = data.divisor; +// } +// if(data.DMode !== undefined) { +// this.soundMode = data.DMode; +// } +// if(data.drawMode !== undefined) { +// this.drawMode = data.drawMode; +// } +// } +// sendData(data) { +// this.port.postMessage(data); +// } +// resetTime() { +// this.byteSample = 0; +// this.resetValues(); +// this.sendData({ byteSample: 0 }); +// } +// resetValues() { +// this.audioSample = 0; +// this.lastTime = -1; +// this.outValue = [0, 0]; +// this.lastFuncValue = [null,null]; +// } +// setFunction(codeText) { +// const chyx = { +// /*bit*/ "bitC": function (x, y, z) { return x & y ? z : 0 }, +// /*bit reverse*/"br": function (x, size = 8) { +// if(size > 32) { throw new Error("br() Size cannot be greater than 32") } else { +// let result = 0; +// for (let idx = 0; idx < (size - 0); idx++) { +// result += chyx.bitC(x, 2 ** idx, 2 ** (size - (idx + 1))) +// } +// return result +// } +// }, +// /*sin that loops every 128 "steps", instead of every pi steps*/"sinf": function (x) { return Math.sin(x / (128 / Math.PI)) }, +// /*cos that loops every 128 "steps", instead of every pi steps*/"cosf": function (x) { return Math.cos(x / (128 / Math.PI)) }, +// /*tan that loops every 128 "steps", instead of every pi steps*/"tanf": function (x) { return Math.tan(x / (128 / Math.PI)) }, +// /*converts t into a string composed of it's bits, regex's that*/"regG": function (t, X) { return X.test(t.toString(2)) } +// /*corrupt sound"crpt": function(x,y=8) {return chyx.br(chyx.br(x,y)+t,y)^chyx.br(t,y)}, +// decorrupt sound"decrpt": function(x,y=8) {return chyx.br(chyx.br(x^chyx.br(t,y),y)-t,y)},*/ +// } +// // Create shortened Math functions +// const params = Object.getOwnPropertyNames(Math); +// const values = params.map(k => Math[k]); +// const chyxNames = Object.getOwnPropertyNames(chyx); +// const chyxFuncs = chyxNames.map(k => chyx[k]); +// params.push('int', 'window', ...chyxNames); +// values.push(Math.floor, globalThis, ...chyxFuncs); +// // ByteBeatProcessor.deleteGlobals(); +// // Code testing +// let isCompiled = false; +// const oldFunc = this.func; + + +// if(this.isFuncbeat) { +// this.func = new Function(...params, codeText).bind(globalThis, ...values); +// } else { +// // Optimize code like eval(unescape(escape`XXXX`.replace(/u(..)/g,"$1%"))) +// codeText = codeText.trim().replace( +// /^eval\(unescape\(escape(?:`|\('|\("|\(`)(.*?)(?:`|'\)|"\)|`\)).replace\(\/u\(\.\.\)\/g,["'`]\$1%["'`]\)\)\)$/, +// (match, m1) => unescape(escape(m1).replace(/u(..)/g, '$1%'))); +// this.func = new Function(...params, 't', '_micSample', `return 0,\n${ codeText || 0 };`) +// .bind(globalThis, ...values); +// } +// isCompiled = true; +// if(this.isFuncbeat) { +// this.func = this.func(); +// this.func(0, this.sampleRate, 0, [0, 0, 0]); +// } else { +// this.func(0, [0, 0, 0]); +// } + + +// this.sendData({ error: { message: '', isCompiled }, updateUrl: true }); +// } +// setSampleRatio(sampleRatio) { +// const timeOffset = Math.floor(this.sampleRatio * this.audioSample) - this.lastTime; +// this.sampleRatio = sampleRatio * this.playbackSpeed; +// this.lastTime = Math.floor(this.sampleRatio * this.audioSample) - timeOffset; +// } +// } + +// registerProcessor('byte-beat-processor', ByteBeatProcessor); \ No newline at end of file