Ambient: leichter Modus für Telefone gegen Ruckeln; sw v79
Auf dem S21 lag die App mit Drift bei ~185 % CPU, der Ton stockte. Auf Telefonen jetzt: Bild in einfacher Auflösung und ~30 Bilder/s, 3,5 s statt 7 s Hall, kein Oversampling, kein ScriptProcessor (Körner) im Hauptthread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bd9193c869
commit
2c64342c53
2 changed files with 30 additions and 19 deletions
|
|
@ -97,6 +97,10 @@
|
||||||
<script>
|
<script>
|
||||||
"use strict";
|
"use strict";
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
|
// Telefone: leichter rechnen, damit der Ton nicht ruckelt (Bild in einfacher Auflösung, ~30 Bilder/s,
|
||||||
|
// kürzerer Hall, kein Oversampling, keine Körner-Aufnahme im Hauptthread).
|
||||||
|
const LITE = /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent);
|
||||||
|
const DPR = () => LITE ? 1 : devicePixelRatio;
|
||||||
const QS = new URLSearchParams(location.search);
|
const QS = new URLSearchParams(location.search);
|
||||||
if (QS.get("embed") === "1") document.documentElement.classList.add("embed");
|
if (QS.get("embed") === "1") document.documentElement.classList.add("embed");
|
||||||
let BG = [11, 14, 20]; // Hintergrundfarbe, per ?bg=%23101010 überschreibbar
|
let BG = [11, 14, 20]; // Hintergrundfarbe, per ?bg=%23101010 überschreibbar
|
||||||
|
|
@ -235,7 +239,7 @@ function buildGraph() {
|
||||||
// gemeinsamer Summenbus -> Tape-Sättigung -> Kompressor
|
// gemeinsamer Summenbus -> Tape-Sättigung -> Kompressor
|
||||||
g.mix = ctx.createGain();
|
g.mix = ctx.createGain();
|
||||||
g.drive = ctx.createGain(); g.drive.gain.value = 1.5;
|
g.drive = ctx.createGain(); g.drive.gain.value = 1.5;
|
||||||
g.shaper = ctx.createWaveShaper(); g.shaper.curve = satCurve(1.6); g.shaper.oversample = "2x";
|
g.shaper = ctx.createWaveShaper(); g.shaper.curve = satCurve(1.6); g.shaper.oversample = LITE ? "none" : "2x";
|
||||||
g.makeup = ctx.createGain(); g.makeup.gain.value = .72;
|
g.makeup = ctx.createGain(); g.makeup.gain.value = .72;
|
||||||
g.comp = ctx.createDynamicsCompressor();
|
g.comp = ctx.createDynamicsCompressor();
|
||||||
g.comp.threshold.value = -18; g.comp.ratio.value = 3; g.comp.attack.value = .05; g.comp.release.value = .4;
|
g.comp.threshold.value = -18; g.comp.ratio.value = 3; g.comp.attack.value = .05; g.comp.release.value = .4;
|
||||||
|
|
@ -246,7 +250,7 @@ function buildGraph() {
|
||||||
g.filter.connect(g.dry).connect(g.mix);
|
g.filter.connect(g.dry).connect(g.mix);
|
||||||
|
|
||||||
// Hall
|
// Hall
|
||||||
g.conv = ctx.createConvolver(); g.conv.buffer = makeImpulse(ctx, 7, 2.6);
|
g.conv = ctx.createConvolver(); g.conv.buffer = makeImpulse(ctx, LITE ? 3.5 : 7, 2.6);
|
||||||
g.send = ctx.createGain(); g.send.gain.value = S.verb;
|
g.send = ctx.createGain(); g.send.gain.value = S.verb;
|
||||||
g.filter.connect(g.send).connect(g.conv);
|
g.filter.connect(g.send).connect(g.conv);
|
||||||
g.conv.connect(g.mix);
|
g.conv.connect(g.mix);
|
||||||
|
|
@ -294,16 +298,20 @@ function buildGraph() {
|
||||||
g.noiseBus.connect(g.mix);
|
g.noiseBus.connect(g.mix);
|
||||||
const nSend = ctx.createGain(); nSend.gain.value = .3; g.noiseBus.connect(nSend).connect(g.conv);
|
const nSend = ctx.createGain(); nSend.gain.value = .3; g.noiseBus.connect(nSend).connect(g.conv);
|
||||||
|
|
||||||
// Granular: Ringpuffer der Stimmen, daraus werden Körner geschnitten
|
// Granular: Ringpuffer der Stimmen, daraus werden Körner geschnitten. Der ScriptProcessor läuft im
|
||||||
S.ring = new Float32Array(ctx.sampleRate * 12); S.wp = 0; S.written = 0;
|
// Hauptthread und hält bei Last den ganzen Graphen auf – auf Telefonen gibt es daher keine Körner.
|
||||||
const proc = ctx.createScriptProcessor(4096, 1, 1);
|
S.wp = 0; S.written = 0;
|
||||||
proc.onaudioprocess = e => {
|
if (!LITE) {
|
||||||
const inp = e.inputBuffer.getChannelData(0), n = S.ring.length;
|
S.ring = new Float32Array(ctx.sampleRate * 12);
|
||||||
for (let i = 0; i < inp.length; i++) { S.ring[S.wp] = inp[i]; S.wp = (S.wp + 1) % n; }
|
const proc = ctx.createScriptProcessor(4096, 1, 1);
|
||||||
S.written += inp.length;
|
proc.onaudioprocess = e => {
|
||||||
};
|
const inp = e.inputBuffer.getChannelData(0), n = S.ring.length;
|
||||||
const zero = ctx.createGain(); zero.gain.value = 0;
|
for (let i = 0; i < inp.length; i++) { S.ring[S.wp] = inp[i]; S.wp = (S.wp + 1) % n; }
|
||||||
g.voiceBus.connect(proc); proc.connect(zero).connect(ctx.destination);
|
S.written += inp.length;
|
||||||
|
};
|
||||||
|
const zero = ctx.createGain(); zero.gain.value = 0;
|
||||||
|
g.voiceBus.connect(proc); proc.connect(zero).connect(ctx.destination);
|
||||||
|
}
|
||||||
g.grainBus = ctx.createGain(); g.grainBus.connect(g.filter);
|
g.grainBus = ctx.createGain(); g.grainBus.connect(g.filter);
|
||||||
|
|
||||||
g.modeBus = ctx.createGain(); g.modeBus.connect(g.mix); // Klangwelten (Rauschen, Natur, …)
|
g.modeBus = ctx.createGain(); g.modeBus.connect(g.mix); // Klangwelten (Rauschen, Natur, …)
|
||||||
|
|
@ -464,7 +472,7 @@ function playSwell(buf, t) {
|
||||||
// Granulare Wolke aus dem eigenen Output
|
// Granulare Wolke aus dem eigenen Output
|
||||||
function snapGrains() {
|
function snapGrains() {
|
||||||
const sr = S.ctx.sampleRate;
|
const sr = S.ctx.sampleRate;
|
||||||
if (S.written < sr * 6) return;
|
if (!S.ring || S.written < sr * 6) return;
|
||||||
const len = Math.min(S.written, S.ring.length), b = S.ctx.createBuffer(1, len, sr), d = b.getChannelData(0);
|
const len = Math.min(S.written, S.ring.length), b = S.ctx.createBuffer(1, len, sr), d = b.getChannelData(0);
|
||||||
for (let i = 0; i < len; i++) d[i] = S.ring[(S.wp - len + i + S.ring.length) % S.ring.length];
|
for (let i = 0; i < len; i++) d[i] = S.ring[(S.wp - len + i + S.ring.length) % S.ring.length];
|
||||||
S.grainBuf = b;
|
S.grainBuf = b;
|
||||||
|
|
@ -1189,8 +1197,8 @@ const cv = $("viz"), cx = cv.getContext("2d");
|
||||||
S.visUntil = performance.now() + 1500;
|
S.visUntil = performance.now() + 1500;
|
||||||
function wake() { if (!rafId) rafId = requestAnimationFrame(draw); }
|
function wake() { if (!rafId) rafId = requestAnimationFrame(draw); }
|
||||||
function resize() {
|
function resize() {
|
||||||
cv.width = Math.round(cv.clientWidth * devicePixelRatio);
|
cv.width = Math.round(cv.clientWidth * DPR());
|
||||||
cv.height = Math.round(cv.clientHeight * devicePixelRatio);
|
cv.height = Math.round(cv.clientHeight * DPR());
|
||||||
wake();
|
wake();
|
||||||
}
|
}
|
||||||
resize();
|
resize();
|
||||||
|
|
@ -1204,9 +1212,9 @@ function spawnParticles(ev) {
|
||||||
const keyHue = (S.root % 12) * 30 + 200;
|
const keyHue = (S.root % 12) * 30 + 200;
|
||||||
for (let i = 0; i < count && parts.length < 450; i++) {
|
for (let i = 0; i < count && parts.length < 450; i++) {
|
||||||
parts.push({
|
parts.push({
|
||||||
x: x + rnd(-40, 40, Math.random) * devicePixelRatio, y: H * rnd(.55, .85, Math.random),
|
x: x + rnd(-40, 40, Math.random) * DPR(), y: H * rnd(.55, .85, Math.random),
|
||||||
vx: rnd(-.12, .12, Math.random), vy: -rnd(.08, .4, Math.random) * (ev.kind === "bell" ? 1.6 : 1),
|
vx: rnd(-.12, .12, Math.random), vy: -rnd(.08, .4, Math.random) * (ev.kind === "bell" ? 1.6 : 1),
|
||||||
r: (ev.kind === "bell" ? rnd(4, 9, Math.random) : rnd(10, 30, Math.random)) * devicePixelRatio,
|
r: (ev.kind === "bell" ? rnd(4, 9, Math.random) : rnd(10, 30, Math.random)) * DPR(),
|
||||||
life: 0, max: ev.kind === "bell" ? rnd(180, 320, Math.random) : Math.min(ev.dur * 40, 700),
|
life: 0, max: ev.kind === "bell" ? rnd(180, 320, Math.random) : Math.min(ev.dur * 40, 700),
|
||||||
h: keyHue + (ev.kind === "choir" ? 60 : ev.kind === "bell" ? 25 : 0) + rnd(-12, 12, Math.random),
|
h: keyHue + (ev.kind === "choir" ? 60 : ev.kind === "bell" ? 25 : 0) + rnd(-12, 12, Math.random),
|
||||||
});
|
});
|
||||||
|
|
@ -1235,13 +1243,16 @@ function modeVis(W, H, dpr) {
|
||||||
if (n(.06 + a * .05)) add({ x: Math.random() * W, y: H * r(.5, .95), vx: r(-.2, .2), vy: -r(.1, .5), r: r(10, 28) * dpr, max: r(300, 600) });
|
if (n(.06 + a * .05)) add({ x: Math.random() * W, y: H * r(.5, .95), vx: r(-.2, .2), vy: -r(.1, .5), r: r(10, 28) * dpr, max: r(300, 600) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let lastDraw = 0;
|
||||||
function draw(ts) {
|
function draw(ts) {
|
||||||
rafId = 0;
|
rafId = 0;
|
||||||
const W = cv.width, H = cv.height, dpr = devicePixelRatio;
|
const W = cv.width, H = cv.height, dpr = DPR();
|
||||||
// Nichts zeichnen (und nicht neu planen), wenn versteckt, 0 px groß oder die Musik steht
|
// Nichts zeichnen (und nicht neu planen), wenn versteckt, 0 px groß oder die Musik steht
|
||||||
if (document.hidden || !W || !H || !cv.clientWidth) return;
|
if (document.hidden || !W || !H || !cv.clientWidth) return;
|
||||||
if (!S.running && performance.now() > S.visUntil) return;
|
if (!S.running && performance.now() > S.visUntil) return;
|
||||||
rafId = requestAnimationFrame(draw);
|
rafId = requestAnimationFrame(draw);
|
||||||
|
if (LITE && ts - lastDraw < 32) return; // Telefone: ~30 Bilder/s reichen
|
||||||
|
lastDraw = ts;
|
||||||
cx.globalCompositeOperation = "source-over";
|
cx.globalCompositeOperation = "source-over";
|
||||||
cx.fillStyle = `rgba(${BG[0]},${BG[1]},${BG[2]},.16)`; cx.fillRect(0, 0, W, H);
|
cx.fillStyle = `rgba(${BG[0]},${BG[1]},${BG[2]},.16)`; cx.fillRect(0, 0, W, H);
|
||||||
if (!S.g) return;
|
if (!S.g) return;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v78';
|
const CACHE = 'diora-v79';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
'/static/js/app.js',
|
'/static/js/app.js',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue