From 45e498eb87987b943ea8cf5fad5dba123d6bdf0e Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 4 Oct 2026 10:42:45 +0200 Subject: [PATCH] Ambient-Musik auf Telefonen leichter gegen Aussetzer; sw v84 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf dem S21 lagen App- und Renderer-Prozess bei je ~105 % CPU. Auf Telefonen jetzt: höchstens 10 Stimmen zugleich, eine Säge und ein Wavetable-Zyklus pro Stimme, FM nur deutlich, keine offline gerenderten Rückwärts-Swells (Rechenspitzen), Bild in halber Auflösung mit ~20 Bildern/s und einfachen Partikeln. Co-Authored-By: Claude Opus 5.5 --- static/ambient/index.html | 44 ++++++++++++++++++++++++++++----------- static/js/sw.js | 2 +- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/static/ambient/index.html b/static/ambient/index.html index f8c2767..3118c64 100644 --- a/static/ambient/index.html +++ b/static/ambient/index.html @@ -100,7 +100,8 @@ 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 DPR = () => LITE ? .5 : devicePixelRatio; // Telefone: das Bild in halber Auflösung, hochskaliert +const MAX_VOICES = 10; // Telefone: höchstens so viele Stimmen zugleich (der Grundton zählt nicht) const QS = new URLSearchParams(location.search); if (QS.get("embed") === "1") document.documentElement.classList.add("embed"); let BG = [11, 14, 20]; // Hintergrundfarbe, per ?bg=%23101010 überschreibbar @@ -353,8 +354,18 @@ function envelope(g, when, att, dur, rel, peak) { } // Pad: detunte Säge/Dreieck + Sinus-Sub + Wavetable-Morph + FM, eigener Filter, Tape-Wow +// Telefone: zählt die klingenden Stimmen; false, wenn schon genug klingen. +function voiceSlot(when, end, always) { + if (!LITE) return true; + if (!always && S.voices >= MAX_VOICES) return false; + S.voices = (S.voices || 0) + 1; + setTimeout(() => { S.voices--; }, Math.max(0, end - S.ctx.currentTime) * 1000 + 200); + return true; +} + function padVoice(f, when, dur, vel = 1) { const ctx = S.ctx, c = S.cur, G = S.g; + if (!voiceSlot(when, when + dur + 10, vel >= 1.4)) return; const out = ctx.createGain(), flt = ctx.createBiquadFilter(), pan = ctx.createStereoPanner(); flt.type = "lowpass"; flt.Q.value = .5; flt.frequency.value = clamp(f * (1.2 + c.bright * 7), 200, 9000); @@ -372,18 +383,21 @@ function padVoice(f, when, dur, vel = 1) { const o = ctx.createOscillator(); o.type = type; o.frequency.value = f * mul; o.detune.value = det; return add(o, gain); }; - mk("sawtooth", 1, c.detune * rnd(.6, 1.2), .35 * c.saw); - mk("sawtooth", 1, -c.detune * rnd(.6, 1.2), .35 * c.saw); + mk("sawtooth", 1, c.detune * rnd(.6, 1.2), (LITE ? .5 : .35) * c.saw); + if (!LITE) mk("sawtooth", 1, -c.detune * rnd(.6, 1.2), .35 * c.saw); // Telefone: eine Säge statt zwei mk("triangle", 1, c.detune * .3, .5 * (1 - c.saw * .5) * (1 - c.wt * .6)); mk("sine", .5, 0, .35); if (c.wt > .04) { // zwei Wavetable-Zyklen, über die Notendauer übergeblendet const mkW = w => { const o = ctx.createOscillator(); o.setPeriodicWave(w); o.frequency.value = f; return o; }; - const ga = add(mkW(pick(S.waves)), .45 * c.wt), gb = add(mkW(pick(S.waves)), 0); - ga.gain.setValueAtTime(.45 * c.wt, when); ga.gain.linearRampToValueAtTime(0, when + dur); - gb.gain.setValueAtTime(0, when); gb.gain.linearRampToValueAtTime(.45 * c.wt, when + dur); + const ga = add(mkW(pick(S.waves)), .45 * c.wt); + if (!LITE) { // Telefone: ein Zyklus ohne Überblendung + const gb = add(mkW(pick(S.waves)), 0); + ga.gain.setValueAtTime(.45 * c.wt, when); ga.gain.linearRampToValueAtTime(0, when + dur); + gb.gain.setValueAtTime(0, when); gb.gain.linearRampToValueAtTime(.45 * c.wt, when + dur); + } } - if (c.fm > .03) { + if (c.fm > (LITE ? .25 : .03)) { // Telefone: FM nur, wo sie deutlich hörbar ist const m = ctx.createOscillator(); m.type = "sine"; m.frequency.value = f * c.ratio; const mg = ctx.createGain(); mg.gain.value = f * c.fm * 1.6; const car = ctx.createOscillator(); car.type = "sine"; car.frequency.value = f; @@ -403,6 +417,7 @@ function padVoice(f, when, dur, vel = 1) { // Chor: Sägezahn durch drei Formantfilter, Vokal gleitet während der Note function choirVoice(f, when, dur) { const ctx = S.ctx, G = S.g; + if (!voiceSlot(when, when + dur + 8)) return; const out = ctx.createGain(), pan = ctx.createStereoPanner(), src = ctx.createGain(); pan.pan.value = rnd(-.6, .6); const att = rnd(3, 6), rel = rnd(5, 8), end = envelope(out, when, att, dur, rel, .16); @@ -515,7 +530,8 @@ function spawnChord(t0) { } function playChord() { if (!S.running) return; - if (rng.v() < S.P.swell) { + // Rückwärts-Swell: offline gerendert, auf Telefonen zu teuer (eine Rechenspitze, der Ton stockt) + if (!LITE && rng.v() < S.P.swell) { renderSwell(freqFromDegree(S.degree + S.scale.length)).then(buf => { if (!S.running) return; const t = S.ctx.currentTime + .1; @@ -1279,7 +1295,7 @@ function draw(ts) { if (document.hidden || !W || !H || !cv.clientWidth) return; if (!S.running && performance.now() > S.visUntil) return; rafId = requestAnimationFrame(draw); - if (LITE && ts - lastDraw < 32) return; // Telefone: ~30 Bilder/s reichen + if (LITE && ts - lastDraw < 48) return; // Telefone: ~20 Bilder/s reichen lastDraw = ts; cx.globalCompositeOperation = "source-over"; cx.fillStyle = `rgba(${BG[0]},${BG[1]},${BG[2]},.16)`; cx.fillRect(0, 0, W, H); @@ -1308,9 +1324,13 @@ function draw(ts) { const p = parts[i]; p.life++; p.x += p.vx * dpr + Math.sin((ts + i * 97) * .0007) * .15; p.y += p.vy * dpr; if (p.life > p.max) { parts.splice(i, 1); continue; } const k = p.life / p.max, a = Math.sin(Math.PI * k) * .35; - const gr = cx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r); - gr.addColorStop(0, `hsla(${p.h},80%,65%,${a})`); gr.addColorStop(1, "hsla(0,0%,0%,0)"); - cx.fillStyle = gr; cx.beginPath(); cx.arc(p.x, p.y, p.r, 0, 6.3); cx.fill(); + if (LITE) cx.fillStyle = `hsla(${p.h},80%,65%,${a * .45})`; // Telefone: einfache Scheiben statt Verläufen + else { + const gr = cx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r); + gr.addColorStop(0, `hsla(${p.h},80%,65%,${a})`); gr.addColorStop(1, "hsla(0,0%,0%,0)"); + cx.fillStyle = gr; + } + cx.beginPath(); cx.arc(p.x, p.y, p.r, 0, 6.3); cx.fill(); } if (S.flash > .02) { cx.fillStyle = `rgba(190,205,255,${S.flash * .28})`; cx.fillRect(0, 0, W, H); S.flash *= .9; } // Spektrumlinie diff --git a/static/js/sw.js b/static/js/sw.js index a4305f9..9eb39fc 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v83'; +const CACHE = 'diora-v84'; const SHELL = [ '/static/css/app.css', '/static/js/app.js',