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