Skip to content

src/audio.ts

111 lines · 3.2 KB · TypeScript
Raw
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)
  }
}