diff --git a/src/engine/music.js b/src/engine/music.js index 6d36ecb..2b3d366 100644 --- a/src/engine/music.js +++ b/src/engine/music.js @@ -19,6 +19,12 @@ export function trackFor(story, { screen, scene, phase }) { 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; @@ -46,22 +52,26 @@ export class Music { 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. + // 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.volume = a.volume < target ? Math.min(target, a.volume + step) : Math.max(target, a.volume - step); - if (this.hold && a.volume === 0 && !a.paused) a.pause(); + 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.volume = Math.max(0, a.volume - step); - this.old = this.old.filter(a => a.volume > 0 || (a.pause(), false)); + 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.volume = 0; + audio.level = 0; + setLevel(audio); audio.play().catch(() => { this.blocked = audio; }); // senza permesso si riprova al primo gesto return audio; } diff --git a/tests/music.test.mjs b/tests/music.test.mjs index 32e0da1..cbc2ef4 100644 --- a/tests/music.test.mjs +++ b/tests/music.test.mjs @@ -83,3 +83,20 @@ test('passando a un\'altra app la musica si ferma subito, e tornando riparte', ( m.update('b.mp3', 0.1); assert.ok(now.paused, 'con la pausa aperta resta ferma'); }); + +test('su iPhone (volume di sola lettura, sempre 1) il brano vecchio si ferma lo stesso: niente organo sopra il paese', () => { + globalThis.Audio = class { constructor(src) { this.src = src; this.paused = true; this.muted = false; } + get volume() { return 1; } set volume(v) {} + play() { this.paused = false; return Promise.resolve(); } pause() { this.paused = true; } }; + const m = new Music({ volume: 0.6 }); + for (let i = 0; i < 20; i++) m.update('paese.mp3', 0.1); + const paese = m.current.audio; + for (let i = 0; i < 20; i++) m.update('pieve.mp3', 0.1); + assert.ok(paese.paused, 'il brano del paese, sfumato, si è fermato'); + assert.deepEqual(m.old, []); + assert.ok(!m.current.audio.paused && !m.current.audio.muted, 'suona solo l\'organo'); + m.muted = true; + for (let i = 0; i < 20; i++) m.update('pieve.mp3', 0.1); + assert.ok(m.current.audio.muted, 'il muto funziona anche dove il volume no'); + delete globalThis.Audio; +});