Ambient: Drift v3 – 23 Klangwelten (Rauschfarben, Natur, Technik, Fokus & Schlaf); sw v75
Some checks failed
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Has been cancelled

Aus der Session "Ambient-Synthesizer-Webapp": Ambient-Musik ist jetzt ein Modus neben
weißem/pinkem/braunem/blauem/violettem/grauem Rauschen, Regen, Gewitter, Meer, Wind, Bach,
Lagerfeuer, Sommernacht, Ventilator, Flugzeugkabine, Zugfahrt, binauralen Beats,
Klangschalen und Herzschlag. Alles live synthetisiert, offline; Schnittstelle zu diora
unverändert (neu: cmd 'mode').

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrwnslz 2026-10-03 23:37:09 +02:00
parent 794dbadbf5
commit 0fbb948c1a
2 changed files with 527 additions and 26 deletions

View file

@ -16,6 +16,10 @@
overflow-x:hidden; overflow-y:auto; } overflow-x:hidden; overflow-y:auto; }
.embed header { display:none; } .embed header { display:none; }
.embed #share { 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; } main > * { pointer-events:auto; }
h1 { margin:0; font-weight:300; letter-spacing:.35em; text-transform:uppercase; font-size:18px; } 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; } .sub { color:var(--dim); font-size:13px; margin-top:4px; }
@ -68,20 +72,24 @@
<div class="panel"> <div class="panel">
<button class="primary" id="toggle">Start</button> <button class="primary" id="toggle">Start</button>
<label>Stimmung <label>Modus
<select id="mode"></select></label>
<label class="m-music">Stimmung
<select id="preset"></select></label> <select id="preset"></select></label>
<label>Seed <label class="m-music">Seed
<input type="text" id="seed" spellcheck="false"></label> <input type="text" id="seed" spellcheck="false"></label>
<button id="reroll" title="Neuer Seed, neue Szene">Neu würfeln</button> <button id="reroll" class="m-music" title="Neuer Seed, neue Szene">Neu würfeln</button>
<button id="share" title="Link mit Seed und Stimmung kopieren">Link kopieren</button> <button id="share" class="m-music" title="Link mit Seed und Stimmung kopieren">Link kopieren</button>
<label>Tuning <label class="m-music">Tuning
<select id="tuning"><option value="just">Rein</option><option value="equal">Gleichstufig</option></select></label> <select id="tuning"><option value="just">Rein</option><option value="equal">Gleichstufig</option></select></label>
<div class="sep"></div> <div class="sep"></div>
<label>Lautstärke <input type="range" id="vol" min="0" max="1" step="0.01" value="0.7"></label> <label>Lautstärke <input type="range" id="vol" min="0" max="1" step="0.01" value="0.7"></label>
<label>Dichte <input type="range" id="density" min="0" max="1" step="0.01" value="0.5"></label> <label class="m-noise"><span id="lla">A</span> <input type="range" id="la" min="0" max="1" step="0.01" value="0.5"></label>
<label>Wandel-Tempo <input type="range" id="drift" min="0" max="1" step="0.01" value="0.5"></label> <label class="m-noise"><span id="llb">B</span> <input type="range" id="lb" min="0" max="1" step="0.01" value="0.5"></label>
<label>Hall <input type="range" id="verb" min="0" max="1" step="0.01" value="0.6"></label> <label class="m-music">Dichte <input type="range" id="density" min="0" max="1" step="0.01" value="0.5"></label>
<label>Spannungsbogen <input type="range" id="arc" min="0" max="1" step="0.01" value="0.6"></label> <label><span id="driftL">Wandel-Tempo</span> <input type="range" id="drift" min="0" max="1" step="0.01" value="0.5"></label>
<label class="m-hall">Hall <input type="range" id="verb" min="0" max="1" step="0.01" value="0.6"></label>
<label class="m-music">Spannungsbogen <input type="range" id="arc" min="0" max="1" step="0.01" value="0.6"></label>
</div> </div>
</main> </main>
@ -165,6 +173,7 @@ const S = {
timers: [], waves: [], loop: { deg:[], oct:[] }, vis: [], timers: [], waves: [], loop: { deg:[], oct:[] }, vis: [],
ring: null, wp: 0, written: 0, grainBuf: null, ring: null, wp: 0, written: 0, grainBuf: null,
}; };
let rafId = 0; // Zeichenschleife (siehe wake/draw)
const speed = () => 0.35 + S.drift * 2.2; const speed = () => 0.35 + S.drift * 2.2;
// Spannungsbogen: 0 = ruhig, 1 = Höhepunkt, über mehrere Minuten // 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.voiceBus.connect(proc); proc.connect(zero).connect(ctx.destination);
g.grainBus = ctx.createGain(); g.grainBus.connect(g.filter); 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.analyser = ctx.createAnalyser(); g.analyser.fftSize = 2048; g.analyser.smoothingTimeConstant = .88;
g.master.connect(g.analyser); g.master.connect(ctx.destination); g.master.connect(g.analyser); g.master.connect(ctx.destination);
} }
@ -562,6 +574,7 @@ function later(fn, minS, maxS, salt, first = .3) {
function tick() { function tick() {
if (!S.running) return; if (!S.running) return;
if (!MODES[S.mode].music) { tickMode(); return; }
const e = eff(), k = .024 * speed(), t = S.ctx.currentTime, G = S.g; 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; 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); G.filter.frequency.setTargetAtTime(500 + S.cur.bright * 4500 * (.6 + .6 * e), t, 2);
@ -583,6 +596,423 @@ function resetScene() {
changeKey(true); setupLoops(); 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 // Kommunikation mit der einbettenden Seite (diora): Radio/Podcast ↔ Ambient
function post(state) { function post(state) {
try { if (window.parent !== window) window.parent.postMessage({ type: "ambient", state }, "*"); } catch (e) {} 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 || d.type !== "ambient") return;
if (d.cmd === "start" && !S.running) start(); if (d.cmd === "start" && !S.running) start();
else if (d.cmd === "stop" && S.running) stop(); else if (d.cmd === "stop" && S.running) stop();
else if (d.cmd === "mode" && MODES[d.mode]) setMode(d.mode);
}); });
function start() { function startMusic() {
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(); });
resetScene(); resetScene();
const t = S.ctx.currentTime;
S.g.master.gain.cancelScheduledValues(t);
S.g.master.gain.setTargetAtTime(+$("vol").value, t, 2.5);
drone(); playChord(); drone(); playChord();
later(playChord, 10, 18, 1); later(playChord, 10, 18, 1);
later(playBells, 2, 6, 2); later(playBells, 2, 6, 2);
@ -614,13 +1038,26 @@ function start() {
later(grainCloud, 3, 8, 7); later(grainCloud, 3, 8, 7);
later(snapGrains, 20, 30, 8, .3); later(snapGrains, 20, 30, 8, .3);
startLoops(); 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"; $("toggle").textContent = "Stopp";
wake(); post("playing"); wake(); post("playing");
} }
function stop() { function stop() {
S.running = false; S.running = false;
S.timers.forEach(t => { clearTimeout(t); clearInterval(t); }); S.timers = []; 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; const t = S.ctx.currentTime;
S.g.master.gain.cancelScheduledValues(t); S.g.master.gain.cancelScheduledValues(t);
S.g.master.gain.setTargetAtTime(0, t, 1.5); S.g.master.gain.setTargetAtTime(0, t, 1.5);
@ -631,12 +1068,12 @@ function stop() {
} }
function reroll() { // neue Szene, laufende Stimmen klingen aus function reroll() { // neue Szene, laufende Stimmen klingen aus
resetScene(); resetScene();
if (S.running) { drone(); playChord(); } if (S.running && MODES[S.mode].music) { drone(); playChord(); }
} }
// ---------- Seed / Link ---------- // ---------- Seed / Link ----------
function writeHash() { 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) { function applySeed(text) {
S.seedText = text.trim() || String(Math.floor(Math.random() * 900000) + 100000); S.seedText = text.trim() || String(Math.floor(Math.random() * 900000) + 100000);
@ -646,7 +1083,7 @@ function applySeed(text) {
function applyPreset(name) { function applyPreset(name) {
if (!PRESETS[name]) name = "Frei"; if (!PRESETS[name]) name = "Frei";
S.presetName = name; S.P = PRESETS[name]; $("preset").value = name; writeHash(); 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 ---------- // ---------- UI ----------
@ -664,6 +1101,14 @@ function arcLabel() {
return p < .15 ? "Ruhe" : p < .4 ? "Aufbau" : p < .6 ? "Höhepunkt" : p < .85 ? "Rückzug" : "Ruhe"; return p < .15 ? "Ruhe" : p < .4 ? "Aufbau" : p < .6 ? "Höhepunkt" : p < .85 ? "Rückzug" : "Ruhe";
} }
function updateInfo() { function updateInfo() {
const M = MODES[S.mode];
if (!M.music) {
const K = S.modeObj;
$("key").textContent = M.name;
$("chord").innerHTML = M.desc + (M.info ? ` <b>${M.info(K && K.ea != null ? K.ea : S.mp.a, K && K.eb != null ? K.eb : S.mp.b)}</b>` : "");
return;
}
if (!S.running) { $("key").textContent = "–"; $("chord").textContent = "Noch still."; return; }
$("key").textContent = `${NOTE[S.root % 12]} ${S.scaleName}`; $("key").textContent = `${NOTE[S.root % 12]} ${S.scaleName}`;
$("chord").innerHTML = `Stufe <b>${ROMAN[S.degree] || S.degree + 1}</b> · ${S.presetName} · ${S.tuning === "just" ? "rein" : "gleichstufig"} · Bogen: <b>${arcLabel()}</b>`; $("chord").innerHTML = `Stufe <b>${ROMAN[S.degree] || S.degree + 1}</b> · ${S.presetName} · ${S.tuning === "just" ? "rein" : "gleichstufig"} · Bogen: <b>${arcLabel()}</b>`;
} }
@ -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; $("density").oninput = e => S.density = +e.target.value;
$("drift").oninput = e => S.drift = +e.target.value; $("drift").oninput = e => S.drift = +e.target.value;
$("arc").oninput = e => S.arcDepth = +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(); }; $("tuning").onchange = e => { S.tuning = e.target.value; updateInfo(); };
$("preset").onchange = e => applyPreset(e.target.value); $("preset").onchange = e => applyPreset(e.target.value);
$("seed").onchange = e => applySeed(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; 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(`<option value="${k}">${m.name}</option>`);
return Object.entries(groups).map(([g, o]) => `<optgroup label="${g}">${o.join("")}</optgroup>`).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)); const q = new URLSearchParams(location.hash.slice(1));
applyPreset(q.get("p") || "Frei"); applyPreset(q.get("p") || "Frei");
applySeed(q.get("seed") || ""); applySeed(q.get("seed") || "");
setMode(q.get("m") || "ambient");
} }
// ---------- Visualisierung ---------- // ---------- Visualisierung ----------
const cv = $("viz"), cx = cv.getContext("2d"); const cv = $("viz"), cx = cv.getContext("2d");
let rafId = 0;
S.visUntil = performance.now() + 1500; S.visUntil = performance.now() + 1500;
function wake() { if (!rafId) rafId = requestAnimationFrame(draw); } function wake() { if (!rafId) rafId = requestAnimationFrame(draw); }
function resize() { 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) { function draw(ts) {
rafId = 0; rafId = 0;
const W = cv.width, H = cv.height, dpr = devicePixelRatio; const W = cv.width, H = cv.height, dpr = devicePixelRatio;
@ -736,7 +1234,8 @@ function draw(ts) {
const bins = new Uint8Array(S.g.analyser.frequencyBinCount); const bins = new Uint8Array(S.g.analyser.frequencyBinCount);
S.g.analyser.getByteFrequencyData(bins); S.g.analyser.getByteFrequencyData(bins);
let lvl = 0; for (let i = 0; i < 80; i++) lvl += bins[i]; lvl = lvl / 80 / 255; 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; hueV += (((target - hueV + 540) % 360) - 180) * .01;
cx.globalCompositeOperation = "lighter"; 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)"); 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); cx.fillStyle = gr; cx.fillRect(0, 0, W, H);
} }
modeVis(W, H, dpr);
// Notenereignisse -> Partikel // Notenereignisse -> Partikel
const now = S.ctx.currentTime; 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; }); 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)"); 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(); 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 // Spektrumlinie
cx.lineWidth = 1.6 * dpr; const N = 90; cx.lineWidth = 1.6 * dpr; const N = 90;
for (let layer = 0; layer < 2; layer++) { for (let layer = 0; layer < 2; layer++) {

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