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',