diff --git a/static/ambient/index.html b/static/ambient/index.html index 555c36a..2d22510 100644 --- a/static/ambient/index.html +++ b/static/ambient/index.html @@ -20,7 +20,7 @@ .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; } + main > * { pointer-events:auto; width:100%; max-width:900px; margin-inline:auto; } /* auf breiten Fenstern zentriert statt über die ganze Breite */ 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; } .state { font-size:13px; color:var(--dim); font-variant-numeric:tabular-nums; } @@ -670,28 +670,31 @@ function dropBuf(kind, rate) { 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". +// Wasser: Luftbläschen als gedämpfte, ansteigende Sinus-Schwingungen (Minnaert-Modell: Frequenz ≈ 3 / Radius, +// Dämpfung wächst mit der Frequenz). Ein Bach "gluckst" in Gruppen: ähnlich große Bläschen platzen kurz hintereinander, +// jede Gruppe hat ihre eigene Lautstärke, und die Gruppenrate schwankt periodisch über die 10 s. let SIN = null; -function fillWater(d, len, sr, rate) { +function fillWater(d, len, sr, rate) { // rate = Gruppen pro Sekunde 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++) { + const NC = Math.round(rate * 10), p1 = Math.random() * 6.28, p2 = Math.random() * 6.28, p3 = Math.random() * 6.28; + for (let c = 0; c < NC; c++) { 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; + const rc = (1 + 4 * Math.pow(Math.random(), 1.5)) * 1e-3; // typischer Radius der Gruppe: 1–5 mm + const loud = Math.pow(Math.random(), 2) * 1.2 + .05, m = 2 + Math.floor(Math.random() * 7), span = (.04 + Math.random() * .16) * sr; + for (let b = 0; b < m; b++) { + const r = rc * (1 + (Math.random() - .5) * .3), f0 = 3 / r, dmp = .043 * f0 + .0014 * Math.pow(f0, 1.5), xi = .3 * dmp; // kräftiger Anstieg = Glucksen + const amp = loud * Math.pow(r / .003, 1.2) * (.5 + Math.random() * .5); + const n = Math.min(Math.floor(sr * 3.5 / dmp), 5000), start = Math.floor(pos * len + Math.random() * span), 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; + } } } } @@ -812,17 +815,20 @@ function buildWind(K) { } 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); + const ctx = S.ctx, g1 = ctx.createGain(), g2 = ctx.createGain(), hp = bq("highpass", 250, .5); + const s1 = K.src(dropBuf("water", 12), g1), s2 = K.src(dropBuf("water", 45), g2); // ruhig / sprudelnd 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); + const sp = bq("highpass", 1800, .5), spl = bq("lowpass", 5000, .5), spg = ctx.createGain(); // Spritzer + K.src(dropBuf("rain", 30), sp); sp.connect(spl).connect(spg).connect(K.out); + const fl = bq("bandpass", 700, .7), fg = ctx.createGain(); // ganz leises, unregelmäßiges Rauschbett + fg.gain.value = .03; K.src(noiseBuf("pink"), fl); fl.connect(fg).connect(hp); + const low = bq("lowpass", 180, .5), lg = ctx.createGain(); lg.gain.value = .05; 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); + sm(g1.gain, 1 - a * .7); sm(g2.gain, a * 1.3); sm(spg.gain, .02 + .07 * a); + const rt = .8 + .5 * b; sm(s1.playbackRate, rt); sm(s2.playbackRate, rt * 1.03); }; - K.every(() => sm(fl.frequency, R(800, 2400), 2), 2, 5); + K.every(() => { sm(fl.frequency, R(500, 1800), .4); sm(fg.gain, R(.01, .07) * (.4 + K.a), .2); }, .4, 1.2); } function buildFire(K) { @@ -965,7 +971,7 @@ const MODES = { 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 }, + stream: { group: "Natur", name: "Bach", desc: "Gluckerndes, plätscherndes Wasser aus Gruppen einzeln berechneter 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 }, diff --git a/static/js/sw.js b/static/js/sw.js index b341106..3542001 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-v75'; +const CACHE = 'diora-v76'; const SHELL = [ '/static/css/app.css', '/static/js/app.js',