Files
tankwar_proj/js/managers/AudioManager.js
T
2026-04-10 22:59:39 +08:00

231 lines
7.3 KiB
JavaScript

/**
* 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<string, AudioBuffer>} */
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;