Ambient-Musik auf Telefonen leichter gegen Aussetzer; sw v84
All checks were successful
Build and push Docker image / build (push) Successful in 13s
Test / test (push) Successful in 6m57s

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 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-04 10:42:45 +02:00
parent 14720b3669
commit 45e498eb87
2 changed files with 33 additions and 13 deletions

View file

@ -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, // 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). // kürzerer Hall, kein Oversampling, keine Körner-Aufnahme im Hauptthread).
const LITE = /Android|iPhone|iPad|Mobile/i.test(navigator.userAgent); 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); 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
@ -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 // 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) { function padVoice(f, when, dur, vel = 1) {
const ctx = S.ctx, c = S.cur, G = S.g; 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(); const out = ctx.createGain(), flt = ctx.createBiquadFilter(), pan = ctx.createStereoPanner();
flt.type = "lowpass"; flt.Q.value = .5; flt.type = "lowpass"; flt.Q.value = .5;
flt.frequency.value = clamp(f * (1.2 + c.bright * 7), 200, 9000); 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; const o = ctx.createOscillator(); o.type = type; o.frequency.value = f * mul; o.detune.value = det;
return add(o, gain); 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), (LITE ? .5 : .35) * c.saw);
mk("sawtooth", 1, -c.detune * rnd(.6, 1.2), .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("triangle", 1, c.detune * .3, .5 * (1 - c.saw * .5) * (1 - c.wt * .6));
mk("sine", .5, 0, .35); mk("sine", .5, 0, .35);
if (c.wt > .04) { // zwei Wavetable-Zyklen, über die Notendauer übergeblendet 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 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); const ga = add(mkW(pick(S.waves)), .45 * c.wt);
ga.gain.setValueAtTime(.45 * c.wt, when); ga.gain.linearRampToValueAtTime(0, when + dur); if (!LITE) { // Telefone: ein Zyklus ohne Überblendung
gb.gain.setValueAtTime(0, when); gb.gain.linearRampToValueAtTime(.45 * c.wt, when + dur); 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 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 mg = ctx.createGain(); mg.gain.value = f * c.fm * 1.6;
const car = ctx.createOscillator(); car.type = "sine"; car.frequency.value = f; 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 // Chor: Sägezahn durch drei Formantfilter, Vokal gleitet während der Note
function choirVoice(f, when, dur) { function choirVoice(f, when, dur) {
const ctx = S.ctx, G = S.g; const ctx = S.ctx, G = S.g;
if (!voiceSlot(when, when + dur + 8)) return;
const out = ctx.createGain(), pan = ctx.createStereoPanner(), src = ctx.createGain(); const out = ctx.createGain(), pan = ctx.createStereoPanner(), src = ctx.createGain();
pan.pan.value = rnd(-.6, .6); pan.pan.value = rnd(-.6, .6);
const att = rnd(3, 6), rel = rnd(5, 8), end = envelope(out, when, att, dur, rel, .16); 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() { function playChord() {
if (!S.running) return; 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 => { renderSwell(freqFromDegree(S.degree + S.scale.length)).then(buf => {
if (!S.running) return; if (!S.running) return;
const t = S.ctx.currentTime + .1; const t = S.ctx.currentTime + .1;
@ -1279,7 +1295,7 @@ function draw(ts) {
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 if (LITE && ts - lastDraw < 48) return; // Telefone: ~20 Bilder/s reichen
lastDraw = ts; 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);
@ -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; 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; } if (p.life > p.max) { parts.splice(i, 1); continue; }
const k = p.life / p.max, a = Math.sin(Math.PI * k) * .35; 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); if (LITE) cx.fillStyle = `hsla(${p.h},80%,65%,${a * .45})`; // Telefone: einfache Scheiben statt Verläufen
gr.addColorStop(0, `hsla(${p.h},80%,65%,${a})`); gr.addColorStop(1, "hsla(0,0%,0%,0)"); else {
cx.fillStyle = gr; cx.beginPath(); cx.arc(p.x, p.y, p.r, 0, 6.3); cx.fill(); 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; } if (S.flash > .02) { cx.fillStyle = `rgba(190,205,255,${S.flash * .28})`; cx.fillRect(0, 0, W, H); S.flash *= .9; }
// Spektrumlinie // Spektrumlinie

View file

@ -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-v83'; const CACHE = 'diora-v84';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app.js', '/static/js/app.js',