Suoni d'ambiente sintetizzati (vento, acqua, uccelli, grilli, campane) col loro volume in Pausa; la fessura in primo piano: si cerca col fascio della torcia il quadro e gli occhi (lavoro di Codex). La foto del palio leggibile da nginx, e un test che lo controlla per tutti i file serviti
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// Web Audio: vento/acqua filtrati e piccoli eventi sintetizzati. Nessun download,
|
||||
// nessun timer in sottofondo; il ciclo del gioco decide quando suonare e quando fermarsi.
|
||||
import { soundscapes } from '../story/soundscapes.js';
|
||||
|
||||
export class Soundscape {
|
||||
constructor({ volume = 0.45, createContext = () => new (globalThis.AudioContext ?? globalThis.webkitAudioContext)(), random = Math.random } = {}) {
|
||||
this.volume = volume;
|
||||
this.createContext = createContext;
|
||||
this.random = random;
|
||||
this.context = null;
|
||||
this.nodes = new Set();
|
||||
this.active = false;
|
||||
this.muted = false;
|
||||
this.key = null;
|
||||
this.elapsed = 0;
|
||||
this.next = 4;
|
||||
this.bed = null;
|
||||
}
|
||||
|
||||
unlock() {
|
||||
try {
|
||||
if (!this.context) {
|
||||
this.context = this.createContext();
|
||||
this.master = this.context.createGain();
|
||||
this.master.gain.value = 0;
|
||||
this.master.connect(this.context.destination);
|
||||
const rate = this.context.sampleRate;
|
||||
this.noise = this.context.createBuffer(1, rate * 4, rate);
|
||||
const data = this.noise.getChannelData(0);
|
||||
for (let i = 0; i < data.length; i++) data[i] = this.random() * 2 - 1;
|
||||
}
|
||||
if (this.context.state === 'suspended') this.context.resume().catch(() => {});
|
||||
} catch {
|
||||
// Anche un'inizializzazione parziale fallita deve lasciare giocabile l'avventura.
|
||||
this.master?.disconnect(); this.context = null; this.master = null; this.noise = null;
|
||||
}
|
||||
}
|
||||
|
||||
get audible() { return !!this.context && this.active && !this.muted && this.volume > 0; }
|
||||
|
||||
configure({ active = this.active, muted = this.muted, volume = this.volume, duck = 1 } = {}) {
|
||||
this.active = active;
|
||||
this.muted = muted;
|
||||
this.volume = Math.max(0, Math.min(1, volume));
|
||||
if (!this.context) return;
|
||||
const gain = this.audible ? this.volume * duck : 0;
|
||||
this.master.gain.cancelScheduledValues(this.context.currentTime);
|
||||
if (!gain) {
|
||||
this.master.gain.setValueAtTime(0, this.context.currentTime);
|
||||
this.clear();
|
||||
} else this.master.gain.setTargetAtTime(gain, this.context.currentTime, 0.12);
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const node of [...this.nodes]) node.stop();
|
||||
this.bed = null;
|
||||
}
|
||||
|
||||
// Tutti i nodi, inclusi filtro e gain, vengono scollegati alla fine del suono.
|
||||
connect(source, gain, filter = null, pan = 0) {
|
||||
const panner = this.context.createStereoPanner?.();
|
||||
if (panner) panner.pan.value = pan;
|
||||
const chain = [source, filter, gain, panner, this.master].filter(Boolean);
|
||||
for (let i = 0; i < chain.length - 1; i++) chain[i].connect(chain[i + 1]);
|
||||
const node = { stop: () => {
|
||||
if (!this.nodes.delete(node)) return;
|
||||
try { source.stop(); } catch { /* già finito */ }
|
||||
chain.slice(0, -1).forEach(n => n.disconnect());
|
||||
} };
|
||||
source.onended = node.stop;
|
||||
this.nodes.add(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
noiseSound(duration, frequency, level, { loop = false, pan = 0, paper = false } = {}) {
|
||||
const c = this.context, now = c.currentTime;
|
||||
const source = c.createBufferSource(), filter = c.createBiquadFilter(), gain = c.createGain();
|
||||
source.buffer = this.noise; source.loop = loop;
|
||||
filter.type = paper ? 'bandpass' : 'lowpass'; filter.frequency.value = frequency; filter.Q.value = 0.5;
|
||||
gain.gain.setValueAtTime(0, now);
|
||||
gain.gain.linearRampToValueAtTime(level, now + (loop ? 1.3 : duration * 0.15));
|
||||
if (!loop) gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
|
||||
const node = this.connect(source, gain, filter, pan);
|
||||
source.start();
|
||||
if (!loop) source.stop(now + duration);
|
||||
return { node, gain, level };
|
||||
}
|
||||
|
||||
tone(frequency, end, duration, level, delay = 0, pan = 0) {
|
||||
const c = this.context, at = c.currentTime + delay, source = c.createOscillator(), gain = c.createGain();
|
||||
source.type = 'sine';
|
||||
source.frequency.setValueAtTime(frequency, at);
|
||||
source.frequency.exponentialRampToValueAtTime(end, at + duration);
|
||||
gain.gain.setValueAtTime(0, at);
|
||||
gain.gain.linearRampToValueAtTime(level, at + 0.015);
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, at + duration);
|
||||
this.connect(source, gain, null, pan);
|
||||
source.start(at); source.stop(at + duration + 0.03);
|
||||
}
|
||||
|
||||
play(kind, { pan = (this.random() - 0.5) * 1.3 } = {}) {
|
||||
if (!this.audible) return;
|
||||
if (kind === 'page') this.noiseSound(0.42, 2800, 0.24, { paper: true });
|
||||
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
|
||||
else if (kind === 'drip') this.tone(1150, 540, 0.18, 0.055, 0, pan);
|
||||
else if (kind === 'bird') {
|
||||
for (let i = 0; i < 3; i++) this.tone(2100 + this.random() * 600, 3500, 0.13, 0.035, i * 0.22, pan);
|
||||
} else if (kind === 'cricket') {
|
||||
for (let i = 0; i < 5; i++) this.tone(3900, 4200, 0.055, 0.016, i * 0.12, pan);
|
||||
} else if (kind === 'bell' || kind === 'glass') {
|
||||
const base = kind === 'bell' ? 440 : 1420, duration = kind === 'bell' ? 3.8 : 0.65;
|
||||
for (const [ratio, level] of [[1, 0.035], [2.76, 0.012], [4.07, 0.006]])
|
||||
this.tone(base * ratio, base * ratio, duration, level, 0, pan);
|
||||
}
|
||||
}
|
||||
|
||||
update(profile, phase, dt) {
|
||||
if (!this.audible) return;
|
||||
const key = `${profile}:${phase}`, def = soundscapes[profile];
|
||||
if (!def) return;
|
||||
if (this.key !== key) {
|
||||
this.clear(); this.key = key; this.elapsed = 0; this.next = 4 + this.random() * 5;
|
||||
}
|
||||
if (!this.bed && def.bed) this.bed = this.noiseSound(0, def.bed === 'water' ? 1700 : 340, def.level, { loop: true });
|
||||
this.elapsed += dt;
|
||||
if (this.bed && this.elapsed > 1.5) this.bed.gain.gain.setTargetAtTime(def.level * (0.8 + 0.2 * Math.sin(this.elapsed * 0.7)), this.context.currentTime, 0.6);
|
||||
if (this.elapsed < this.next) return;
|
||||
const events = phase === 'sera' ? def.night : def.day;
|
||||
this.play(events[Math.floor(this.random() * events.length)]);
|
||||
this.next = this.elapsed + def.gap[0] + this.random() * (def.gap[1] - def.gap[0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user