Ambient-Musik auf Telefonen leichter gegen Aussetzer; sw v84
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:
parent
14720b3669
commit
45e498eb87
2 changed files with 33 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue