109 lines
3.3 KiB
JavaScript
109 lines
3.3 KiB
JavaScript
|
|
// Легкий звук без файлів: усе синтезується WebAudio.
|
|||
|
|
// Дощ у польового, гудіння термінала в аналітика, кліки, штамп, фінальний стінг.
|
|||
|
|
"use strict";
|
|||
|
|
window.Snd = (() => {
|
|||
|
|
let ctx = null,
|
|||
|
|
master = null,
|
|||
|
|
started = false,
|
|||
|
|
muted = localStorage.getItem("ch3snd") === "off";
|
|||
|
|
|
|||
|
|
function ensure() {
|
|||
|
|
if (ctx) return;
|
|||
|
|
ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|||
|
|
master = ctx.createGain();
|
|||
|
|
master.gain.value = muted ? 0 : 1;
|
|||
|
|
master.connect(ctx.destination);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function noiseBuf(secs) {
|
|||
|
|
const b = ctx.createBuffer(1, ctx.sampleRate * secs, ctx.sampleRate);
|
|||
|
|
const d = b.getChannelData(0);
|
|||
|
|
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
|
|||
|
|
return b;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// фоновий шар: дощ (поле) або гудіння ЕПТ (аналітик)
|
|||
|
|
function start(role) {
|
|||
|
|
ensure();
|
|||
|
|
if (started) return;
|
|||
|
|
started = true;
|
|||
|
|
const src = ctx.createBufferSource();
|
|||
|
|
src.buffer = noiseBuf(3);
|
|||
|
|
src.loop = true;
|
|||
|
|
const filt = ctx.createBiquadFilter();
|
|||
|
|
const amb = ctx.createGain();
|
|||
|
|
if (role === "field") {
|
|||
|
|
filt.type = "bandpass";
|
|||
|
|
filt.frequency.value = 1100;
|
|||
|
|
filt.Q.value = 0.4;
|
|||
|
|
amb.gain.value = 0.045;
|
|||
|
|
// пориви дощу
|
|||
|
|
const lfo = ctx.createOscillator(), lg = ctx.createGain();
|
|||
|
|
lfo.frequency.value = 0.11;
|
|||
|
|
lg.gain.value = 0.018;
|
|||
|
|
lfo.connect(lg).connect(amb.gain);
|
|||
|
|
lfo.start();
|
|||
|
|
} else {
|
|||
|
|
filt.type = "lowpass";
|
|||
|
|
filt.frequency.value = 130;
|
|||
|
|
amb.gain.value = 0.02;
|
|||
|
|
const hum = ctx.createOscillator(), hg = ctx.createGain();
|
|||
|
|
hum.frequency.value = 50;
|
|||
|
|
hg.gain.value = 0.006;
|
|||
|
|
hum.connect(hg).connect(master);
|
|||
|
|
hum.start();
|
|||
|
|
}
|
|||
|
|
src.connect(filt).connect(amb).connect(master);
|
|||
|
|
src.start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function blip(freq, dur, vol, type) {
|
|||
|
|
if (!ctx) return;
|
|||
|
|
const o = ctx.createOscillator(), gn = ctx.createGain();
|
|||
|
|
o.type = type || "square";
|
|||
|
|
o.frequency.value = freq;
|
|||
|
|
gn.gain.setValueAtTime(vol, ctx.currentTime);
|
|||
|
|
gn.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + dur);
|
|||
|
|
o.connect(gn).connect(master);
|
|||
|
|
o.start();
|
|||
|
|
o.stop(ctx.currentTime + dur);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const click = () => blip(1300, 0.03, 0.05);
|
|||
|
|
function stamp() {
|
|||
|
|
if (!ctx) return;
|
|||
|
|
blip(85, 0.16, 0.22, "sine");
|
|||
|
|
const n = ctx.createBufferSource();
|
|||
|
|
n.buffer = noiseBuf(0.08);
|
|||
|
|
const gn = ctx.createGain();
|
|||
|
|
gn.gain.setValueAtTime(0.12, ctx.currentTime);
|
|||
|
|
gn.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.09);
|
|||
|
|
n.connect(gn).connect(master);
|
|||
|
|
n.start();
|
|||
|
|
}
|
|||
|
|
function sting(solved) {
|
|||
|
|
if (!ctx) return;
|
|||
|
|
const seq = solved ? [220, 277, 330, 440] : [196, 185, 147, 98];
|
|||
|
|
seq.forEach((f, i) =>
|
|||
|
|
setTimeout(() => blip(f, 0.5, 0.09, "triangle"), i * 190),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
function toggle() {
|
|||
|
|
muted = !muted;
|
|||
|
|
localStorage.setItem("ch3snd", muted ? "off" : "on");
|
|||
|
|
if (master) master.gain.value = muted ? 0 : 1;
|
|||
|
|
return muted;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// клік на будь-якій кнопці — тихий тик
|
|||
|
|
document.addEventListener(
|
|||
|
|
"click",
|
|||
|
|
(e) => {
|
|||
|
|
if (e.target.closest("button")) click();
|
|||
|
|
},
|
|||
|
|
true,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return { start, click, stamp, sting, toggle, get muted() { return muted; } };
|
|||
|
|
})();
|