← Public packages
@kentcdodds/pirate-side-scroller
Left-to-right pirate ship shooter on the open sea, built with Three.js for phone or desktop
src/audio.ts
111 lines · 3.2 KB · TypeScriptconst 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)
}
}