export class AudioController { private audioContext: AudioContext | null = null private soundFiles: Record = {} private audioCache: Record = {} constructor(soundFiles: Record = {}) { this.soundFiles = soundFiles } private initAudioContext() { if (!this.audioContext) { this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() } } /** * 播放指定类型的音效 * 优先使用预设的音频文件,如果没有配置或加载失败,则使用 Web Audio API 合成音效 */ async play(type: 'start' | 'tick' | 'flip') { // 尝试播放音频文件 if (this.soundFiles[type]) { try { await this.playFile(type) return } catch (error) { console.warn(`Failed to play audio file for ${type}, falling back to synth.`, error) } } // 回退到合成音效 this.playSynth(type) } private playFile(type: string): Promise { return new Promise((resolve, reject) => { const url = this.soundFiles[type] if (!url) { reject(new Error('No file url')) return } // 使用缓存的 Audio 对象 if (!this.audioCache[type]) { this.audioCache[type] = new Audio(url) } const audio = this.audioCache[type] audio.currentTime = 0 audio.play() .then(() => resolve()) .catch(e => reject(e)) }) } private playSynth(type: 'start' | 'tick' | 'flip') { this.initAudioContext() if (!this.audioContext) return const ctxTime = this.audioContext.currentTime const gain = this.audioContext.createGain() gain.connect(this.audioContext.destination) if (type === 'start') { const osc = this.audioContext.createOscillator() osc.connect(gain) // 开始音效:高音短促提示 osc.type = 'sine' osc.frequency.setValueAtTime(880, ctxTime) osc.frequency.exponentialRampToValueAtTime(440, ctxTime + 0.3) gain.gain.setValueAtTime(0.5, ctxTime) gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3) osc.start(ctxTime) osc.stop(ctxTime + 0.3) } else if (type === 'tick') { const osc = this.audioContext.createOscillator() osc.connect(gain) // 倒计时音效:急促的滴答声 osc.type = 'triangle' osc.frequency.setValueAtTime(600, ctxTime) gain.gain.setValueAtTime(0.3, ctxTime) gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.1) osc.start(ctxTime) osc.stop(ctxTime + 0.1) } else if (type === 'flip') { // 翻书音效:使用白噪声模拟纸张摩擦 const bufferSize = this.audioContext.sampleRate * 0.5 // 0.5秒缓冲 const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate) const data = buffer.getChannelData(0) // 生成白噪声 for (let i = 0; i < bufferSize; i++) { data[i] = Math.random() * 2 - 1 } const noise = this.audioContext.createBufferSource() noise.buffer = buffer // 滤波器:低通滤波器,模拟纸张的闷声 const filter = this.audioContext.createBiquadFilter() filter.type = 'lowpass' filter.frequency.setValueAtTime(400, ctxTime) filter.frequency.exponentialRampToValueAtTime(3000, ctxTime + 0.1) // 频率快速扫过,模拟快速翻动 noise.connect(filter) filter.connect(gain) // 音量包络:快速淡入淡出 gain.gain.setValueAtTime(0, ctxTime) gain.gain.linearRampToValueAtTime(0.8, ctxTime + 0.05) gain.gain.exponentialRampToValueAtTime(0.01, ctxTime + 0.3) noise.start(ctxTime) noise.stop(ctxTime + 0.3) } } }