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

a prototype by BBC Research & Development

Tape Loops

Info

Early radiophonic work was made using tape machines left in studios overnight. Tape machines allowed multiple sound manipulations: you could speed up sounds, slow them down, reverse them, create loops or echo effects.

Fragments of recorded sounds were manipulated and then spliced together to form compositions and tape loops ­ sometimes several metres long.

In this video you can watch Delia Derbyshire triggering tape loops in time to create a piece of music.

Now you can try to build up a composition with our simulation of the Workshop's tape machines. Keep scrolling to find out how we built them using the Web Audio API.

Demo

Code

tapeloops.coffee

Tape Loops

In the early days of the Radiophonics Workshop pieces of music were painstakingly composed a note at a time by recording and splicing together pieces of tapes in loops. In this video you can see Delia Derbyshire explaining the process.

Tape machine

This demo is a simulation of three tape machines with variable speed controls that have to be triggered in time to build up a simple composition. It is a simple example of the use of the Web Audio API's AudioBufferSourceNode.

This application is a simulation of three tape loop machines with variable speed controls using the Web Audio API.

Preamble

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 ->

Player

AudioBufferSourceNodes work in a slightly counter-intuitive way for performance reasons. Once a sound has been triggered with a start message, it cannot be re-triggered. The FAQ on HTML5 Rocks discusses the reasons behind this. To make the sample player more natural to work with in this application we wrap an AudioBufferSourceNode in a custom class.

    class Player
      constructor: (@url) ->
        this.loadBuffer()

Set the default playback speed.

        this.setBaseSpeed(1)
        this.setSpeedFine(1)

      play: ->
        if @buffer

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

          @source = context.createBufferSource()
          @source.buffer = @buffer
          @source.connect context.destination
          @source.loop = true
          this.setSpeed()

Trigger the source to play immediately.

          @source.start 0

      stop: ->
        if @buffer && @source

Stop the sample playback immediately.

          @source.stop 0

      setBaseSpeed: (speed) ->
        @base_speed = speed
        this.setSpeed()

      setSpeedFine: (speed) ->
        @fine_speed = speed
        this.setSpeed()

The playback speed is a combination of the "base speed" (normal or double speed playback) and a "fine speed" control.

      setSpeed: ->
        if @source
          @source.playbackRate.value = @base_speed * @fine_speed

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

      loadBuffer: ->
        self = this

        request = new XMLHttpRequest()
        request.open('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 decode #{self.url}"

          context.decodeAudioData request.response, onsuccess, onerror

        request.send()

TapeMachine

A class that, given a DOM element el and a Player, simulates a tape machine.

    class TapeMachine
      constructor: (@el, @player) ->
        @setupSpeedToggle()
        @setupFineSpeed()
        @setupPlayStop()

The tape speed switch toggles the base speed of the player between normal speed half speed.

      setupSpeedToggle: () ->

Bind a Switch to the double-speed element within the current el.

        speed_toggle = new Switch(el: $(@el).find('.speed'), states: ['normal', 'half'])

The switch fires normal and half events. We bind these events to the setBaseSpeed method of the player.

        speed_toggle.on('normal', => @player.setBaseSpeed(1))
        speed_toggle.on('half', => @player.setBaseSpeed(0.5))

Attach a Knob to the fine speed control to vary the playback speed by ±3%.

      setupFineSpeed: () ->
        fine_speed_control = new Knob(
          el: $(@el).find('.fine-speed')
          initial_value: 1
          valueMin: 0.97
          valueMax: 1.03
        )

The Knob triggers valueChanged events when turned. We send the value to the setSpeedFine method on the player.

        fine_speed_control.on('valueChanged', (v) =>
          @player.setSpeedFine(v)
        )

A switch to toggle the play state of the machine.

      setupPlayStop: () ->
        play_stop_control = new Switch(el: $(@el).find('.play'))
        play_stop_control.on('on', => @player.play())
        play_stop_control.on('off', => @player.stop())

Application Setup

Create an audio context for our application to use.

    context = new AudioContext

Instantiate three separate players with the three loops.

    player1 = new Player('/audio/delia_loop_01.wav')
    player2 = new Player('/audio/delia_loop_02.wav')
    player3 = new Player('/audio/delia_loop_03.wav')

Set up the UI.

    new TapeMachine('#machine1', player1)
    new TapeMachine('#machine2', player2)
    new TapeMachine('#machine3', player3)
  )