Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
100 lines
4.3 KiB
JavaScript
100 lines
4.3 KiB
JavaScript
// Colonna sonora: un brano alla volta, in loop, con dissolvenza quando cambia.
|
|
// Quale brano suona lo decidono i dati (story.music e scene.music). Si prova a partire subito; se il browser non lo
|
|
// permette prima di un gesto del giocatore, il brano riparte al primo clic o tasto (unlock).
|
|
|
|
import { versioned } from '../version.js';
|
|
|
|
// Il brano del momento: titolo, fine, oppure quello della scena (o quello comune del gioco). Sotto il cartello
|
|
// d'apertura nessuno: lì suona la sua musichetta, una volta sola (main.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 }) {
|
|
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];
|
|
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
|
}
|
|
|
|
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) {
|
|
a.volume = a.level;
|
|
a.muted = a.level === 0;
|
|
}
|
|
|
|
export class Music {
|
|
constructor({ muted = false, volume = 0.6 } = {}) {
|
|
this.muted = muted;
|
|
this.volume = volume;
|
|
this.blocked = null;
|
|
this.want = null;
|
|
this.current = null; // { src, audio }
|
|
this.old = [];
|
|
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
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
// Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi 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;
|
|
a.level = a.level < target ? Math.min(target, a.level + step) : Math.max(target, a.level - step);
|
|
setLevel(a);
|
|
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) {
|
|
const audio = new Audio(versioned(src));
|
|
audio.loop = true;
|
|
audio.level = 0;
|
|
setLevel(audio);
|
|
audio.play().catch(() => { this.blocked = audio; }); // senza permesso si riprova al primo gesto
|
|
return audio;
|
|
}
|
|
|
|
unlock() {
|
|
const a = this.blocked;
|
|
this.blocked = null;
|
|
if (a && a === this.current?.audio) a.play().catch(() => {});
|
|
}
|
|
|
|
toggle() {
|
|
this.muted = !this.muted;
|
|
return this.muted;
|
|
}
|
|
|
|
setVolume(v) {
|
|
this.volume = Math.max(0, Math.min(1, v));
|
|
if (this.volume > 0) this.muted = false;
|
|
return this.volume;
|
|
}
|
|
|
|
settings() { return { volume: this.volume, muted: this.muted }; }
|
|
}
|