Previous 19 Next

Audio

The WebAudio api brings advanced audio playback to the web.

src/AudioSystem.ts
export class AudioIdentifier {
  constructor(
    readonly id: number
  ) { }
}

export class AudioSystem {
  private context = new AudioContext
  private identifierToBuffer: { [id: number]: AudioBuffer | undefined } = {}
  private nextAudioIdentifierId = 0
  private globalGainNode = this.context.createGain()
  private dest = this.globalGainNode

  constructor() {
    this.globalGainNode.connect(this.context.destination)
    this.globalGainNode.gain.value = 0.01

    this.unlockContext()
  }

  private unlockContext() {
    if (this.context.state == "suspended") {
      const resumeAudioContext = () => {
        this.context.resume()
        window.removeEventListener('click', resumeAudioContext)
        window.removeEventListener('keydown', resumeAudioContext)
      }
      window.addEventListener('click', resumeAudioContext)
      window.addEventListener('keydown', resumeAudioContext)
    }
  }

  load(uri: string | null, onLoad: (id: AudioIdentifier) => void = () => { }) {
    const id = new AudioIdentifier(this.nextAudioIdentifierId++)
    if (uri == null) return id

    fetch(uri).then(response => {
      return response.arrayBuffer()
    }).then(arrayBuffer => {
      return this.context.decodeAudioData(arrayBuffer)
    }).then(audioBuffer => {
      this.identifierToBuffer[id.id] = audioBuffer
      onLoad(id)
    }).catch((e) => {
      console.log("AudioSystem: Failed to load " + uri, e)
    })
    return id
  }

  play(identifier: AudioIdentifier) {
    const audioBuffer = this.identifierToBuffer[identifier.id]
    if (!audioBuffer) return
    const bufferSource = this.context.createBufferSource()
    bufferSource.buffer = audioBuffer
    bufferSource.connect(this.dest)
    bufferSource.start()
  }
}

Webaudio is a graph based audio player the structure we will use is shown in the image where multiple buffer sources connect to a gain node that we use to regulate the audio of everything that is then connected to the audioContext destination that will then be played by the speakers.

Audio will not play without user gesture

The AudioContext will start in the "suspended" state when it is not created during an user generated event. Therefore we will need to resume it after the user has pressed a key or a mouse button.

Multiple buffer sources connected to the global gain node that is connected to the audioContext.dest which goes to the speaker
Audio system graph

Buffer source

AudioBufferSourceNode sources works as objects that play data from AudioBuffers. So each time we want to play a sound we will create a new AudioBufferSourceNode and then conenct it to our globalGain node. We use a global gain node so we are able to mute all the sound if we want that feature.

src/sounds.ts
const audio = document.createElement("audio")
const supportedFormats = {
  webm: audio.canPlayType("audio/webm"),
  ogg: audio.canPlayType("audio/ogg"),
}

function getSupportedFormat(alternatives: ReadonlyArray<string>) {
  for (const alternative of alternatives) {
    const suffix = alternative.split(".")[1]
    if (suffix == "webm") if (supportedFormats.webm == "maybe" || supportedFormats.webm == "probably") return alternative
    if (suffix == "ogg") if (supportedFormats.webm == "maybe" || supportedFormats.webm == "probably") return alternative
  }
  return null
}

export const playerJump = getSupportedFormat([require("./assets/player_jump.webm"), require("./assets/player_jump.ogg")])
export const playerLand = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const playerDie = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const playerHitWall = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const useKnife = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const usePistol = getSupportedFormat([require("./assets/pistol.webm"), require("./assets/pistol.ogg")])
export const useMachineGun = getSupportedFormat([require("./assets/machine_gun.webm"), require("./assets/machine_gun.ogg")])
export const useHandCannon = getSupportedFormat([require("./assets/hand_cannon.webm"), require("./assets/hand_cannon.ogg")])
export const theme = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const basketPickup = getSupportedFormat([require("./assets/crate_pickup.webm"), require("./assets/crate_pickup.ogg")])
export const killEnemy = getSupportedFormat([require("./assets/bullet_hit.webm"), require("./assets/bullet_hit.ogg")])

Links

Previous 19 Next