Recreating the sounds of the BBC Radiophonic Workshop using the Web Audio API

a prototype by BBC Research & Development

Gunfire Effects Generator


Prior to 1954, gunshot effects in drama were made firing a real gun in front of a microphone. Guns could easily damage microphones, scare performers and had safety implications.

The BBC Designs Department produced a successful electronic alternative which sounded realistic and was capable of being used for a broad range of sounds which could not be made naturally.

Have a play with our simulation of the Gunfire Effects Generator to create your own gun sounds, then keep scrolling to find out how we built it using the Web Audio API



Block diagram of the Electronic Gunfire Effects Generator


We use jQuery, backbone.js and some custom UI elements (namely a knob and a switch) in this application. We make these libraries available to our application using require.js

require(["jquery", "backbone", "knob", "switch"], ($, Backbone, Knob, Switch) ->
  $(document).ready ->

This BufferSource plays a white noise signal read in from a wav file The same effect could be achived using a Javascript Node with the following code to generate random numbers in a range of -1 - 1

class WhiteNoise

  constructor: (context) ->
    self = this
    @context = context
    @node = @context.createJavaScriptNode(1024, 1,2)
    @node.onaudioprocess = (e) -> self.process(e)

  process: (e) ->
    data0 = e.outputBuffer.getChannelData(0)
    data1 = e.outputBuffer.getChannelData(1)
    for i in [0..data0.length-1]
      data0[i] = ((Math.random() * 2) - 1)
      data1[i] = data0[i]
    class Player
      constructor: (@url) ->
        @source = audioContext.createBufferSource()
      play: ->
        if @buffer

Set the buffer of a new AudioBufferSourceNode equal to the samples loaded by loadBuffer

          @source.buffer = @buffer
          @source.loop = true
          @source.start 0

Load the samples from the provided url, decode and store in an instance variable.

      loadBuffer: ->
        self = this

        request = new XMLHttpRequest()'GET', @url, true)
        request.responseType = 'arraybuffer'

Load the decoded sample into the buffer if the request is successful

        request.onload = =>
          onsuccess = (buffer) ->
            self.buffer = buffer

          onerror = -> alert "Could not load #{@url}"

          audioContext.decodeAudioData request.response, onsuccess, onerror


this class uses a gain node to generate a volume ramp at specific times to simulate the attack and release time of the gunshot

    class Envelope
      constructor: () ->
        self = this
        @node = audioContext.createGain()
        @node.gain.value = 0

      addEventToQueue: () ->

Set gain to 0 "now"

        @node.gain.linearRampToValueAtTime(0, audioContext.currentTime);

Attack - ramp to 1 in 0.0001ms

        @node.gain.linearRampToValueAtTime(1, audioContext.currentTime + 0.001);

Decay - ramp to 0.3 over 100ms

        @node.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.101);

Release - ramp down to 0 over 500ms

        @node.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.500);

Now we create and connect the noise to the envelope generators so that they can be triggered by the timing node we also create 4 voices to allow shots to overlap

    audioContext = new webkitAudioContext

Create the noise source

    noise = new Player("/audio/white_noise.wav")

We create 4 instances of the envelope class to provide 4 seperate voices this is necessary as if the raite of fire is very fast the envelope will not have time to reach zero before being triggered again

    voice1 = new Envelope()
    voice2 = new Envelope()
    voice3 = new Envelope()
    voice4 = new Envelope()

Connect the noise source to the 4 voices


connect the voice outputs to a filter to allow a simulation of distance

    filter = audioContext.createBiquadFilter()

set to low pass

    filter.type = 0
    filter.Q.value = 1
    filter.frequency.value = 800

connect the voices to the filter


connect the filter to a master gain node

    gainMaster = audioContext.createGain()
    gainMaster.gain.value = 5

connect the gain node to the output destination


a function to select the next voice and queue the event

    voiceSelect = 0
    fireRate = 1000
    intervalTimer = 0

    schedule = ()  ->
      if voiceSelect > 4 then voiceSelect = 1
      if voiceSelect == 1 then voice1.addEventToQueue()
      if voiceSelect == 2 then voice2.addEventToQueue()
      if voiceSelect == 3 then voice3.addEventToQueue()
      if voiceSelect == 4 then voice4.addEventToQueue()

set up the controls

    volume_knob = new Knob(el: '#volume')
    rate_of_fire_knob = new Knob(el: '#rate-of-fire')
    distance_knob = new Knob(el: '#distance')
    multi_fire_switch = new Switch(el: '#multi-fire')
    trigger = $('#trigger')

set the rapid fire rate

    multi_fire_switch.on('on', =>
      intervalTimer = setInterval (-> schedule()), fireRate

clear the rapid fire function

    multi_fire_switch.on('off', =>

set the master gain value

    volume_knob.on('valueChanged', (v) =>
      gainMaster.gain.value = v * 20

set the filter frequency

    distance_knob.on('valueChanged', (v) =>
      filter.frequency.value = ((v * 800) + 100)

change the rate of fire

    rate_of_fire_knob.on('valueChanged', (v) =>
      fireRate = (v + 1) * 150
      intervalTimer = setInterval (-> schedule()), fireRate


trigger a single shot>schedule())