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.

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 and showing one of the more tricky aspects - that of "beat matching" the individual loops so that they are in sync.

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

AudioBufferSourceNode's 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 a 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 load #{@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 exist within.

    context = new AudioContext

Instantiate three separate players with the three loops.

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

Setup the UI

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