/** * AudioManager.js * Manages game sound effects using wx.createWebAudioContext for programmatic synthesis. * No external audio files needed — all sounds are generated via PCM buffers. */ class AudioManager { constructor() { this._soundEnabled = true; this._musicEnabled = true; /** @type {AudioContext|null} WebAudio context */ this._audioCtx = null; // Cached audio buffers for each sound /** @type {Map} */ this._buffers = new Map(); this._initialized = false; // Listen for settings changes if (typeof GameGlobal !== 'undefined' && GameGlobal.eventBus) { GameGlobal.eventBus.on('settings:changed', (settings) => { this._soundEnabled = settings.soundEnabled; this._musicEnabled = settings.musicEnabled; }); } } /** * Initialize WebAudio context and generate all sound buffers. * Must be called after user interaction (touch) on some platforms. */ init() { if (this._initialized) return; try { if (typeof wx !== 'undefined' && wx.createWebAudioContext) { this._audioCtx = wx.createWebAudioContext(); } } catch (e) { console.warn('[AudioManager] Failed to create WebAudioContext:', e); } if (!this._audioCtx) { console.warn('[AudioManager] WebAudioContext not available, audio disabled.'); return; } // Pre-generate all sound effect buffers this._generateSounds(); this._initialized = true; console.log('[AudioManager] Initialized with programmatic audio synthesis.'); } /** * Generate all game sound effect buffers. * @private */ _generateSounds() { const ctx = this._audioCtx; const sampleRate = ctx.sampleRate; // Shoot sound: short high-frequency burst this._buffers.set('shoot', this._generateBuffer(sampleRate, 0.08, (i, len) => { const t = i / len; const freq = 800 - t * 400; const envelope = 1 - t; return Math.sin(2 * Math.PI * freq * i / sampleRate) * envelope * 0.3; })); // Explosion (small): noise burst with decay this._buffers.set('explosion_small', this._generateBuffer(sampleRate, 0.2, (i, len) => { const t = i / len; const envelope = (1 - t) * (1 - t); const noise = Math.random() * 2 - 1; const tone = Math.sin(2 * Math.PI * 120 * i / sampleRate); return (noise * 0.6 + tone * 0.4) * envelope * 0.35; })); // Explosion (big): longer, deeper noise burst this._buffers.set('explosion_big', this._generateBuffer(sampleRate, 0.4, (i, len) => { const t = i / len; const envelope = (1 - t) * (1 - t); const noise = Math.random() * 2 - 1; const tone = Math.sin(2 * Math.PI * 60 * i / sampleRate); const tone2 = Math.sin(2 * Math.PI * 90 * i / sampleRate); return (noise * 0.5 + tone * 0.3 + tone2 * 0.2) * envelope * 0.4; })); // Hit (bullet hits armor but doesn't destroy): metallic ping this._buffers.set('hit', this._generateBuffer(sampleRate, 0.1, (i, len) => { const t = i / len; const envelope = (1 - t); return Math.sin(2 * Math.PI * 1200 * i / sampleRate) * envelope * 0.2 + Math.sin(2 * Math.PI * 2400 * i / sampleRate) * envelope * 0.1; })); // Bullet hit wall: short thud this._buffers.set('hit_wall', this._generateBuffer(sampleRate, 0.06, (i, len) => { const t = i / len; const envelope = (1 - t); const noise = Math.random() * 2 - 1; return (noise * 0.4 + Math.sin(2 * Math.PI * 300 * i / sampleRate) * 0.6) * envelope * 0.2; })); // Power-up pickup: ascending chime this._buffers.set('powerup', this._generateBuffer(sampleRate, 0.25, (i, len) => { const t = i / len; const freq = 400 + t * 800; const envelope = t < 0.1 ? t / 0.1 : (1 - (t - 0.1) / 0.9); return (Math.sin(2 * Math.PI * freq * i / sampleRate) * 0.5 + Math.sin(2 * Math.PI * freq * 1.5 * i / sampleRate) * 0.2) * envelope * 0.3; })); // Game over: descending tone this._buffers.set('gameover', this._generateBuffer(sampleRate, 0.6, (i, len) => { const t = i / len; const freq = 400 - t * 250; const envelope = 1 - t; return Math.sin(2 * Math.PI * freq * i / sampleRate) * envelope * 0.3; })); // Victory: ascending fanfare this._buffers.set('victory', this._generateBuffer(sampleRate, 0.5, (i, len) => { const t = i / len; // Three-note ascending pattern let freq; if (t < 0.33) freq = 523; // C5 else if (t < 0.66) freq = 659; // E5 else freq = 784; // G5 const segT = (t % 0.33) / 0.33; const envelope = segT < 0.1 ? segT / 0.1 : Math.max(0, 1 - (segT - 0.1) / 0.9); return (Math.sin(2 * Math.PI * freq * i / sampleRate) * 0.4 + Math.sin(2 * Math.PI * freq * 2 * i / sampleRate) * 0.15) * envelope * 0.3; })); // Move: low rumble (very short, for tank movement) this._buffers.set('move', this._generateBuffer(sampleRate, 0.05, (i, len) => { const t = i / len; const envelope = 1 - t; return Math.sin(2 * Math.PI * 80 * i / sampleRate) * envelope * 0.1; })); } /** * Generate a PCM audio buffer. * @private * @param {number} sampleRate * @param {number} duration - Duration in seconds. * @param {Function} generator - (sampleIndex, totalSamples) => sampleValue [-1, 1] * @returns {AudioBuffer} */ _generateBuffer(sampleRate, duration, generator) { const ctx = this._audioCtx; const length = Math.floor(sampleRate * duration); const buffer = ctx.createBuffer(1, length, sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < length; i++) { data[i] = generator(i, length); } return buffer; } /** * Play a sound effect by name. * @param {string} name - Sound name (shoot, explosion_small, explosion_big, hit, hit_wall, powerup, gameover, victory, move). */ playSFX(name) { if (!this._soundEnabled || !this._initialized || !this._audioCtx) return; const buffer = this._buffers.get(name); if (!buffer) return; try { const source = this._audioCtx.createBufferSource(); source.buffer = buffer; source.connect(this._audioCtx.destination); source.start(0); } catch (e) { // Silently ignore playback errors } } /** * Register a sound effect (kept for backward compatibility). * @param {string} name * @param {string} path */ register(name, path) { // No-op: sounds are now generated programmatically } /** * Play background music (no-op for now, can be implemented later with audio files). * @param {string} path */ playBGM(path) { // BGM requires audio files — not implemented in programmatic mode } /** Stop background music. */ stopBGM() {} /** Pause all audio. */ pauseAll() {} /** Resume audio. */ resumeAll() {} /** Destroy audio context. */ destroy() { if (this._audioCtx) { try { this._audioCtx.close(); } catch (e) {} this._audioCtx = null; } this._buffers.clear(); this._initialized = false; } /** Whether sound effects are enabled. */ get soundEnabled() { return this._soundEnabled; } set soundEnabled(v) { this._soundEnabled = v; } /** Whether music is enabled. */ get musicEnabled() { return this._musicEnabled; } set musicEnabled(v) { this._musicEnabled = v; } } module.exports = AudioManager;