Ambient: Drift v3 – 23 Klangwelten (Rauschfarben, Natur, Technik, Fokus & Schlaf); sw v75
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:
parent
794dbadbf5
commit
0fbb948c1a
2 changed files with 527 additions and 26 deletions
|
|
@ -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++) {
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue