| class GameAudio { |
| private ctx: AudioContext | null = null; |
| private ambientOscillators: { osc: OscillatorNode; gain: GainNode }[] = []; |
| private isAmbientPlaying = false; |
|
|
| private initCtx() { |
| if (!this.ctx) { |
| const AudioCtx = window.AudioContext || (window as any).webkitAudioContext; |
| if (AudioCtx) { |
| this.ctx = new AudioCtx(); |
| } |
| } |
| if (this.ctx && this.ctx.state === 'suspended') { |
| this.ctx.resume(); |
| } |
| return this.ctx; |
| } |
|
|
| playPop() { |
| const ctx = this.initCtx(); |
| if (!ctx) return; |
|
|
| const osc = ctx.createOscillator(); |
| const gain = ctx.createGain(); |
|
|
| osc.connect(gain); |
| gain.connect(ctx.destination); |
|
|
| const now = ctx.currentTime; |
| osc.type = 'sine'; |
| |
| |
| osc.frequency.setValueAtTime(800, now); |
| osc.frequency.exponentialRampToValueAtTime(150, now + 0.08); |
|
|
| |
| gain.gain.setValueAtTime(0.3, now); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08); |
|
|
| osc.start(now); |
| osc.stop(now + 0.08); |
| } |
|
|
| playExplosion() { |
| const ctx = this.initCtx(); |
| if (!ctx) return; |
|
|
| const now = ctx.currentTime; |
| |
| |
| const bufferSize = ctx.sampleRate * 0.3; |
| const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); |
| const data = buffer.getChannelData(0); |
| for (let i = 0; i < bufferSize; i++) { |
| data[i] = Math.random() * 2 - 1; |
| } |
|
|
| const noise = ctx.createBufferSource(); |
| noise.buffer = buffer; |
|
|
| |
| const filter = ctx.createBiquadFilter(); |
| filter.type = 'lowpass'; |
| filter.frequency.setValueAtTime(1000, now); |
| filter.frequency.exponentialRampToValueAtTime(10, now + 0.3); |
|
|
| const gain = ctx.createGain(); |
| gain.gain.setValueAtTime(0.5, now); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3); |
|
|
| noise.connect(filter); |
| filter.connect(gain); |
| gain.connect(ctx.destination); |
|
|
| noise.start(now); |
| noise.stop(now + 0.3); |
|
|
| |
| const sub = ctx.createOscillator(); |
| const subGain = ctx.createGain(); |
| sub.type = 'sine'; |
| sub.frequency.setValueAtTime(120, now); |
| sub.frequency.linearRampToValueAtTime(40, now + 0.2); |
|
|
| subGain.gain.setValueAtTime(0.6, now); |
| subGain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); |
|
|
| sub.connect(subGain); |
| subGain.connect(ctx.destination); |
|
|
| sub.start(now); |
| sub.stop(now + 0.25); |
| } |
|
|
| playLaser() { |
| const ctx = this.initCtx(); |
| if (!ctx) return; |
|
|
| const osc = ctx.createOscillator(); |
| const gain = ctx.createGain(); |
|
|
| osc.connect(gain); |
| gain.connect(ctx.destination); |
|
|
| const now = ctx.currentTime; |
| osc.type = 'sawtooth'; |
|
|
| |
| osc.frequency.setValueAtTime(200, now); |
| osc.frequency.exponentialRampToValueAtTime(1500, now + 0.12); |
|
|
| |
| gain.gain.setValueAtTime(0.15, now); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); |
|
|
| osc.start(now); |
| osc.stop(now + 0.12); |
| } |
|
|
| playChime() { |
| const ctx = this.initCtx(); |
| if (!ctx) return; |
|
|
| const now = ctx.currentTime; |
| const notes = [523.25, 659.25, 783.99, 1046.50]; |
|
|
| notes.forEach((freq, index) => { |
| const osc = ctx.createOscillator(); |
| const gain = ctx.createGain(); |
| const delay = index * 0.06; |
|
|
| osc.connect(gain); |
| gain.connect(ctx.destination); |
|
|
| osc.type = 'triangle'; |
| osc.frequency.setValueAtTime(freq, now + delay); |
|
|
| gain.gain.setValueAtTime(0, now + delay); |
| gain.gain.linearRampToValueAtTime(0.25, now + delay + 0.02); |
| gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 0.5); |
|
|
| osc.start(now + delay); |
| osc.stop(now + delay + 0.6); |
| }); |
| } |
|
|
| startAmbient() { |
| const ctx = this.initCtx(); |
| if (!ctx || this.isAmbientPlaying) return; |
|
|
| this.isAmbientPlaying = true; |
| const now = ctx.currentTime; |
|
|
| |
| const freqs = [130.81, 196.00, 261.63, 329.63]; |
|
|
| this.ambientOscillators = freqs.map((freq) => { |
| const osc = ctx.createOscillator(); |
| const gain = ctx.createGain(); |
|
|
| osc.type = 'sine'; |
| osc.frequency.value = freq; |
|
|
| |
| gain.gain.setValueAtTime(0, now); |
| gain.gain.linearRampToValueAtTime(0.025, now + 2.0); |
|
|
| osc.connect(gain); |
| gain.connect(ctx.destination); |
|
|
| osc.start(now); |
|
|
| return { osc, gain }; |
| }); |
| } |
|
|
| stopAmbient() { |
| if (!this.isAmbientPlaying) return; |
| this.isAmbientPlaying = false; |
|
|
| if (!this.ctx) return; |
| const now = this.ctx.currentTime; |
|
|
| this.ambientOscillators.forEach(({ osc, gain }) => { |
| try { |
| gain.gain.setValueAtTime(gain.gain.value, now); |
| gain.gain.linearRampToValueAtTime(0, now + 1.5); |
| setTimeout(() => { |
| try { |
| osc.stop(); |
| } catch {} |
| }, 1600); |
| } catch {} |
| }); |
|
|
| this.ambientOscillators = []; |
| } |
|
|
| toggleAmbient() { |
| if (this.isAmbientPlaying) { |
| this.stopAmbient(); |
| return false; |
| } else { |
| this.startAmbient(); |
| return true; |
| } |
| } |
|
|
| isPlayingAmbient() { |
| return this.isAmbientPlaying; |
| } |
| } |
|
|
| export const gameAudio = new GameAudio(); |
|
|