diff --git a/packages/superdough/remove.html b/packages/superdough/remove.html deleted file mode 100644 index 59476043c..000000000 --- a/packages/superdough/remove.html +++ /dev/null @@ -1,295 +0,0 @@ -🌱 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/synth.mjs b/packages/superdough/synth.mjs index 554bb1e9a..abadec311 100644 --- a/packages/superdough/synth.mjs +++ b/packages/superdough/synth.mjs @@ -147,8 +147,14 @@ export function registerSynthSounds() { registerSound( 'bytebeat', (begin, value, onended) => { + const defaultBeats = [ + '(t%255 >= t/255%255)*255', + '(t*(t*8%60 <= 300)|(-t)*(t*4%512 < 256))+t/400', + 't', + ] + const { n } = value + const { byteBeatExpression = defaultBeats[n % defaultBeats.length] } = value const ac = getAudioContext(); - let { byteBeatExpression } = value; let { duration } = value; const [attack, decay, sustain, release] = getADSRValues( diff --git a/packages/superdough/worklets.mjs b/packages/superdough/worklets.mjs index 3d1bbccbe..282c9a717 100644 --- a/packages/superdough/worklets.mjs +++ b/packages/superdough/worklets.mjs @@ -762,28 +762,22 @@ 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.codeText = '0'; this.port.onmessage = (event) => { - this.codeText = event.data.trim().replace( + 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.virtualRate = 112600; // 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.framebuffer = new Float32Array(Math.floor(sampleRate / 60)); this.func = null; } @@ -829,64 +823,19 @@ class ByteBeatProcessor extends AudioWorkletProcessor { 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); - - + this.func = Function('t', 'return ' + codeText); const output = outputs[0]; - const ct = params.begin[0] const tIncrement = this.virtualRate / sampleRate - - // 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) + t = t / (this.virtualRate / 256) * freq + const funcValue = this.func(t) let signal = (funcValue & 255) / 127.5 - 1 const out = signal; @@ -894,363 +843,12 @@ class ByteBeatProcessor extends AudioWorkletProcessor { 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