L'Ingorgo Sonoro: dal manifesto in piazza, ingrandito e toccato, «Quanto darei per essere all'Ingorgo stasera?!?» e l'effetto speciale; il centro di notte pieno zeppo di gente (sfondi di Codex), le console di Andrea (Suno) che si mescolano camminando, Andrea più grande alla festa, uscendo si torna a maggio. Baldoria al campo sportivo col bandierone della Sampdoria (Codex) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
This commit is contained in:
+44
-21
@@ -9,22 +9,39 @@ import { versioned } from '../version.js';
|
||||
// Quale brano: quello della scena se ce l'ha; altrimenti, all'aperto, quello della fase del giorno (la sera ha il suo);
|
||||
// altrimenti quello generale del paese.
|
||||
// Un primo piano con la sua musica (i tavoli da carte, col valzer) la porta con sé finché resta aperto.
|
||||
export function trackFor(story, { screen, scene, phase, closeup }) {
|
||||
// Una scena con le console accese (`stages`, ognuna col suo `when`) non ha un brano solo ma una miscela: ogni console
|
||||
// suona il suo, più forte quanto più Andrea (`at`) le è vicino (mix). Le console possono stare anche fuori dal quadro:
|
||||
// è la musica della piazza accanto, che arriva da lontano.
|
||||
// `ok(cond)` dice se una condizione della storia vale adesso (game.js, check).
|
||||
export function trackFor(story, { screen, scene, phase, closeup, at, ok = () => false }) {
|
||||
const m = story.music ?? {};
|
||||
if (screen === 'splash') return null;
|
||||
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
||||
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
||||
if (closeup && story.closeups?.[closeup]?.music) return story.closeups[closeup].music;
|
||||
const sc = story.scenes[scene];
|
||||
const stages = (sc?.stages ?? []).filter(st => ok(st.when));
|
||||
if (stages.length) return mix(stages, at ?? sc.entry ?? [320, 300]);
|
||||
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
||||
}
|
||||
|
||||
// Il volume di ogni console: pieno fino a `near` pixel, poi cala fino a `far` (non sotto un filo, `floor`: la festa
|
||||
// si sente sempre). La più forte per prima.
|
||||
export function mix(stages, [x, y]) {
|
||||
return stages.map(st => {
|
||||
const d = Math.hypot(x - st.at[0], (y - st.at[1]) * 1.5), near = st.near ?? 90, far = st.far ?? 520;
|
||||
const k = Math.max(0, Math.min(1, (d - near) / (far - near)));
|
||||
return { src: st.music, gain: Math.max(st.floor ?? 0.08, (1 - k) ** 1.6) };
|
||||
}).sort((a, b) => b.gain - a.gain);
|
||||
}
|
||||
|
||||
const FADE = 1.2;
|
||||
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta.
|
||||
function setLevel(a) {
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta. Lì, nella
|
||||
// miscela delle console, suona solo la più forte (`alone`): le altre a volume pieno coprirebbero tutto.
|
||||
function setLevel(a, alone = false) {
|
||||
a.volume = a.level;
|
||||
a.muted = a.level === 0;
|
||||
a.muted = a.level === 0 || (alone && a.level < 1 && Math.abs(a.volume - a.level) > 0.01);
|
||||
}
|
||||
|
||||
export class Music {
|
||||
@@ -33,40 +50,46 @@ export class Music {
|
||||
this.volume = volume;
|
||||
this.blocked = null;
|
||||
this.want = null;
|
||||
this.current = null; // { src, audio }
|
||||
this.old = [];
|
||||
this.tracks = new Map(); // src -> { src, audio, gain }: quelli voluti e quelli che sfumano
|
||||
this.wanted = []; // i src voluti adesso, il più forte per primo
|
||||
this.duck = 1; // 1 = pieno; meno di 1 mentre parla qualcuno
|
||||
this.hold = false; // in pausa: il brano sfuma e si ferma, poi riprende da dov'era
|
||||
this.away = false; // pagina nascosta (sul telefono si è passati a un'altra app): tutto fermo subito
|
||||
}
|
||||
|
||||
// Il brano principale (il più forte) e quelli che stanno sfumando.
|
||||
get current() { return this.tracks.get(this.wanted[0]) ?? null; }
|
||||
get old() { return [...this.tracks.values()].filter(t => !this.wanted.includes(t.src)).map(t => t.audio); }
|
||||
|
||||
// Nascosta la pagina il browser non chiama più update (niente fotogrammi): i brani si fermano qui, di colpo.
|
||||
// Tornati, update li fa ripartire da dov'erano (se non c'è la pausa).
|
||||
setAway(away) {
|
||||
this.away = away;
|
||||
if (away) for (const a of [this.current?.audio, ...this.old]) a?.pause();
|
||||
if (away) for (const t of this.tracks.values()) t.audio.pause();
|
||||
}
|
||||
|
||||
// Chiamata a ogni fotogramma con il brano voluto: avvia, cambia e sfuma.
|
||||
update(src, dt) {
|
||||
this.want = src;
|
||||
if (src !== this.current?.src) {
|
||||
if (this.current) this.old.push(this.current.audio);
|
||||
this.current = src ? { src, audio: this.start(src) } : null;
|
||||
// Chiamata a ogni fotogramma con il brano voluto (o la miscela delle console, [{ src, gain }]): avvia, cambia e sfuma.
|
||||
update(want, dt) {
|
||||
this.want = want;
|
||||
const list = want == null ? [] : typeof want === 'string' ? [{ src: want, gain: 1 }] : want;
|
||||
this.wanted = list.map(w => w.src);
|
||||
for (const w of list) {
|
||||
const t = this.tracks.get(w.src) ?? { src: w.src, audio: this.start(w.src) };
|
||||
t.gain = w.gain;
|
||||
this.tracks.set(w.src, t);
|
||||
}
|
||||
// Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero, e lì si fermano.
|
||||
// Ogni brano voluto sale (o scende) verso il suo volume; quelli lasciati sfumano fino a zero, e lì si fermano.
|
||||
// Il livello si tiene in `level`, non si rilegge da audio.volume: su iPhone e iPad quello è di sola lettura e
|
||||
// resta sempre 1, e un brano vecchio non arrivava mai a zero (in chiesa l'organo suonava insieme al paese).
|
||||
const step = dt / FADE, target = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
if (this.current) {
|
||||
const a = this.current.audio;
|
||||
const step = dt / FADE, full = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
for (const t of [...this.tracks.values()]) {
|
||||
const a = t.audio, on = this.wanted.includes(t.src), target = on ? full * t.gain : 0;
|
||||
a.level = a.level < target ? Math.min(target, a.level + step) : Math.max(target, a.level - step);
|
||||
setLevel(a);
|
||||
setLevel(a, list.length > 1 && t.src !== this.wanted[0]);
|
||||
if (!on) { if (a.level === 0) { a.pause(); this.tracks.delete(t.src); } continue; }
|
||||
if (this.hold && a.level === 0 && !a.paused) a.pause();
|
||||
else if (!this.hold && !this.away && a.paused && a !== this.blocked) a.play().catch(() => { this.blocked = a; });
|
||||
}
|
||||
for (const a of this.old) { a.level = Math.max(0, a.level - step); setLevel(a); }
|
||||
this.old = this.old.filter(a => a.level > 0 || (a.pause(), false));
|
||||
}
|
||||
|
||||
start(src) {
|
||||
@@ -81,7 +104,7 @@ export class Music {
|
||||
unlock() {
|
||||
const a = this.blocked;
|
||||
this.blocked = null;
|
||||
if (a && a === this.current?.audio) a.play().catch(() => {});
|
||||
if (a && [...this.tracks.values()].some(t => t.audio === a && this.wanted.includes(t.src))) a.play().catch(() => {});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
Reference in New Issue
Block a user