// 背景音乐。 // // 两条交互约定: // 1. 音源与音量是「用户级覆盖」:切换壁纸不得把用户自定义的音源/音量清回预设默认值。 // 2. 音源相同的设置是 no-op:否则两档壁纸共用同一首曲子时,一切换就把音乐从头开始放。 export default class AudioController { constructor(audioId, options = {}) { this.audio = document.getElementById(audioId); this.isPlaying = false; this.retryTimer = undefined; this.audio.loop = true; if (options.source) this.setSource(options.source); if (options.volume !== undefined) this.setVolume(options.volume); } // audio.src 会被解析成绝对 URL;预设里的 source 也是绝对 URL(preset.js 用 import.meta.url 推导) get source() { return this.audio.src || ""; } setSource(source) { if (!source) return; if (this.source === new URL(source, document.baseURI).href) return; const wasPlaying = this.isPlaying; this.audio.src = source; this.audio.load(); if (wasPlaying) this.play(); } setVolume(volume) { const value = Math.max(0, Math.min(1, Number(volume))); this.audio.volume = Number.isFinite(value) ? value : 1; if (this.audio.volume <= 0) this.pause(); else if (!this.isPlaying) this.play(); } play() { if (this.isPlaying) return; this.audio.play().then( () => { this.isPlaying = true; }, (error) => { this.isPlaying = false; // 只重试一次:自动播放被拒时重试多少次都没用,重试只是覆盖"音频尚未就绪" if (this.retryTimer === undefined) { this.retryTimer = setTimeout(() => { this.retryTimer = undefined; this.play(); }, 500); } }, ); } pause() { this.audio.pause(); this.isPlaying = false; } }