first commit
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user