| |
| class AudioManager { |
| constructor() { |
| this.audioContext = null; |
| this.sounds = new Map(); |
| this.isEnabled = true; |
| this.volume = 0.7; |
| this.isInitialized = false; |
| this.hasShownInteractionHint = false; |
| |
| |
| this.soundConfig = { |
| potActivate: { frequency: 440, duration: 0.5, type: 'magic' }, |
| cooking: { frequency: 220, duration: 0.3, type: 'bubble' }, |
| foodPop: { frequency: 660, duration: 0.2, type: 'pop' }, |
| animalAppear: { frequency: 880, duration: 0.4, type: 'chirp' }, |
| girlAppear: { frequency: 1320, duration: 0.6, type: 'sparkle' }, |
| celebration: { frequency: 1760, duration: 1.0, type: 'fanfare' }, |
| stopCooking: { frequency: 330, duration: 0.4, type: 'whoosh' } |
| }; |
| |
| this.init(); |
| } |
| |
| async init() { |
| try { |
| |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| |
| |
| this.setupUserInteractionHandlers(); |
| |
| console.log('音频系统已准备,等待用户交互启动'); |
| } catch (error) { |
| console.warn('音频系统初始化失败:', error); |
| this.isEnabled = false; |
| } |
| } |
| |
| setupUserInteractionHandlers() { |
| const startAudio = async () => { |
| if (this.audioContext && this.audioContext.state === 'suspended') { |
| try { |
| await this.audioContext.resume(); |
| |
| |
| if (!this.isInitialized) { |
| this.preGenerateSounds(); |
| this.isInitialized = true; |
| console.log('音频系统初始化完成'); |
| } |
| |
| |
| this.hideAudioActivationHint(); |
| |
| } catch (error) { |
| console.warn('启动音频上下文失败:', error); |
| } |
| } |
| }; |
| |
| |
| const events = ['click', 'touchstart', 'keydown']; |
| const handler = () => { |
| startAudio(); |
| |
| events.forEach(event => { |
| document.removeEventListener(event, handler); |
| }); |
| }; |
| |
| events.forEach(event => { |
| document.addEventListener(event, handler, { once: true }); |
| }); |
| } |
| |
| showAudioActivationHint() { |
| const hint = document.getElementById('audioActivationHint'); |
| if (hint) { |
| hint.style.display = 'block'; |
| console.log('显示音频激活提示'); |
| } |
| } |
| |
| hideAudioActivationHint() { |
| const hint = document.getElementById('audioActivationHint'); |
| if (hint) { |
| hint.style.display = 'none'; |
| console.log('隐藏音频激活提示'); |
| } |
| } |
| |
| async ensureAudioContext() { |
| if (!this.audioContext) return false; |
| |
| if (this.audioContext.state === 'suspended') { |
| try { |
| await this.audioContext.resume(); |
| |
| |
| if (!this.isInitialized) { |
| this.preGenerateSounds(); |
| this.isInitialized = true; |
| console.log('音频系统延迟初始化完成'); |
| } |
| |
| |
| this.hideAudioActivationHint(); |
| } catch (error) { |
| console.warn('无法恢复音频上下文:', error); |
| return false; |
| } |
| } |
| |
| return this.audioContext.state === 'running'; |
| } |
| |
| preGenerateSounds() { |
| |
| Object.keys(this.soundConfig).forEach(soundName => { |
| this.generateSound(soundName); |
| }); |
| } |
| |
| generateSound(soundName) { |
| const config = this.soundConfig[soundName]; |
| if (!config) return null; |
| |
| const sampleRate = this.audioContext.sampleRate; |
| const duration = config.duration; |
| const length = sampleRate * duration; |
| const buffer = this.audioContext.createBuffer(1, length, sampleRate); |
| const data = buffer.getChannelData(0); |
| |
| |
| switch (config.type) { |
| case 'magic': |
| this.generateMagicSound(data, config, sampleRate); |
| break; |
| case 'bubble': |
| this.generateBubbleSound(data, config, sampleRate); |
| break; |
| case 'pop': |
| this.generatePopSound(data, config, sampleRate); |
| break; |
| case 'chirp': |
| this.generateChirpSound(data, config, sampleRate); |
| break; |
| case 'sparkle': |
| this.generateSparkleSound(data, config, sampleRate); |
| break; |
| case 'fanfare': |
| this.generateFanfareSound(data, config, sampleRate); |
| break; |
| case 'whoosh': |
| this.generateWhooshSound(data, config, sampleRate); |
| break; |
| default: |
| this.generateSimpleBeep(data, config, sampleRate); |
| } |
| |
| this.sounds.set(soundName, buffer); |
| return buffer; |
| } |
| |
| generateMagicSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const freq = baseFreq * (1 + progress * 0.5); |
| |
| |
| const vibrato = 1 + 0.1 * Math.sin(2 * Math.PI * 6 * t); |
| |
| |
| const wave = Math.sin(2 * Math.PI * freq * vibrato * t); |
| |
| |
| const envelope = Math.sin(Math.PI * progress) * Math.exp(-progress * 2); |
| |
| data[i] = wave * envelope * 0.3; |
| } |
| } |
| |
| generateBubbleSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const randomFactor = 1 + 0.3 * (Math.random() - 0.5); |
| const freq = baseFreq * randomFactor; |
| |
| |
| const wave1 = Math.sin(2 * Math.PI * freq * t); |
| const wave2 = Math.sin(2 * Math.PI * freq * 1.5 * t) * 0.5; |
| const wave3 = Math.sin(2 * Math.PI * freq * 2 * t) * 0.25; |
| |
| const wave = wave1 + wave2 + wave3; |
| |
| |
| const envelope = Math.exp(-progress * 8); |
| |
| data[i] = wave * envelope * 0.2; |
| } |
| } |
| |
| generatePopSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const freq = baseFreq * (1 - progress * 0.8); |
| |
| |
| const noise = (Math.random() - 0.5) * 0.3; |
| |
| |
| const wave = Math.sin(2 * Math.PI * freq * t) + noise; |
| |
| |
| const envelope = Math.exp(-progress * 15); |
| |
| data[i] = wave * envelope * 0.4; |
| } |
| } |
| |
| generateChirpSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const freqMod = Math.sin(2 * Math.PI * 8 * t) * 0.2; |
| const freq = baseFreq * (1 + freqMod); |
| |
| const wave = Math.sin(2 * Math.PI * freq * t); |
| |
| |
| const envelope = Math.sin(Math.PI * progress) * Math.exp(-progress * 3); |
| |
| data[i] = wave * envelope * 0.25; |
| } |
| } |
| |
| generateSparkleSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const wave1 = Math.sin(2 * Math.PI * baseFreq * t); |
| const wave2 = Math.sin(2 * Math.PI * baseFreq * 2 * t) * 0.5; |
| const wave3 = Math.sin(2 * Math.PI * baseFreq * 3 * t) * 0.25; |
| const wave4 = Math.sin(2 * Math.PI * baseFreq * 4 * t) * 0.125; |
| |
| const wave = wave1 + wave2 + wave3 + wave4; |
| |
| |
| const envelope = Math.exp(-progress * 1.5); |
| |
| data[i] = wave * envelope * 0.2; |
| } |
| } |
| |
| generateFanfareSound(data, config, sampleRate) { |
| |
| const baseFreq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| |
| const freq1 = baseFreq; |
| const freq2 = baseFreq * 1.25; |
| const freq3 = baseFreq * 1.5; |
| |
| const wave1 = Math.sin(2 * Math.PI * freq1 * t); |
| const wave2 = Math.sin(2 * Math.PI * freq2 * t) * 0.8; |
| const wave3 = Math.sin(2 * Math.PI * freq3 * t) * 0.6; |
| |
| const wave = wave1 + wave2 + wave3; |
| |
| |
| const envelope = Math.sin(Math.PI * progress) * 0.8; |
| |
| data[i] = wave * envelope * 0.15; |
| } |
| } |
| |
| generateWhooshSound(data, config, sampleRate) { |
| |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const progress = i / length; |
| |
| |
| const noise = (Math.random() - 0.5) * 2; |
| |
| |
| const cutoff = 1000 * (1 - progress); |
| const filtered = noise * Math.exp(-progress * 3); |
| |
| |
| const envelope = Math.exp(-progress * 5); |
| |
| data[i] = filtered * envelope * 0.3; |
| } |
| } |
| |
| generateSimpleBeep(data, config, sampleRate) { |
| |
| const freq = config.frequency; |
| const length = data.length; |
| |
| for (let i = 0; i < length; i++) { |
| const t = i / sampleRate; |
| const progress = i / length; |
| |
| const wave = Math.sin(2 * Math.PI * freq * t); |
| const envelope = Math.exp(-progress * 3); |
| |
| data[i] = wave * envelope * 0.3; |
| } |
| } |
| |
| async playSound(soundName, volume = 1.0) { |
| if (!this.isEnabled) return; |
| |
| const canPlay = await this.ensureAudioContext(); |
| if (!canPlay) { |
| |
| if (!this.hasShownInteractionHint) { |
| this.showAudioActivationHint(); |
| this.hasShownInteractionHint = true; |
| } |
| return; |
| } |
| |
| if (!this.isInitialized) { |
| console.warn('音频系统尚未初始化'); |
| return; |
| } |
| |
| const buffer = this.sounds.get(soundName); |
| if (!buffer) { |
| console.warn(`音效 ${soundName} 不存在`); |
| return; |
| } |
| |
| try { |
| const source = this.audioContext.createBufferSource(); |
| const gainNode = this.audioContext.createGain(); |
| |
| source.buffer = buffer; |
| source.connect(gainNode); |
| gainNode.connect(this.audioContext.destination); |
| |
| |
| gainNode.gain.value = this.volume * volume; |
| |
| source.start(); |
| |
| |
| source.onended = () => { |
| source.disconnect(); |
| gainNode.disconnect(); |
| }; |
| |
| } catch (error) { |
| console.warn(`播放音效 ${soundName} 失败:`, error); |
| } |
| } |
| |
| |
| playPotActivate() { |
| this.playSound('potActivate'); |
| } |
| |
| playCooking() { |
| this.playSound('cooking', 0.5); |
| } |
| |
| playFoodPop() { |
| this.playSound('foodPop', 0.8); |
| } |
| |
| playAnimalAppear() { |
| this.playSound('animalAppear', 0.6); |
| } |
| |
| playGirlAppear() { |
| this.playSound('girlAppear', 0.7); |
| } |
| |
| playCelebration() { |
| this.playSound('celebration', 0.9); |
| } |
| |
| playStopCooking() { |
| this.playSound('stopCooking', 0.6); |
| } |
| |
| |
| playRandomFoodPop() { |
| const randomPitch = 0.8 + Math.random() * 0.4; |
| this.playSound('foodPop', randomPitch); |
| } |
| |
| |
| setVolume(volume) { |
| this.volume = Math.max(0, Math.min(1, volume)); |
| } |
| |
| |
| setEnabled(enabled) { |
| this.isEnabled = enabled; |
| localStorage.setItem('magicPotAudioEnabled', enabled.toString()); |
| } |
| |
| |
| getStatus() { |
| return { |
| enabled: this.isEnabled, |
| initialized: this.isInitialized, |
| volume: this.volume, |
| soundCount: this.sounds.size, |
| contextState: this.audioContext ? this.audioContext.state : 'none' |
| }; |
| } |
| |
| |
| async testAllSounds() { |
| const soundNames = Object.keys(this.soundConfig); |
| |
| for (let i = 0; i < soundNames.length; i++) { |
| const soundName = soundNames[i]; |
| console.log(`测试音效: ${soundName}`); |
| await this.playSound(soundName); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 800)); |
| } |
| } |
| } |
|
|
| |
| window.audioManager = new AudioManager(); |
|
|