diff --git a/static/ambient/index.html b/static/ambient/index.html index fd541e0..555c36a 100644 --- a/static/ambient/index.html +++ b/static/ambient/index.html @@ -16,6 +16,10 @@ overflow-x:hidden; overflow-y:auto; } .embed header { display:none; } .embed #share { display:none; } + .embed .state { padding-right:58px; } /* oben rechts liegt in diora ein Knopf */ + .noise .m-music, .noise .meters { display:none !important; } + html:not(.noise) .m-noise { display:none; } + .noise:not(.rev) .m-hall { display:none; } main > * { pointer-events:auto; } h1 { margin:0; font-weight:300; letter-spacing:.35em; text-transform:uppercase; font-size:18px; } .sub { color:var(--dim); font-size:13px; margin-top:4px; } @@ -68,20 +72,24 @@
-
@@ -165,6 +173,7 @@ const S = { timers: [], waves: [], loop: { deg:[], oct:[] }, vis: [], ring: null, wp: 0, written: 0, grainBuf: null, }; +let rafId = 0; // Zeichenschleife (siehe wake/draw) const speed = () => 0.35 + S.drift * 2.2; // Spannungsbogen: 0 = ruhig, 1 = Höhepunkt, über mehrere Minuten @@ -293,6 +302,9 @@ function buildGraph() { g.voiceBus.connect(proc); proc.connect(zero).connect(ctx.destination); g.grainBus = ctx.createGain(); g.grainBus.connect(g.filter); + g.modeBus = ctx.createGain(); g.modeBus.connect(g.mix); // Klangwelten (Rauschen, Natur, …) + g.modeRev = ctx.createGain(); g.modeRev.gain.value = 0; g.modeBus.connect(g.modeRev).connect(g.conv); + g.analyser = ctx.createAnalyser(); g.analyser.fftSize = 2048; g.analyser.smoothingTimeConstant = .88; g.master.connect(g.analyser); g.master.connect(ctx.destination); } @@ -562,6 +574,7 @@ function later(fn, minS, maxS, salt, first = .3) { function tick() { if (!S.running) return; + if (!MODES[S.mode].music) { tickMode(); return; } const e = eff(), k = .024 * speed(), t = S.ctx.currentTime, G = S.g; for (const key in S.cur) S.cur[key] += (S.tgt[key] - S.cur[key]) * k; G.filter.frequency.setTargetAtTime(500 + S.cur.bright * 4500 * (.6 + .6 * e), t, 2); @@ -583,6 +596,423 @@ function resetScene() { changeKey(true); setupLoops(); } +// ---------- Klangwelten (Modi) ---------- +// Musik ist nur ein Modus von vielen. Alle anderen sind ebenfalls komplett synthetisiert. +S.mode = "ambient"; S.modeObj = null; S.mp = { a: .5, b: .5 }; S.flash = 0; S.tickId = 0; +const T = () => S.ctx.currentTime; +const sm = (p, v, tc = .6) => p.setTargetAtTime(v, T(), tc); +const R = (a, b) => a + Math.random() * (b - a); +function bq(type, f, Q = .7, gain = 0) { + const n = S.ctx.createBiquadFilter(); n.type = type; n.frequency.value = f; n.Q.value = Q; n.gain.value = gain; return n; +} + +// Rauschfarben: 10 s Stereo-Loop pro Typ, Naht per Equal-Power-Crossfade, auf gleiche Lautheit normalisiert +const NB = {}; +function noiseBuf(type) { + if (NB[type]) return NB[type]; + const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 10, fade = Math.floor(sr * .5), tot = len + fade; + const buf = ctx.createBuffer(2, len, sr); + for (let c = 0; c < 2; c++) { + const raw = new Float32Array(tot); + let b0=0,b1=0,b2=0,b3=0,b4=0,b5=0,b6=0, brown=0, prevP=0, prevW=0; + for (let i = 0; i < tot; i++) { + const w = Math.random() * 2 - 1; + if (type === "white") raw[i] = w; + else if (type === "violet") { raw[i] = w - prevW; prevW = w; } + else if (type === "brown") { brown = (brown + .02 * w) / 1.02; raw[i] = brown; } + else { // pink, blue (= Ableitung von pink) + b0=.99886*b0+w*.0555179; b1=.99332*b1+w*.0750759; b2=.969*b2+w*.153852; + b3=.8665*b3+w*.3104856; b4=.55*b4+w*.5329522; b5=-.7616*b5-w*.016898; + const p = b0+b1+b2+b3+b4+b5+b6+w*.5362; b6 = w*.115926; + if (type === "pink") raw[i] = p; else { raw[i] = p - prevP; prevP = p; } + } + } + let sum = 0; for (let i = 0; i < tot; i++) sum += raw[i] * raw[i]; + const k = .2 / Math.sqrt(sum / tot + 1e-12), d = buf.getChannelData(c); + for (let i = 0; i < len; i++) d[i] = clamp(raw[i] * k, -1, 1); + for (let i = 0; i < fade; i++) { + const th = i / fade * Math.PI / 2; + d[i] = clamp((raw[i] * Math.sin(th) + raw[len + i] * Math.cos(th)) * k, -1, 1); + } + } + return NB[type] = buf; +} + +// Einzelereignisse (Tropfen, Knistern, Blubbern), kreisförmig in 10 s geschrieben -> nahtloser Loop +const DB = {}; +function dropBuf(kind, rate) { + const key = kind + rate; if (DB[key]) return DB[key]; + const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 10, buf = ctx.createBuffer(2, len, sr); + const target = { rain: .16, pop: .09, water: .14 }[kind]; + for (let c = 0; c < 2; c++) { + const d = buf.getChannelData(c), N = Math.round(rate * 10); + if (kind === "water") fillWater(d, len, sr, rate); + else for (let e = 0; e < N; e++) { + const pos = Math.floor(Math.random() * len); + if (kind === "rain") { + const n = 40 + Math.floor(Math.random() * 220), amp = Math.pow(Math.random(), 2.2) * .9, k = .12 + Math.random() * .75; + let lp = 0; + for (let j = 0; j < n; j++) { lp += (Math.random() * 2 - 1 - lp) * k; d[(pos + j) % len] += lp * Math.exp(-j / (n * .25)) * amp; } + } else if (kind === "pop") { + const burst = Math.random() < .25 ? 2 + Math.floor(Math.random() * 5) : 1; + for (let b = 0; b < burst; b++) { + const p = (pos + b * Math.floor(sr * (.004 + Math.random() * .02))) % len; + const n = 10 + Math.floor(Math.random() * 70), amp = Math.pow(Math.random(), 3) * 1.2, k = .35 + Math.random() * .6; + let lp = 0; + for (let j = 0; j < n; j++) { lp += (Math.random() * 2 - 1 - lp) * k; d[(p + j) % len] += lp * Math.exp(-j / (n * .3)) * amp; } + } + } + } + let sum = 0; for (let i = 0; i < len; i++) sum += d[i] * d[i]; + const k = target / Math.sqrt(sum / len + 1e-12); + for (let i = 0; i < len; i++) d[i] = clamp(d[i] * k, -1, 1); + } + return DB[key] = buf; +} + +// Wasser: tausende Luftbläschen als gedämpfte, leicht ansteigende Sinus-Schwingungen nach dem Minnaert-Modell +// (Frequenz ≈ 3 / Radius, Dämpfung wächst mit der Frequenz). Dichte, überlagerte Bläschen ergeben das Plätschern; +// die Ereignisrate schwankt periodisch über die 10 s, damit der Bach "atmet". +let SIN = null; +function fillWater(d, len, sr, rate) { + if (!SIN) { SIN = new Float32Array(4096); for (let i = 0; i < 4096; i++) SIN[i] = Math.sin(i / 4096 * 2 * Math.PI); } + const N = Math.round(rate * 10), p1 = Math.random() * 6.28, p2 = Math.random() * 6.28, p3 = Math.random() * 6.28; + for (let e = 0; e < N; e++) { + let pos; + do { + pos = Math.random(); + var lam = 1 + .45 * Math.sin(2 * Math.PI * 3 * pos + p1) + .3 * Math.sin(2 * Math.PI * 7 * pos + p2) + .2 * Math.sin(2 * Math.PI * 11 * pos + p3); + } while (Math.random() * 1.95 > lam); + const r = (0.7 + 7.3 * Math.pow(Math.random(), 2.2)) * 1e-3; // Radius 0,7–8 mm, kleine häufiger + const f0 = Math.min(4500, 3 / r), dmp = .043 * f0 + .0014 * Math.pow(f0, 1.5), xi = .1 * dmp; + const amp = Math.pow(r / .004, 1.3) * (.3 + Math.random()); + const n = Math.min(Math.floor(sr * 3.5 / dmp), 4000), start = Math.floor(pos * len), dec = Math.exp(-dmp / sr), att = sr * .0008; + let env = 1, phase = 0; + for (let j = 0; j < n; j++) { + phase += f0 * (1 + xi * j / sr) / sr; + d[(start + j) % len] += SIN[((phase - Math.floor(phase)) * 4096) | 0] * env * amp * (j < att ? j / att : 1); + env *= dec; + } + } +} + +// Grillen: je eine Grille pro Buffer (Chirp aus 3–4 Pulsen), Periode teilt die Loop-Länge exakt +const CB = [], CRICKET_BASE = R(4400, 4800); // Chor: alle Tiere nahezu gleiche Tonhöhe (±1 %) +function cricketBuf(i) { + if (CB[i]) return CB[i]; + const ctx = S.ctx, sr = ctx.sampleRate, len = sr * 8, buf = ctx.createBuffer(1, len, sr), d = buf.getChannelData(0); + const fc = CRICKET_BASE * (1 + R(-.012, .012)), nCh = Math.floor(8 / R(.36, .42)), P = len / nCh, pulses = 3 + Math.floor(Math.random() * 2), off = Math.random() * P; + for (let k = 0; k < nCh; k++) for (let p = 0; p < pulses; p++) { + const start = Math.floor(k * P + off + p * sr * .024), n = Math.floor(sr * .016); + for (let j = 0; j < n; j++) d[(start + j) % len] += Math.sin(2 * Math.PI * fc * j / sr) * Math.pow(Math.sin(Math.PI * j / n), 2) * (p === 0 ? .7 : 1); + } + let sum = 0; for (let j = 0; j < len; j++) sum += d[j] * d[j]; + const k = .16 / Math.sqrt(sum / len + 1e-12); + for (let j = 0; j < len; j++) d[j] = clamp(d[j] * k, -1, 1); + return CB[i] = buf; +} + +// Baukasten pro Modus: eigener Ausgang, Quellen, Timer – alles lässt sich sauber ausblenden +function newKit(gain) { + const ctx = S.ctx, K = { out: ctx.createGain(), srcs: [], timers: [], dead: false, a: .5, b: .5, set() {} }; + K.out.gain.value = 0; K.out.gain.setTargetAtTime(gain, T(), 1.4); + K.out.connect(S.g.modeBus); + K.src = (buf, dest, rate = 1) => { + const s = ctx.createBufferSource(); s.buffer = buf; s.loop = true; s.playbackRate.value = rate; + s.connect(dest); s.start(0, Math.random() * buf.duration); K.srcs.push(s); return s; + }; + K.osc = (type, f, dest) => { const o = ctx.createOscillator(); o.type = type; o.frequency.value = f; o.connect(dest); o.start(); K.srcs.push(o); return o; }; + K.every = (fn, min, max, first = .1) => { + const slot = K.timers.length; K.timers.push(0); + const val = x => typeof x === "function" ? x() : x; + const loop = () => { if (K.dead) return; fn(); K.timers[slot] = setTimeout(loop, R(val(min), val(max)) * 1000); }; + K.timers[slot] = setTimeout(loop, first * 1000); + }; + K.apply = (a, b) => { K.a = a; K.b = b; K.set(a, b); }; + K.stop = () => { + K.dead = true; K.timers.forEach(clearTimeout); + K.out.gain.cancelScheduledValues(T()); K.out.gain.setTargetAtTime(0, T(), .7); + setTimeout(() => { K.srcs.forEach(s => { try { s.stop(); } catch (e) {} }); try { K.out.disconnect(); } catch (e) {} }, 4000); + }; + return K; +} + +// ----- Bauteile der einzelnen Modi ----- +function buildColor(K, type) { + const ctx = S.ctx, head = ctx.createGain(); + K.src(noiseBuf(type === "grey" ? "white" : type), head); + let n = head; + if (type === "grey") for (const f of [bq("lowshelf", 200, .7, 12), bq("peaking", 3200, .7, -10), bq("highshelf", 9000, .7, 9)]) { n.connect(f); n = f; } + const lp = bq("lowpass", 20000, .5), am = ctx.createGain(), lfoG = ctx.createGain(); + n.connect(lp); lp.connect(am).connect(K.out); + K.osc("sine", .11, lfoG); lfoG.connect(am.gain); + K.set = (a, b) => { sm(lp.frequency, 500 * Math.pow(40, a), .4); const d = b * .8; sm(am.gain, 1 - d / 2); sm(lfoG.gain, d / 2); }; +} + +function buildRain(K, storm) { + const ctx = S.ctx, gl = ctx.createGain(), gh = ctx.createGain(), lp = bq("lowpass", 9000, .5); + const wash = ctx.createGain(), hp = bq("highpass", 350, .5), wl = bq("lowpass", 7000, .5); + K.src(dropBuf("rain", 60), gl); K.src(dropBuf("rain", 320), gh); gl.connect(lp); gh.connect(lp); + K.src(noiseBuf("pink"), hp); hp.connect(wl).connect(wash).connect(lp); lp.connect(K.out); + K.set = (a, b) => { + sm(gl.gain, 1 - a); sm(gh.gain, a * 1.1); sm(wash.gain, .05 + .25 * a); + sm(lp.frequency, storm ? 7000 : 2500 * Math.pow(5.5, b), .6); + }; + if (storm) K.every(() => thunder(K), () => 9 / (.3 + K.a), () => 32 / (.3 + K.a), 4); +} +function thunder(K) { + const ctx = S.ctx, near = K.b, d = (1 - near) * 5 + .3 + Math.random() * 1.2, t0 = T() + d; + S.flash = .4 + .6 * near; setTimeout(() => { if (!K.dead) S.flash = Math.max(S.flash, .3 + .5 * near); }, 140); + const dur = R(6, 10), lp = bq("lowpass", 100 + 220 * near, .7), g = ctx.createGain(), s = ctx.createBufferSource(); + s.buffer = noiseBuf("brown"); s.connect(lp).connect(g).connect(K.out); + const peak = .9 * (.35 + .65 * near); + lp.frequency.setValueAtTime(100 + 220 * near, t0); lp.frequency.exponentialRampToValueAtTime(60, t0 + dur); + g.gain.setValueAtTime(0, t0); g.gain.linearRampToValueAtTime(peak, t0 + .35); + let tt = t0 + .35, amp = peak; + for (let i = 0; i < 4; i++) { tt += R(.8, 1.8); amp *= R(.45, .85); g.gain.linearRampToValueAtTime(amp * R(.6, 1), tt); } + g.gain.linearRampToValueAtTime(0, t0 + dur); + s.start(t0, R(0, .5), dur + .2); + const cb = bq("bandpass", R(900, 2500), .8), cg = ctx.createGain(), cs = ctx.createBufferSource(); + cs.buffer = noiseBuf("white"); cs.connect(cb).connect(cg).connect(K.out); + cg.gain.setValueAtTime(0, t0); cg.gain.linearRampToValueAtTime(.7 * near * near, t0 + .01); cg.gain.exponentialRampToValueAtTime(.001, t0 + .4); + cs.start(t0, R(0, 3), .5); +} + +function buildOcean(K) { + const ctx = S.ctx, lp = bq("lowpass", 220, .6), bg = ctx.createGain(), hp = bq("highpass", 1800, .5), fl = bq("lowpass", 7500, .5), fg = ctx.createGain(); + K.src(noiseBuf("brown"), lp); lp.connect(bg).connect(K.out); bg.gain.value = .12; + K.src(noiseBuf("white"), hp); hp.connect(fl).connect(fg).connect(K.out); fg.gain.value = 0; + const bed = ctx.createGain(), bl = bq("lowpass", 900, .5); bed.gain.value = .06; K.src(noiseBuf("pink"), bl); bl.connect(bed).connect(K.out); + K.waveEnd = T() + .1; + const sched = () => { + if (K.dead) return; + const a = K.a, b = K.b, Tw = R(7, 12) * (1.25 - .35 * a), A = (.35 + .65 * a) * R(.6, 1), t0 = K.waveEnd, rise = Tw * .42; + bg.gain.linearRampToValueAtTime(.12, t0); bg.gain.linearRampToValueAtTime(A, t0 + rise); bg.gain.linearRampToValueAtTime(.12, t0 + Tw); + lp.frequency.linearRampToValueAtTime(220, t0); lp.frequency.linearRampToValueAtTime(350 + 900 * b + 500 * A, t0 + rise); lp.frequency.linearRampToValueAtTime(220, t0 + Tw); + fg.gain.linearRampToValueAtTime(0, t0 + rise * .6); fg.gain.linearRampToValueAtTime(A * .55 * (.25 + b), t0 + rise * 1.15); fg.gain.linearRampToValueAtTime(0, t0 + Tw * .98); + K.waveEnd = t0 + Tw; + K.timers[0] = setTimeout(sched, Math.max(300, (K.waveEnd - T() - 2.5) * 1000)); + }; + K.timers.push(0); sched(); +} + +function buildWind(K) { + const ctx = S.ctx, bp = bq("bandpass", 500, 1.2), g = ctx.createGain(), wb = bq("bandpass", 900, 22), wg = ctx.createGain(), low = bq("lowpass", 250, .5), lg = ctx.createGain(); + g.gain.value = 0; wg.gain.value = 0; lg.gain.value = .3; + K.src(noiseBuf("pink"), bp); bp.connect(g).connect(K.out); + K.src(noiseBuf("white"), wb); wb.connect(wg).connect(K.out); + K.src(noiseBuf("brown"), low); low.connect(lg).connect(K.out); + K.set = a => sm(lg.gain, .15 + .6 * a, 1); + K.every(() => { + const a = K.a, b = K.b; + sm(bp.frequency, R(250, 1100) * (.6 + .8 * a), 2.5); sm(bp.Q, R(.7, 2.2), 2.5); + sm(g.gain, (.12 + R(0, .8) * a) * .9, 2.2); + sm(wb.frequency, R(600, 1800), 3); sm(wg.gain, b * R(.3, 1) * 10, 2.5); + }, 2.5, 6, .1); +} + +function buildStream(K) { + const ctx = S.ctx, g1 = ctx.createGain(), g2 = ctx.createGain(), hp = bq("highpass", 220, .5); + const s1 = K.src(dropBuf("water", 350), g1), s2 = K.src(dropBuf("water", 1300), g2); + g1.connect(hp); g2.connect(hp); + const fl = bq("bandpass", 1200, .5), fg = ctx.createGain(); K.src(noiseBuf("pink"), fl); fl.connect(fg).connect(hp); // verschmiert die Bläschen zu einem Fluss + const low = bq("lowpass", 220, .5), lg = ctx.createGain(); lg.gain.value = .12; K.src(noiseBuf("brown"), low); low.connect(lg).connect(K.out); + hp.connect(K.out); + K.set = (a, b) => { + sm(g1.gain, 1 - a * .8); sm(g2.gain, a * 1.2); sm(fg.gain, .04 + .12 * a); + const rt = .75 + .65 * b; sm(s1.playbackRate, rt); sm(s2.playbackRate, rt * 1.03); + }; + K.every(() => sm(fl.frequency, R(800, 2400), 2), 2, 5); +} + +function buildFire(K) { + const ctx = S.ctx, rl = bq("lowpass", 450, .6), rg = ctx.createGain(), hp = bq("highpass", 3500, .5), hg = ctx.createGain(); + const g1 = ctx.createGain(), g2 = ctx.createGain(), ph = bq("highpass", 600, .5); + K.src(noiseBuf("brown"), rl); rl.connect(rg).connect(K.out); rg.gain.value = .5; + K.src(noiseBuf("pink"), hp); hp.connect(hg).connect(K.out); hg.gain.value = .03; + K.src(dropBuf("pop", 12), g1); K.src(dropBuf("pop", 45), g2); g1.connect(ph); g2.connect(ph); ph.connect(K.out); + K.set = (a, b) => { sm(g1.gain, (1 - b) * (.6 + .5 * a)); sm(g2.gain, b * (.6 + .5 * a)); }; + K.every(() => sm(rg.gain, (.2 + .6 * K.a) * R(.55, 1), .35), .3, 1.2); +} + +function buildCrickets(K) { + const ctx = S.ctx, cs = []; + for (let i = 0; i < 6; i++) { + const g = ctx.createGain(), pn = ctx.createStereoPanner(); g.gain.value = 0; pn.pan.value = R(-.85, .85); + const src = K.src(cricketBuf(i), g); g.connect(pn).connect(K.out); cs.push({ g, src }); + } + const bed = ctx.createGain(), lp = bq("lowpass", 220, .5); bed.gain.value = .05; K.src(noiseBuf("brown"), lp); lp.connect(bed).connect(K.out); + K.set = (a, b) => { const n = 1 + a * 5; cs.forEach((c, i) => { sm(c.g.gain, clamp(n - i, 0, 1) * .8, 1.2); sm(c.src.playbackRate, .8 + .45 * b, 1); }); }; +} + +function buildFan(K, cabin) { + const ctx = S.ctx, lp = bq("lowpass", 900, .6), g = ctx.createGain(), hp = bq("highpass", 2000, .5), lp2 = bq("lowpass", 6000, .5), hg = ctx.createGain(), hg2 = ctx.createGain(); + g.gain.value = 1; hg.gain.value = cabin ? .035 : .05; hg2.gain.value = 0; + K.src(noiseBuf("brown"), lp); lp.connect(g).connect(K.out); + K.src(noiseBuf("pink"), hp); hp.connect(lp2).connect(hg).connect(K.out); + const o1 = K.osc("sine", cabin ? 83 : 100, hg2), o2 = K.osc("sine", cabin ? 83.8 : 200, hg2), o3 = K.osc("sine", cabin ? 166 : 300, hg2); + hg2.connect(K.out); + K.set = (a, b) => { + if (cabin) { sm(lp.frequency, 400 + 1400 * a); sm(hg2.gain, .02 + .12 * b); } + else { sm(lp.frequency, 500 + 1500 * a); sm(o1.frequency, 70 + 90 * a); sm(o2.frequency, 140 + 180 * a); sm(o3.frequency, 210 + 270 * a); sm(hg2.gain, .01 + .1 * b); } + }; +} + +function buildBinaural(K) { + const ctx = S.ctx, l = ctx.createGain(), r = ctx.createGain(), l2 = ctx.createGain(), r2 = ctx.createGain(); + const pl = ctx.createStereoPanner(), pr = ctx.createStereoPanner(); pl.pan.value = -1; pr.pan.value = 1; + l.gain.value = r.gain.value = .16; l2.gain.value = r2.gain.value = .04; + l.connect(pl); l2.connect(pl); r.connect(pr); r2.connect(pr); pl.connect(K.out); pr.connect(K.out); + const oL = K.osc("sine", 200, l), oR = K.osc("sine", 210, r), oL2 = K.osc("sine", 400, l2), oR2 = K.osc("sine", 410, r2); + const bed = ctx.createGain(), lp = bq("lowpass", 1800, .5); bed.gain.value = .22; K.src(noiseBuf("pink"), lp); lp.connect(bed).connect(K.out); + K.set = (a, b) => { + const beat = Math.pow(40, a), f0 = 100 * Math.pow(4, b); + sm(oL.frequency, f0 - beat / 2, .3); sm(oR.frequency, f0 + beat / 2, .3); + sm(oL2.frequency, 2 * f0 - beat / 2, .3); sm(oR2.frequency, 2 * f0 + beat / 2, .3); + }; +} + +function buildBowls(K) { + const ctx = S.ctx, ratios = [1, 2.71, 5.15, 8.2], amps = [1, .45, .22, .1], decs = [16, 10, 6, 3.5]; + const strike = () => { + const t = T() + .05, midi = pick([48, 50, 52, 55, 57], Math.random) + 12 * Math.round(K.b * 1.5), f = mtof(midi); + const pn = ctx.createStereoPanner(); pn.pan.value = R(-.6, .6); pn.connect(K.out); + ratios.forEach((ra, i) => [-R(.3, 1.1), R(.3, 1.1)].forEach(det => { + const o = ctx.createOscillator(), g = ctx.createGain(), dec = decs[i] * R(.8, 1.2); + o.frequency.value = f * ra + det; + g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(amps[i] * .07, t + .008); g.gain.exponentialRampToValueAtTime(.0001, t + dec); + o.connect(g).connect(pn); o.start(t); o.stop(t + dec + .1); + })); + }; + K.every(strike, () => 4 / (.35 + K.a * 1.4), () => 9 / (.35 + K.a * 1.4), .3); +} + +function buildWomb(K) { + const ctx = S.ctx, lp = bq("lowpass", 500, .5), g = ctx.createGain(), sw = bq("bandpass", 900, .4), swg = ctx.createGain(), tl = bq("lowpass", 160, .7); + g.gain.value = .55; swg.gain.value = .12; + K.src(noiseBuf("brown"), lp); lp.connect(g).connect(K.out); + K.src(noiseBuf("pink"), sw); sw.connect(swg).connect(K.out); tl.connect(K.out); + const thump = (t, f0, amp) => { + const o = ctx.createOscillator(), og = ctx.createGain(); + o.frequency.setValueAtTime(f0, t); o.frequency.exponentialRampToValueAtTime(f0 * .55, t + .14); + og.gain.setValueAtTime(0, t); og.gain.linearRampToValueAtTime(amp, t + .012); og.gain.exponentialRampToValueAtTime(.001, t + .25); + o.connect(og).connect(tl); o.start(t); o.stop(t + .3); + }; + const per = () => 60 / (55 + K.a * 60); + K.every(() => { const t = T() + .05; thump(t, 64, .9); thump(t + .27, 72, .6); }, per, () => per() * 1.01, .3); + K.set = (a, b) => { sm(tl.frequency, 100 + 300 * b); sm(lp.frequency, 300 + 500 * b); }; +} + +// Zugfahrt: Rollgeräusch + Schienenstöße. Jeder Stoß wird von vier Achsen nacheinander angeschlagen +// (zwei pro Drehgestell, Abstand = halbe Schienenlänge) -> gleichmäßig "tschuck-tschuck ... tschuck-tschuck", Takt folgt der Geschwindigkeit. +function railClack(K, t, amp) { + const ctx = S.ctx, nb = noiseBuf("white"); + const s1 = ctx.createBufferSource(), bp = bq("bandpass", R(500, 1100) * (.7 + .8 * K.b), .9), g1 = ctx.createGain(); + s1.buffer = nb; s1.connect(bp).connect(g1).connect(K.out); + g1.gain.setValueAtTime(0, t); g1.gain.linearRampToValueAtTime(amp * .9, t + .003); g1.gain.exponentialRampToValueAtTime(.001, t + .07); + s1.start(t, R(0, 8), .1); + const o = ctx.createOscillator(), og = ctx.createGain(); // dumpfer Schlag im Wagenboden + o.frequency.setValueAtTime(110, t); o.frequency.exponentialRampToValueAtTime(55, t + .1); + og.gain.setValueAtTime(0, t); og.gain.linearRampToValueAtTime(amp * .7, t + .004); og.gain.exponentialRampToValueAtTime(.001, t + .16); + o.connect(og).connect(K.out); o.start(t); o.stop(t + .2); + const s2 = ctx.createBufferSource(), rb = bq("bandpass", R(190, 260), 8), g2 = ctx.createGain(); // Nachklingen der Karosserie + s2.buffer = nb; s2.connect(rb).connect(g2).connect(K.out); + g2.gain.setValueAtTime(0, t); g2.gain.linearRampToValueAtTime(amp * .9, t + .004); g2.gain.exponentialRampToValueAtTime(.001, t + .35); + s2.start(t, R(0, 8), .4); +} +function buildTrain(K) { + const ctx = S.ctx, lp = bq("lowpass", 700, .6), g = ctx.createGain(), bp = bq("bandpass", 900, .5), bg = ctx.createGain(); + const hum = ctx.createGain(), sway = ctx.createGain(), swayG = ctx.createGain(); + g.gain.value = .45; bg.gain.value = .12; hum.gain.value = .02; + K.src(noiseBuf("brown"), lp); lp.connect(g).connect(sway).connect(K.out); + K.src(noiseBuf("pink"), bp); bp.connect(bg).connect(sway); + const ho = K.osc("sine", 120, hum); hum.connect(K.out); + sway.gain.value = .9; K.osc("sine", .23, swayG); swayG.gain.value = .08; swayG.connect(sway.gain); // leichtes Schaukeln + K.v = 25; K.nextJoint = T() + .3; K.nextPoints = T() + R(25, 50); + K.set = (a, b) => { + K.v = 14 + 36 * a; // 50 … 180 km/h + sm(lp.frequency, 350 + 450 * a + 700 * b, 1); sm(bg.gain, .03 + .1 * a * (.4 + b), 1); sm(ho.frequency, 70 + 120 * a, 1); sm(hum.gain, .015 + .03 * a, 1); + }; + K.every(() => { + const now = T(); + while (K.nextJoint < now + .5) { // alle Achsen eines Schienenstoßes vorplanen + const v = K.v, t0 = K.nextJoint, amp = .8 * (.85 + Math.random() * .3); + [0, 2.5 / v, 12.5 / v, 15 / v].forEach((o, i) => railClack(K, t0 + o, amp * (i % 2 ? .8 : 1))); + K.nextJoint += 25 / v; + } + if (now > K.nextPoints) { // Weiche: kurzes schnelles Klapperbündel + const n = 7 + Math.floor(Math.random() * 5); + for (let i = 0; i < n; i++) railClack(K, now + .2 + i * R(.07, .13), R(.5, .9)); + K.nextPoints = now + R(40, 90); + } + }, .12, .12, .05); +} + +// ----- Modus-Tabelle ----- +const bandOf = hz => hz < 4 ? "Delta" : hz < 8 ? "Theta" : hz < 13 ? "Alpha" : hz < 30 ? "Beta" : "Gamma"; +const binInfo = (a, b) => { const hz = Math.pow(40, a); return `${hz.toFixed(1)} Hz · ${bandOf(hz)} · Träger ${Math.round(100 * Math.pow(4, b))} Hz · Kopfhörer!`; }; +const colorDesc = "Regler: Höhen-Dämpfung und langsames Atmen der Lautstärke."; +const MODES = { + ambient: { group: "Musik", name: "Ambient-Musik", music: true, hue: 220 }, + white: { group: "Rauschfarben", name: "Weißes Rauschen", desc: "Gleiche Energie in allen Frequenzen – hell und rauschig, maskiert Geräusche gut. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.3, hue: 215, vis: "float", build: K => buildColor(K, "white") }, + pink: { group: "Rauschfarben", name: "Pinkes Rauschen", desc: "Nimmt pro Oktave um 3 dB ab – ausgewogen, wie Regen und Blätterrauschen. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.42, hue: 330, vis: "float", build: K => buildColor(K, "pink") }, + brown: { group: "Rauschfarben", name: "Braunes Rauschen", desc: "Tief und dumpf (−6 dB pro Oktave), wie Donnergrollen oder ein Wasserfall. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.6, hue: 28, vis: "float", build: K => buildColor(K, "brown") }, + blue: { group: "Rauschfarben", name: "Blaues Rauschen", desc: "Zu den Höhen ansteigend, zischend und klar. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.25, hue: 205, vis: "float", build: K => buildColor(K, "blue") }, + violet: { group: "Rauschfarben", name: "Violettes Rauschen", desc: "Noch stärker zu den Höhen – sehr hell, fast nur Zischen. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.8, 0], gain: 0.25, hue: 275, vis: "float", build: K => buildColor(K, "violet") }, + grey: { group: "Rauschfarben", name: "Graues Rauschen", desc: "Weißes Rauschen, an die Ohrempfindlichkeit angepasst – wirkt über alle Höhen gleich laut. " + colorDesc, labels: ["Höhen-Dämpfung", "Atmen"], def: [.9, 0], gain: 0.2, hue: 190, vis: "float", build: K => buildColor(K, "grey") }, + + rain: { group: "Natur", name: "Regen", desc: "Tausende einzeln synthetisierte Tropfen auf einem Rauschteppich.", labels: ["Stärke", "Dumpf ↔ Hell"], def: [.5, .55], gain: 1.3, hue: 210, vis: "rain", build: K => buildRain(K, false) }, + storm: { group: "Natur", name: "Gewitter", desc: "Starker Regen mit Blitz und rollendem Donner in unregelmäßigen Abständen.", labels: ["Häufigkeit & Regen", "Entfernung (fern ↔ nah)"], def: [.6, .5], gain: .8, hue: 245, vis: "rain", rev: .5, build: K => buildRain(K, true) }, + ocean: { group: "Natur", name: "Meer", desc: "Wellen, die anrollen, brechen und im Schaum auslaufen.", labels: ["Wellengang", "Brandung dumpf ↔ hell"], def: [.5, .5], gain: .9, hue: 195, vis: "float", build: buildOcean }, + wind: { group: "Natur", name: "Wind", desc: "Böen mit wandernder Farbe, auf Wunsch mit feinem Pfeifen.", labels: ["Stärke", "Pfeifen"], def: [.5, .25], gain: .9, hue: 180, vis: "wind", build: buildWind }, + stream: { group: "Natur", name: "Bach", desc: "Plätscherndes Wasser aus tausenden einzeln berechneten Luftbläschen.", labels: ["Strömung", "Tonhöhe"], def: [.5, .5], gain: 0.5, hue: 170, vis: "float", build: buildStream }, + fire: { group: "Natur", name: "Lagerfeuer", desc: "Rollendes Brummen der Flamme und Knistern.", labels: ["Flammenstärke", "Knistern selten ↔ dicht"], def: [.5, .45], gain: .8, hue: 25, vis: "rise", build: buildFire }, + crickets: { group: "Natur", name: "Sommernacht", desc: "Bis zu sechs Grillen, jede mit eigenem Rhythmus und eigener Tonhöhe.", labels: ["Anzahl", "Tonhöhe / Tempo"], def: [.5, .45], gain: 0.2, hue: 75, vis: "fly", build: buildCrickets }, + + fan: { group: "Technik", name: "Ventilator", desc: "Gleichmäßiger Luftstrom mit tiefem Motorbrummen.", labels: ["Drehzahl", "Brummen"], def: [.4, .35], gain: 0.3, hue: 200, vis: "float", build: K => buildFan(K, false) }, + cabin: { group: "Technik", name: "Flugzeugkabine", desc: "Dumpfes Rauschen und tiefes Triebwerksdröhnen – beliebt zum Einschlafen.", labels: ["Dumpf ↔ offen", "Triebwerk"], def: [.3, .5], gain: 0.27, hue: 215, vis: "float", build: K => buildFan(K, true) }, + + train: { group: "Technik", name: "Zugfahrt", desc: "Gleichmäßiges Rollen mit dem Tschuck-tschuck der Schienenstöße, ab und zu rattert eine Weiche.", labels: ["Geschwindigkeit", "Abteil dumpf ↔ Fenster offen"], def: [.4, .35], gain: .45, hue: 35, vis: "wind", info: a => Math.round((14 + 36 * a) * 3.6) + " km/h", build: buildTrain }, + + delta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Delta", desc: "Zwei leicht verstimmte Sinustöne links und rechts, Differenz ≈ 2 Hz (Tiefschlaf).", labels: ["Beat-Frequenz", "Trägerton"], def: [.19, .35], gain: 0.45, hue: 250, vis: "float", info: binInfo, build: buildBinaural }, + theta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Theta", desc: "Differenz ≈ 6 Hz (Meditation, Dämmerzustand).", labels: ["Beat-Frequenz", "Trägerton"], def: [.49, .35], gain: 0.45, hue: 280, vis: "float", info: binInfo, build: buildBinaural }, + alpha: { group: "Fokus & Schlaf", name: "Binaurale Beats: Alpha", desc: "Differenz ≈ 10 Hz (entspannte Wachheit).", labels: ["Beat-Frequenz", "Trägerton"], def: [.62, .35], gain: 0.45, hue: 160, vis: "float", info: binInfo, build: buildBinaural }, + beta: { group: "Fokus & Schlaf", name: "Binaurale Beats: Beta", desc: "Differenz ≈ 18 Hz (Konzentration).", labels: ["Beat-Frequenz", "Trägerton"], def: [.78, .35], gain: 0.45, hue: 45, vis: "float", info: binInfo, build: buildBinaural }, + bowls: { group: "Fokus & Schlaf", name: "Klangschalen", desc: "Angeschlagene Schalen mit schwebenden Obertönen und langem Nachklang.", labels: ["Häufigkeit", "Tonhöhe"], def: [.4, .35], gain: 1.4, hue: 40, rev: .9, vis: "float", build: buildBowls }, + womb: { group: "Fokus & Schlaf", name: "Herzschlag", desc: "Gedämpfter Herzschlag in tiefem Rauschen – wie im Mutterleib.", labels: ["Herzschlag-Tempo", "Dumpf ↔ nah"], def: [.2, .4], gain: 0.25, hue: 5, rev: .15, vis: "float", build: buildWomb }, +}; + +function applyRev() { + if (!S.g) return; + const M = MODES[S.mode]; + S.g.modeRev.gain.setTargetAtTime(M.music ? 0 : (M.rev || 0) * S.verb * 1.2, S.ctx.currentTime, .3); +} +function startSoundscape() { + const M = MODES[S.mode], K = newKit(M.gain || .7); + S.modeObj = K; M.build(K); K.apply(S.mp.a, S.mp.b); applyRev(); +} +function tickMode() { // „Wandel": die beiden Regler wandern langsam von selbst + const K = S.modeObj; if (!K) return; + const t = S.ctx.currentTime, amp = S.drift * .3; + const w1 = Math.sin(t * .031 + 1) * .5 + Math.sin(t * .083 + 4) * .5, w2 = Math.sin(t * .027 + 2) * .5 + Math.sin(t * .067 + 5) * .5; + K.ea = clamp(S.mp.a + amp * w1, 0, 1); K.eb = clamp(S.mp.b + amp * w2, 0, 1); + K.apply(K.ea, K.eb); updateInfo(); +} +function leaveMusic() { + S.timers.forEach(clearTimeout); S.timers = []; + const G = S.g, t = S.ctx.currentTime; + G.voiceBus.gain.setTargetAtTime(0, t, .7); G.grainBus.gain.setTargetAtTime(0, t, .7); + G.windGain.gain.setTargetAtTime(0, t, .7); G.seaGain.gain.setTargetAtTime(0, t, .7); +} +function enterMode() { + if (MODES[S.mode].music) { + const G = S.g, t = S.ctx.currentTime; + G.voiceBus.gain.setTargetAtTime(1, t, .3); G.grainBus.gain.setTargetAtTime(1, t, .3); + startMusic(); + } else startSoundscape(); + applyRev(); +} + // Kommunikation mit der einbettenden Seite (diora): Radio/Podcast ↔ Ambient function post(state) { try { if (window.parent !== window) window.parent.postMessage({ type: "ambient", state }, "*"); } catch (e) {} @@ -593,17 +1023,11 @@ addEventListener("message", e => { if (!d || d.type !== "ambient") return; if (d.cmd === "start" && !S.running) start(); else if (d.cmd === "stop" && S.running) stop(); + else if (d.cmd === "mode" && MODES[d.mode]) setMode(d.mode); }); -function start() { - if (!S.ctx) buildGraph(); - S.running = true; S.timers = []; - // Ohne Nutzergeste (z. B. Befehl von außen) kann der Browser den Ton blockieren -> dann ehrlich "stopped" melden - S.ctx.resume().then(() => { if (S.running && S.ctx.state !== "running") stop(); }, () => { if (S.running) stop(); }); +function startMusic() { resetScene(); - const t = S.ctx.currentTime; - S.g.master.gain.cancelScheduledValues(t); - S.g.master.gain.setTargetAtTime(+$("vol").value, t, 2.5); drone(); playChord(); later(playChord, 10, 18, 1); later(playBells, 2, 6, 2); @@ -614,13 +1038,26 @@ function start() { later(grainCloud, 3, 8, 7); later(snapGrains, 20, 30, 8, .3); startLoops(); - S.timers.push(setInterval(tick, 1000)); +} + +function start() { + if (!S.ctx) buildGraph(); + S.running = true; S.timers = []; + // Ohne Nutzergeste (z. B. Befehl von außen) kann der Browser den Ton blockieren -> dann ehrlich "stopped" melden + S.ctx.resume().then(() => { if (S.running && S.ctx.state !== "running") stop(); }, () => { if (S.running) stop(); }); + const t = S.ctx.currentTime; + S.g.master.gain.cancelScheduledValues(t); + S.g.master.gain.setTargetAtTime(+$("vol").value, t, 2.5); + S.tickId = setInterval(tick, 1000); + enterMode(); $("toggle").textContent = "Stopp"; wake(); post("playing"); } function stop() { S.running = false; S.timers.forEach(t => { clearTimeout(t); clearInterval(t); }); S.timers = []; + clearInterval(S.tickId); + if (S.modeObj) { S.modeObj.stop(); S.modeObj = null; } const t = S.ctx.currentTime; S.g.master.gain.cancelScheduledValues(t); S.g.master.gain.setTargetAtTime(0, t, 1.5); @@ -631,12 +1068,12 @@ function stop() { } function reroll() { // neue Szene, laufende Stimmen klingen aus resetScene(); - if (S.running) { drone(); playChord(); } + if (S.running && MODES[S.mode].music) { drone(); playChord(); } } // ---------- Seed / Link ---------- function writeHash() { - try { history.replaceState(null, "", "#seed=" + encodeURIComponent(S.seedText) + "&p=" + encodeURIComponent(S.presetName)); } catch (e) {} + try { history.replaceState(null, "", "#seed=" + encodeURIComponent(S.seedText) + "&p=" + encodeURIComponent(S.presetName) + "&m=" + encodeURIComponent(S.mode)); } catch (e) {} } function applySeed(text) { S.seedText = text.trim() || String(Math.floor(Math.random() * 900000) + 100000); @@ -646,7 +1083,7 @@ function applySeed(text) { function applyPreset(name) { if (!PRESETS[name]) name = "Frei"; S.presetName = name; S.P = PRESETS[name]; $("preset").value = name; writeHash(); - if (S.running) { wanderTimbre(); changeKey(false); } + if (S.running && MODES[S.mode].music) { wanderTimbre(); changeKey(false); } } // ---------- UI ---------- @@ -664,6 +1101,14 @@ function arcLabel() { return p < .15 ? "Ruhe" : p < .4 ? "Aufbau" : p < .6 ? "Höhepunkt" : p < .85 ? "Rückzug" : "Ruhe"; } function updateInfo() { + const M = MODES[S.mode]; + if (!M.music) { + const K = S.modeObj; + $("key").textContent = M.name; + $("chord").innerHTML = M.desc + (M.info ? ` ${M.info(K && K.ea != null ? K.ea : S.mp.a, K && K.eb != null ? K.eb : S.mp.b)}` : ""); + return; + } + if (!S.running) { $("key").textContent = "–"; $("chord").textContent = "Noch still."; return; } $("key").textContent = `${NOTE[S.root % 12]} ${S.scaleName}`; $("chord").innerHTML = `Stufe ${ROMAN[S.degree] || S.degree + 1} · ${S.presetName} · ${S.tuning === "just" ? "rein" : "gleichstufig"} · Bogen: ${arcLabel()}`; } @@ -674,7 +1119,7 @@ $("vol").oninput = e => { if (S.running) S.g.master.gain.setTargetAtTime(+e.targ $("density").oninput = e => S.density = +e.target.value; $("drift").oninput = e => S.drift = +e.target.value; $("arc").oninput = e => S.arcDepth = +e.target.value; -$("verb").oninput = e => { S.verb = +e.target.value; if (S.g) S.g.send.gain.setTargetAtTime(S.verb * 1.2, S.ctx.currentTime, .2); }; +$("verb").oninput = e => { S.verb = +e.target.value; if (S.g) { S.g.send.gain.setTargetAtTime(S.verb * 1.2, S.ctx.currentTime, .2); applyRev(); } }; $("tuning").onchange = e => { S.tuning = e.target.value; updateInfo(); }; $("preset").onchange = e => applyPreset(e.target.value); $("seed").onchange = e => applySeed(e.target.value); @@ -687,16 +1132,46 @@ $("share").onclick = async () => { }; S.density = +$("density").value; S.drift = +$("drift").value; S.verb = +$("verb").value; S.arcDepth = +$("arc").value; -// Startwerte aus dem Link (#seed=…&p=…) +// Modus-Auswahl +$("mode").innerHTML = (() => { + const groups = {}; + for (const [k, m] of Object.entries(MODES)) (groups[m.group] = groups[m.group] || []).push(``); + return Object.entries(groups).map(([g, o]) => `${o.join("")}`).join(""); +})(); +function applyModeUI() { + const M = MODES[S.mode], root = document.documentElement; + root.classList.toggle("noise", !M.music); root.classList.toggle("rev", !!M.rev); + if (!M.music) { $("lla").textContent = M.labels[0]; $("llb").textContent = M.labels[1]; $("la").value = S.mp.a; $("lb").value = S.mp.b; } + $("driftL").textContent = M.music ? "Wandel-Tempo" : "Schwankung"; + applyRev(); +} +function setMode(name) { + if (!MODES[name]) name = "ambient"; + const was = MODES[S.mode], M = MODES[name]; + S.mode = name; $("mode").value = name; + if (!M.music) S.mp = { a: M.def[0], b: M.def[1] }; + applyModeUI(); writeHash(); + if (S.running) { + if (was.music) leaveMusic(); + if (S.modeObj) { S.modeObj.stop(); S.modeObj = null; } + enterMode(); + } + updateInfo(); wake(); +} +$("mode").onchange = e => setMode(e.target.value); +$("la").oninput = e => { S.mp.a = +e.target.value; if (S.modeObj) S.modeObj.apply(S.mp.a, S.mp.b); }; +$("lb").oninput = e => { S.mp.b = +e.target.value; if (S.modeObj) S.modeObj.apply(S.mp.a, S.mp.b); }; + +// Startwerte aus dem Link (#seed=…&p=…&m=…) { const q = new URLSearchParams(location.hash.slice(1)); applyPreset(q.get("p") || "Frei"); applySeed(q.get("seed") || ""); + setMode(q.get("m") || "ambient"); } // ---------- Visualisierung ---------- const cv = $("viz"), cx = cv.getContext("2d"); -let rafId = 0; S.visUntil = performance.now() + 1500; function wake() { if (!rafId) rafId = requestAnimationFrame(draw); } function resize() { @@ -723,6 +1198,29 @@ function spawnParticles(ev) { }); } } +// Partikel der Klangwelten: Regen, Funken, Windschlieren, Glühwürmchen, sonst treibende Lichter +function modeVis(W, H, dpr) { + const M = MODES[S.mode], K = S.modeObj; + if (M.music || !S.running || !K) return; + const a = K.a, add = p => { if (parts.length < 450) parts.push(Object.assign({ life: 0, vx: 0, vy: 0, h: M.hue }, p)); }; + const n = x => Math.random() < x, r = (lo, hi) => lo + Math.random() * (hi - lo); + switch (M.vis) { + case "rain": + for (let i = 0; i < 1 + Math.floor(a * 5); i++) add({ x: Math.random() * W, y: -8 * dpr, vx: -1.2, vy: r(7, 11), r: r(1.6, 3) * dpr, max: r(40, 80) }); + break; + case "rise": + if (n(.5 + a * .4)) add({ x: W * r(.35, .65), y: H * .9, vx: r(-.5, .5), vy: -r(.8, 2.4), r: r(3, 9) * dpr, max: r(70, 160), h: M.hue + r(-10, 25) }); + break; + case "wind": + if (n(.4 + a * .5)) add({ x: -10 * dpr, y: Math.random() * H, vx: r(4, 9) * (.5 + a), vy: r(-.4, .4), r: r(2, 5) * dpr, max: r(60, 140) }); + break; + case "fly": + if (n(.05)) add({ x: Math.random() * W, y: H * r(.3, .95), vx: r(-.3, .3), vy: r(-.3, .3), r: r(5, 10) * dpr, max: r(200, 420) }); + break; + default: + 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) }); + } +} function draw(ts) { rafId = 0; const W = cv.width, H = cv.height, dpr = devicePixelRatio; @@ -736,7 +1234,8 @@ function draw(ts) { const bins = new Uint8Array(S.g.analyser.frequencyBinCount); S.g.analyser.getByteFrequencyData(bins); let lvl = 0; for (let i = 0; i < 80; i++) lvl += bins[i]; lvl = lvl / 80 / 255; - const target = (S.root % 12) * 30 + 200 + S.cur.bright * 40; + const M0 = MODES[S.mode]; + const target = M0.music ? (S.root % 12) * 30 + 200 + S.cur.bright * 40 : M0.hue; hueV += (((target - hueV + 540) % 360) - 180) * .01; cx.globalCompositeOperation = "lighter"; @@ -748,6 +1247,7 @@ function draw(ts) { gr.addColorStop(0, `hsla(${hueV + i * 28},70%,50%,${.04 + lvl * .16})`); gr.addColorStop(1, "hsla(0,0%,0%,0)"); cx.fillStyle = gr; cx.fillRect(0, 0, W, H); } + modeVis(W, H, dpr); // Notenereignisse -> Partikel const now = S.ctx.currentTime; S.vis = S.vis.filter(ev => { if (ev.t <= now) { if (now - ev.t < 2) spawnParticles(ev); return false; } return ev.t - now < 120; }); @@ -759,6 +1259,7 @@ function draw(ts) { 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 cx.lineWidth = 1.6 * dpr; const N = 90; for (let layer = 0; layer < 2; layer++) { diff --git a/static/js/sw.js b/static/js/sw.js index 611a144..b341106 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-v74'; +const CACHE = 'diora-v75'; const SHELL = [ '/static/css/app.css', '/static/js/app.js',