mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-12 22:15:27 -04:00
WIP oscillations
This commit is contained in:
Vendored
+16
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
<html><!-- license: AGPL-3.0 --><head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🌱 audio worklets</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #222;
|
||||
max-width: 500px;
|
||||
margin: auto;
|
||||
font-family: serif;
|
||||
font-size: 1.2em;
|
||||
color: #edd;
|
||||
text-align: left;
|
||||
padding: 20px 8px;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "FontWithASyntaxHighlighter";
|
||||
src: url("/fonts/FontWithASyntaxHighlighter-Regular.woff2")
|
||||
format("woff2");
|
||||
}
|
||||
a {
|
||||
color: cyan;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
textarea,
|
||||
pre {
|
||||
overflow: auto;
|
||||
font-family: "FontWithASyntaxHighlighter", monospace;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
border: 0;
|
||||
outline: none;
|
||||
background-color: #44444490;
|
||||
border: 0;
|
||||
color: white;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
background-color: #44444490;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>🌱 audio worklets</h2>
|
||||
<p>
|
||||
In the <a href="/audio-dsp/buffers.html">last post</a>, 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:
|
||||
</p>
|
||||
<div>
|
||||
<small>💡 hit ctrl+enter to update the code & ctrl+. to stop</small>
|
||||
</div>
|
||||
<textarea id="code" spellcheck="false" rows="8"></textarea>
|
||||
<a id="play">play</a> | <a id="stop">stop</a>
|
||||
<canvas id="canvas" width="800" height="200"></canvas>
|
||||
<p>
|
||||
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).
|
||||
</p>
|
||||
<p>Here are some examples to try:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#TWF0aC5zaW4odC80NDEwMCoyKk1hdGguUEkqMTIxKQ==">hello sine</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#TWF0aC5zaW4odC80NDEwMCoyKk1hdGguUEkqMik=">slow sine (inaudible)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KApNYXRoLnNpbih0LzQ0MTAwKjIqTWF0aC5QSSoyMDApCitNYXRoLnNpbih0LzQ0MTAwKjIqTWF0aC5QSSozMDApCitNYXRoLnNpbih0LzQ0MTAwKjIqTWF0aC5QSSo0MDApCikvNA==">
|
||||
just intonation triad
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KHQvNDQxMDApKjYxJTEtLjU=">hello saw</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KCh0LzQ0MTAwKjYxKSUxPi41Py0xOjEpLzI=">hello pulse</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KHQ9dC80NDEwMCwKTWF0aC5zaW4odCoyKk1hdGguUEkqMTIwKS8zCitNYXRoLnNpbih0KjIqTWF0aC5QSSozMDEpLzMKKQ==">wandering sine</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KCgoIC8vIGJ5IHN0aW1tZXIgKDIwMTEtMTAtMDMpCnQqKDR8dD4+MTMmMyk+Pih+dD4+MTEmMSkmMTI4fHQqKHQ+PjExJnQ+PjEzKSoofnQ+PjkmMykmMTI3CikgJiAyNTUpIC8gMTI3LjUgLSAxKS80">bytebeat 1</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KCgoIC8vIGxocGhyIC0gRnJhY3RhbGl6ZWQgUGFzdAoodD4+MTBedD4+MTEpJTUqKCh0Pj4xNCYzXnQ+PjE1JjEpKzEpKnQlOTkrKCgzKyh0Pj4xNCYzKS0odD4+MTYmMSkpLzMqdCU5OSY2NCkKKSAmIDI1NSkgLyAxMjcuNSAtIDEpLzQ=">bytebeat 2</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#ICh0PXQvNDQxMDAsCk1hdGguc2luKHQqMSpNYXRoLlBJKjM1MCkvNAorTWF0aC5zaW4odCoxKk1hdGguUEkqNTIwKS84IAorTWF0aC5zaW4odCoyKk1hdGguUEkqKCgodCo5KSUxMj4uNT8tLjg6MTcwMCkpKS8xMAorTWF0aC5zaW4odCoyKk1hdGguUEkqKCgodCo0KSUyPi4yPy0uODoyMjAwKSkpLzEwCitNYXRoLnNpbih0KjIqTWF0aC5QSSooKCh0KjMpJTE+LjI/LS44OjM2MDApKSkvMTAKKygodCoyKSUxPi4yPy0uODouOCkvNAorKCh0KSUxPi4zPy0uODouOCkvMgop">gladyouask - chirps</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#KHQ9dC80NDEwMCwKLy8gc291cmNlOiBodHRwczovL3d3dy5yZWRkaXQuY29tL3IvZWRtcHJvZHVjdGlvbi9jb21tZW50cy8xOHVtNXgzL3RpcHNfYW5kX3RyaWNrc19mb3JfYXV0aGVudGljX3RyODA4X3NvdW5kc19raWNrLwpbMjA0LDI5OCwzNjYsNTE1LDU0MCw4MDBdLm1hcChoej0+eyAvLyBoaWhhdCB0dW5pbmcKICBsZXQgYT0wOwogIGNvbnN0IEkwPTE4OyAvLyBmaXJzdCBoYXJtb255IChoaXBhc3MgZmlsdGVyKQogIGNvbnN0IE49MzA7IC8vIGhhcm1vbnkgY291bnQKICAvLyBzcXVhcmUgd2F2ZSBmb3VyaWVyIHNlcmllczoKICBmb3IgKGxldCBpID0gSTA7IGkgPCBJMCtOOyBpKz0yKSB7CiAgICBpZiAoaHoqaSA+PSAyMjAwMCkgYnJlYWs7IC8vIHN0b3AgYXQgbnlxdWlzdCBsaW1pdAogICAgYSArPSBNYXRoLnNpbih0KjIqTWF0aC5QSSpoeippKS9OOwogIH07CiAgcmV0dXJuIGE7Cn0pLnJlZHVjZSgoYSxiKT0+YStiKQoqCjAuMSpbMTAsMSwyLDUsOSwxLDQsNywxMCwwLDIsNSwxLDcsMSw3XVsoKHQqMTApfDApJTE2XSAvLyBwYXR0ZXJuCioKKCgxLSgodCUwLjEpKjEwKSkqKjEuNykpIC8qIGVudmVsb3BlICov">aks - TR-808-like hihat</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<details>
|
||||
<summary id="loc">show page source</summary>
|
||||
<pre id="pre"></pre>
|
||||
</details>
|
||||
<p>
|
||||
<a href="/">back to garten.salat</a>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
// init canvas
|
||||
const canvas = document.querySelector("#canvas");
|
||||
const drawContext = canvas.getContext("2d");
|
||||
canvas.width = 800;
|
||||
canvas.height = 200;
|
||||
drawSilence(drawContext);
|
||||
|
||||
// init audio context
|
||||
const ctx = new AudioContext();
|
||||
document.addEventListener("click", function firstClick() {
|
||||
ctx.resume(); // autoply policy
|
||||
document.removeEventListener("click", firstClick);
|
||||
});
|
||||
|
||||
// init code input
|
||||
const input = document.querySelector("#code");
|
||||
document.querySelector("#play").onclick = () => play();
|
||||
document.querySelector("#stop").onclick = () => stop();
|
||||
let worklet;
|
||||
async function play() {
|
||||
await ctx.resume();
|
||||
stop();
|
||||
worklet = await runWorklet(ctx, input.value);
|
||||
worklet.port.onmessage = (e) => drawBuffer(e.data);
|
||||
}
|
||||
let stop = () => {
|
||||
worklet?.disconnect();
|
||||
worklet?.port.postMessage("stop");
|
||||
};
|
||||
|
||||
// read base64 code from url
|
||||
let urlCode = window.location.hash.slice(1);
|
||||
if (urlCode) {
|
||||
urlCode = atob(urlCode);
|
||||
console.log("loaded code from url!");
|
||||
}
|
||||
input.value = urlCode || `Math.sin(t/44100*2*Math.PI*220)/2`;
|
||||
// live coding / sharing logic
|
||||
window.addEventListener("hashchange", function () {
|
||||
const urlCode = atob(window.location.hash.slice(1));
|
||||
input.value = urlCode;
|
||||
play();
|
||||
});
|
||||
input.addEventListener("keydown", async (e) => {
|
||||
if ((e.ctrlKey || e.altKey) && e.key === "Enter") {
|
||||
const hash = btoa(input.value);
|
||||
let urlCode = window.location.hash.slice(1);
|
||||
if (hash !== urlCode) {
|
||||
window.location.hash = "#" + hash;
|
||||
} else {
|
||||
play();
|
||||
}
|
||||
}
|
||||
if ((e.ctrlKey || e.altKey) && e.code === "Period") {
|
||||
e.preventDefault();
|
||||
stop();
|
||||
}
|
||||
});
|
||||
|
||||
async function runWorklet(ac, expression) {
|
||||
const name = `worklet-${Date.now()}`;
|
||||
const workletCode = `
|
||||
let dsp = (t) => ${expression};
|
||||
class MyProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.t = 0;
|
||||
this.active = true;
|
||||
this.framebuffer = new Float32Array(Math.floor(sampleRate/60));
|
||||
this.port.onmessage = (e) => e.data === "stop" && (this.active = false)
|
||||
}
|
||||
process(inputs, outputs, parameters) {
|
||||
const output = outputs[0];
|
||||
for (let i = 0; i < output[0].length; i++) {
|
||||
const out = dsp(this.t);
|
||||
for (let c = 0; c < output.length; c++)
|
||||
output[c][i] = out;
|
||||
this.t++;
|
||||
this.framebuffer[this.t%this.framebuffer.length] = out;
|
||||
if(this.t % this.framebuffer.length === 0 && this.t > 0) {
|
||||
this.port.postMessage(this.framebuffer)
|
||||
}
|
||||
}
|
||||
return this.active;
|
||||
}
|
||||
}
|
||||
registerProcessor('${name}', MyProcessor);`;
|
||||
await ac.resume();
|
||||
const dataURL = `data:text/javascript;base64,${btoa(workletCode)}`;
|
||||
await ac.audioWorklet.addModule(dataURL);
|
||||
const node = new AudioWorkletNode(ac, name);
|
||||
node.connect(ac.destination);
|
||||
return node;
|
||||
}
|
||||
|
||||
function drawBuffer(samples) {
|
||||
drawContext.clearRect(0, 0, canvas.width, canvas.height);
|
||||
plot(
|
||||
(x) => samples[Math.floor(x)], // this is bad..
|
||||
drawContext,
|
||||
[1, samples.length],
|
||||
[-1, 1],
|
||||
"white",
|
||||
2
|
||||
);
|
||||
}
|
||||
function drawSilence(ctx, color = "white", lineWidth = 2) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, canvas.height / 2);
|
||||
ctx.lineTo(canvas.width, canvas.height / 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// plot function, see /plot.html
|
||||
function plot(
|
||||
fn,
|
||||
ctx,
|
||||
xrange = [-1, 1],
|
||||
yrange = [-1, 1],
|
||||
color = "black",
|
||||
lineWidth = 4
|
||||
) {
|
||||
// these 3 functions are very good to know..
|
||||
const lerp = (v, min, max) => v * (max - min) + min;
|
||||
const invLerp = (v, min, max) => (v - min) / (max - min);
|
||||
const remap = (v, vmin, vmax, omin, omax) =>
|
||||
lerp(invLerp(v, vmin, vmax), omin, omax);
|
||||
// prepare draw context
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
// function ranges
|
||||
const [x0, x1] = xrange;
|
||||
const [y0, y1] = yrange;
|
||||
// draw ranges
|
||||
const [px0, px1] = [0, ctx.canvas.width];
|
||||
const [py0, py1] = [ctx.canvas.height - ctx.lineWidth, ctx.lineWidth];
|
||||
// actual draw logic
|
||||
ctx.beginPath();
|
||||
for (let px = 0; px < ctx.canvas.width; px++) {
|
||||
const x = remap(px, px0, px1, x0, x1);
|
||||
const y = fn(x);
|
||||
const py = remap(y, y0, y1, py0, py1);
|
||||
px === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const html = document.querySelector("html").outerHTML;
|
||||
const loc = html.split("\n").length;
|
||||
document.querySelector("#pre").textContent = html;
|
||||
document.querySelector("#loc").textContent = `show source (${loc} loc)`;
|
||||
</script></body></html>
|
||||
back to garten.salat
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user