const MUTED_KEY = 'pirate-side-scroller:muted' const MASTER_VOLUME = 0.5 type ToneOptions = { type: OscillatorType from: number to: number duration: number volume: number } type NoiseOptions = { duration: number volume: number from: number to: number } type AudioGraph = { context: AudioContext master: GainNode noise: AudioBuffer } export class Sfx { muted = localStorage.getItem(MUTED_KEY) === '1' private graph: AudioGraph | null = null /** Browsers only allow audio to start from a user gesture, so call this from one. */ unlock() { if (!this.graph) { const context = new AudioContext() const master = context.createGain() master.gain.value = this.muted ? 0 : MASTER_VOLUME master.connect(context.destination) const noise = context.createBuffer(1, context.sampleRate, context.sampleRate) const data = noise.getChannelData(0) for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1 this.graph = { context, master, noise } } if (this.graph.context.state === 'suspended') void this.graph.context.resume() } toggleMuted() { this.muted = !this.muted localStorage.setItem(MUTED_KEY, this.muted ? '1' : '0') if (this.graph) this.graph.master.gain.value = this.muted ? 0 : MASTER_VOLUME return this.muted } shoot() { this.tone({ type: 'sine', from: 160, to: 50, duration: 0.12, volume: 0.12 }) this.noise({ duration: 0.08, volume: 0.05, from: 900, to: 100 }) } hit() { this.tone({ type: 'triangle', from: 260, to: 140, duration: 0.06, volume: 0.08 }) } explosion() { this.noise({ duration: 0.4, volume: 0.28, from: 2500, to: 150 }) } playerHit() { this.noise({ duration: 0.7, volume: 0.45, from: 1200, to: 60 }) this.tone({ type: 'sawtooth', from: 220, to: 40, duration: 0.6, volume: 0.15 }) } private tone({ type, from, to, duration, volume }: ToneOptions) { if (!this.graph || this.muted) return const { context, master } = this.graph const now = context.currentTime const oscillator = context.createOscillator() oscillator.type = type oscillator.frequency.setValueAtTime(from, now) oscillator.frequency.exponentialRampToValueAtTime(to, now + duration) const gain = context.createGain() gain.gain.setValueAtTime(volume, now) gain.gain.exponentialRampToValueAtTime(0.0001, now + duration) oscillator.connect(gain).connect(master) oscillator.start(now) oscillator.stop(now + duration) } private noise({ duration, volume, from, to }: NoiseOptions) { if (!this.graph || this.muted) return const { context, master, noise } = this.graph const now = context.currentTime const source = context.createBufferSource() source.buffer = noise const filter = context.createBiquadFilter() filter.type = 'lowpass' filter.frequency.setValueAtTime(from, now) filter.frequency.exponentialRampToValueAtTime(to, now + duration) const gain = context.createGain() gain.gain.setValueAtTime(volume, now) gain.gain.exponentialRampToValueAtTime(0.0001, now + duration) source.connect(filter).connect(gain).connect(master) source.start(now) source.stop(now + duration) } }