super-mario / lib /engine /AudioManager.ts
asemxin
Add multiple levels and sound effects system
4539eae
// Audio Manager - handles all game sounds and music
export type SoundEffect = 'jump' | 'coin' | 'stomp' | 'death' | 'levelup' | 'gameover';
export class AudioManager {
private sounds: Map<string, HTMLAudioElement> = new Map();
private bgm: HTMLAudioElement | null = null;
private isMuted: boolean = false;
private volume: number = 0.5;
private initialized: boolean = false;
constructor() {
if (typeof window !== 'undefined') {
this.initSounds();
}
}
private initSounds(): void {
// Create audio elements with Web Audio API fallback
const soundEffects: SoundEffect[] = ['jump', 'coin', 'stomp', 'death', 'levelup', 'gameover'];
soundEffects.forEach(sound => {
const audio = new Audio();
audio.volume = this.volume;
// Use data URLs for simple 8-bit sounds
audio.src = this.getDataUrl(sound);
audio.preload = 'auto';
this.sounds.set(sound, audio);
});
// Background music
this.bgm = new Audio();
this.bgm.volume = this.volume * 0.3;
this.bgm.loop = true;
this.initialized = true;
}
private getDataUrl(sound: SoundEffect): string {
// Generate simple 8-bit style sounds using oscillator tones encoded as data URLs
// These are placeholder audio - in production, use actual audio files
const ctx = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
const frequencies: Record<SoundEffect, { freq: number; duration: number; type: OscillatorType }> = {
jump: { freq: 400, duration: 0.15, type: 'square' },
coin: { freq: 800, duration: 0.1, type: 'square' },
stomp: { freq: 200, duration: 0.1, type: 'square' },
death: { freq: 150, duration: 0.5, type: 'sawtooth' },
levelup: { freq: 600, duration: 0.3, type: 'square' },
gameover: { freq: 100, duration: 0.8, type: 'sawtooth' },
};
// Store context for later use
this.audioContext = ctx;
this.soundConfigs = frequencies;
// Return empty data URL - we'll use Web Audio API directly
return 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=';
}
private audioContext: AudioContext | null = null;
private soundConfigs: Record<SoundEffect, { freq: number; duration: number; type: OscillatorType }> | null = null;
play(sound: SoundEffect): void {
if (this.isMuted || !this.initialized) return;
// Use Web Audio API for dynamic sound generation
if (this.audioContext && this.soundConfigs) {
try {
const config = this.soundConfigs[sound];
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.type = config.type;
oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime);
// Frequency slide for jump sound
if (sound === 'jump') {
oscillator.frequency.exponentialRampToValueAtTime(
config.freq * 2,
this.audioContext.currentTime + config.duration
);
}
// Frequency drop for death sound
if (sound === 'death' || sound === 'gameover') {
oscillator.frequency.exponentialRampToValueAtTime(
config.freq * 0.5,
this.audioContext.currentTime + config.duration
);
}
// Coin sound - quick double beep
if (sound === 'coin') {
oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime);
oscillator.frequency.setValueAtTime(config.freq * 1.5, this.audioContext.currentTime + 0.05);
}
gainNode.gain.setValueAtTime(this.volume * 0.3, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + config.duration);
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + config.duration);
} catch (e) {
console.warn('Audio playback failed:', e);
}
}
}
playBGM(): void {
if (this.isMuted || !this.bgm) return;
// Create a simple looping melody using Web Audio API
if (this.audioContext) {
this.createBGMLoop();
}
}
private bgmInterval: ReturnType<typeof setInterval> | null = null;
private createBGMLoop(): void {
if (!this.audioContext || this.bgmInterval) return;
const notes = [262, 294, 330, 349, 392, 440, 494, 523]; // C major scale
let noteIndex = 0;
this.bgmInterval = setInterval(() => {
if (this.isMuted || !this.audioContext) {
if (this.bgmInterval) clearInterval(this.bgmInterval);
return;
}
const oscillator = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
oscillator.type = 'square';
oscillator.frequency.setValueAtTime(notes[noteIndex % notes.length], this.audioContext.currentTime);
gainNode.gain.setValueAtTime(this.volume * 0.1, this.audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
oscillator.connect(gainNode);
gainNode.connect(this.audioContext.destination);
oscillator.start(this.audioContext.currentTime);
oscillator.stop(this.audioContext.currentTime + 0.15);
noteIndex++;
}, 200);
}
stopBGM(): void {
if (this.bgmInterval) {
clearInterval(this.bgmInterval);
this.bgmInterval = null;
}
if (this.bgm) {
this.bgm.pause();
this.bgm.currentTime = 0;
}
}
setVolume(volume: number): void {
this.volume = Math.max(0, Math.min(1, volume));
this.sounds.forEach(audio => {
audio.volume = this.volume;
});
if (this.bgm) {
this.bgm.volume = this.volume * 0.3;
}
}
getVolume(): number {
return this.volume;
}
toggleMute(): boolean {
this.isMuted = !this.isMuted;
if (this.isMuted) {
this.stopBGM();
}
return this.isMuted;
}
isSoundMuted(): boolean {
return this.isMuted;
}
setMuted(muted: boolean): void {
this.isMuted = muted;
if (muted) {
this.stopBGM();
}
}
// Resume audio context (needed after user interaction)
resume(): void {
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}
}
// Singleton instance
let audioManagerInstance: AudioManager | null = null;
export function getAudioManager(): AudioManager {
if (!audioManagerInstance) {
audioManagerInstance = new AudioManager();
}
return audioManagerInstance;
}