Output ChannelOutput Channel

Recreating phone sounds with Web Audio

November 23, 2015

If you're new to sound design, recreating the sounds associated with telephones is a great place to start. This is because they are relatively simple to understand and can be implemented in Web Audio with just a few lines of code, but that's not to say they're completely trivial.

In this article we'll be recreating the following sounds:

  • Dial tone
  • DTMF tones
  • Ringback tone

There are some slight differences in how these sound from country to country but I'll be focusing on the UK versions. However, you should be able to use the techniques presented here to recreate any tone from any country.

Dial Tone

This is the sound you hear when you pick up a landline phone. In the UK, it consists of 2 continuous sine waves at frequencies of 350 Hz and 440 Hz. So, to get things started, we'll create an Audio Context.

var context = new AudioContext();

Now we're going to create a Tone object that will keep all of our functions and properties nicely kept together. We'll pass in our Audio Context to be set as a property of the object. We'll also have a property called status, which, if it equals 1, indicates the tone is playing, otherwise, if it is 0 the dial tone will be off.

function Tone(context) {
    this.context = context;
    this.status = 0;
}

Next we'll write a setup method that creates the 2 oscillator nodes and sets their frequencies to 350 Hz and 440 Hz. Even though we could connect these directly to the output, I prefer to connect them via a gain node, so I can adjust the volume more easily. (It will also prove useful later on, when we aren't dealing with continuous tones.) For extra realness, I'm going to connect in a low pass filter too, which cuts out any high frequencies, just like a real phone line would. With that in mind, I'll create a gain node and filter in this method too, connect both oscillators to the gain, and the gain to the filter.

Tone.prototype.setup = function(){
    this.osc1 = context.createOscillator();
    this.osc2 = context.createOscillator();
    this.osc1.frequency.value = 350;
    this.osc2.frequency.value = 440;

    this.gainNode = this.context.createGain();
    this.gainNode.gain.value = 0.25;

    this.filter = this.context.createBiquadFilter();
    this.filter.type = "lowpass";
    this.filter.frequency = 8000;

    this.osc1.connect(this.gainNode);
    this.osc2.connect(this.gainNode);

    this.gainNode.connect(this.filter);
    this.filter.connect(context.destination);
}

Now we need to think about how we can start and stop the tone. We'll create a start() and stop() method to allow us to do this.

Tone.prototype.start = function(){
    this.setup();
    this.osc1.start(0);
    this.osc2.start(0);
    this.status = 1;
    }

Tone.prototype.stop = function(){
    this.osc1.stop(0);
    this.osc2.stop(0);
    this.status = 0;
}

Next, we'll create a basic HTML user interface, consisting of a single button. We also need to create an instance of the Tone object. Next, we need to add an event listener to the button. When the button is pressed, our event listener code will determine whether we need to call the start or stop method.

<button id='js-toggle-dial-tone'>Toggle Dial Tone</button>

<script>
var context = new AudioContext();
var dialTone = new Tone(context);
$("#js-toggle-dial-tone").click(function(e){
    e.preventDefault();
    if (dialTone.status === 0){
        // The tone is currently off, so we need to turn it on.
        dialTone.start();
    } else {
        // The tone is currently on, so we need to turn it off.
        dialTone.stop();
    }
});
</script>

And that completes dial tone. You can play around with a working version of this in the CodePen embed below.

See the Pen Dial Tone in Web Audio by Ed Ball (@edball) on CodePen.

DTMF Frequencies

Next up, we'll begin work on the DTMF (Dual Tone Multi Frequency) tones. These are the sounds you hear when you press numbers on the keypad, and, like the dial tone, are made up of pairs of sine waves. The exact frequencies used for each number are displayed in the table below.

1209Hz 1336Hz 1477Hz
697Hz 1 2 3
770Hz 4 5 6
852Hz 7 8 9
941Hz * 0 #

When it comes to actually implementing these in code, we have already done virtually all of the hard work when we created the dial tone. We just need a way to easily specify the 2 frequencies instead of having them hard coded into the setup method.

To do this, we'll modify the Tone object to accept 2 new parameters freq1 and freq2. These will become properties of the object.

function Tone(context, freq1, freq2) {
    this.context = context;
    this.status = 0;
    this.freq1 = freq1;
    this.freq2 = freq2;
}

Then, we'll modify the setup method to use the 2 new properties, instead of the hard coded frequencies we had before.

Tone.prototype.setup = function(){
    this.osc1 = context.createOscillator();
    this.osc2 = context.createOscillator();
    this.osc1.frequency.value = this.freq1;
    this.osc2.frequency.value = this.freq2;

    this.gainNode = this.context.createGain();
    this.gainNode.gain.value = 0.25;

    this.filter = this.context.createBiquadFilter();
    this.filter.type = "lowpass";
    this.filter.frequency = 8000;

    this.osc1.connect(this.gainNode);
    this.osc2.connect(this.gainNode);

    this.gainNode.connect(this.filter);
    this.filter.connect(context.destination);
}

Now, once again, we need to create some sort of HTML user interface to work with our newly modified object. We're going to create 12 buttons, one for each DTMF tone.

<ul class='js-dtmf-interface'>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    <li>*</li>
    <li>0</li>
    <li>#</li>
</ul>

In terms of functionality, we want to hear a tone when we click/tap and hold on one of these elements, then to switch off when we release our mouse/finger, mimicking a real dial pad.

In our javascript we're going to have an object, dtmfFrequencies which contains all the 12 possible frequency combinations. When we detect a click, we'll be able to figure out which frequencies to play, by looking it up in this object. (I got the frequencies listed here from Wikipedia.)

var dtmfFrequencies = {
    "1": {f1: 697, f2: 1209},
    "2": {f1: 697, f2: 1336},
    "3": {f1: 697, f2: 1477},
    "4": {f1: 770, f2: 1209},
    "5": {f1: 770, f2: 1336},
    "6": {f1: 770, f2: 1477},
    "7": {f1: 852, f2: 1209},
    "8": {f1: 852, f2: 1336},
    "9": {f1: 852, f2: 1477},
    "*": {f1: 941, f2: 1209},
    "0": {f1: 941, f2: 1336},
    "#": {f1: 941, f2: 1477}
}

Instead of creating 12 tone instances, we're just going to create one, and change the frequencies as required.

// Create a new Tone instance. (We've initialised it with 
// frequencies of 350 and 440 but it doesn't really matter
// what we choose because we will be changing them in the 
// function below)
var dtmf = new Tone(context, 350, 440);

$(".js-dtmf-interface li").on("mousedown touchstart", function(e){
    e.preventDefault();

    var keyPressed = $(this).html(); // this gets the number/character that was pressed
    var frequencyPair = dtmfFrequencies[keyPressed]; // this looks up which frequency pair we need

    // this sets the freq1 and freq2 properties
    dtmf.freq1 = frequencyPair.f1;
    dtmf.freq2 = frequencyPair.f2;

    if (dtmf.status == 0){
        dtmf.start();
    }
});

// we detect the mouseup event on the window tag as opposed to the li
// tag because otherwise if we release the mouse when not over a button,
// the tone will remain playing
$(window).on("mouseup touchend", function(){
    if (typeof dtmf !== "undefined" && dtmf.status){
        dtmf.stop();
    }
});

That completes the DTMF part of this article. You can see a working version in the codepen embed below, where I've also added a little bit of CSS to the interface.

See the Pen DTMF Tones in Web Audio by Ed Ball (@edball) on CodePen.

Ringback tone

Finally, we'll look at how to create the ringback tone. This is the sound you hear when the phone of the person you're calling is ringing. Once again, it is a tone made up of 2 sine waves (400 Hz and 450 Hz in the UK). However, unlike in previous examples, the tone is not continuous. In the UK, it has a pattern or "cadence" of 0.4s on, 0.2s off, 0.4s on, 2s off. The total length of this cycle is 3 seconds.

We can deal with this by leaving the oscillators running, but turning our gain node to 0 at the required times. There are a number of ways to do this, such as utilising some sort of timing system like the Web Audio Api Clock. However, because the pattern is quite a simple one, I'd like to use a low frequency oscillator (LFO).

If the pattern went on and off in equal times, say on for 0.5s then off for 0.5s etc, we could simply use a square wave as our LFO with frequency 1Hz to do this. However, our pattern is not quite as simple as that, so we'll have to be a bit more clever.

We're going to create our waveform data manually, and place it in a buffer (a small snippet of sound), which we'll loop continuously. The buffer needs to be 3 seconds long, and contain the following waveform.

To create the buffer, we'll need to use Web Audio's createBuffer method. We'll create and populate our buffer in a new method of the Tone object called createRingerLFO.

Tone.prototype.createRingerLFO = function() {
    // Create an empty 3 second mono buffer at the
    // sample rate of the AudioContext
    var channels = 1;
    var sampleRate = this.context.sampleRate;
    var frameCount = sampleRate * 3;
    var arrayBuffer = this.context.createBuffer(channels, frameCount, sampleRate);

    // getChannelData allows us to access and edit the buffer data and change.
    var bufferData = arrayBuffer.getChannelData(0);
    for (var i = 0; i < frameCount; i++) {
        // if the sample lies between 0 and 0.4 seconds, or 0.6 and 1 second, we want it to be on.
        if ((i/sampleRate > 0 && i/sampleRate < 0.4) || (i/sampleRate > 0.6 && i/sampleRate < 1.0)){
            bufferData[i] = 0.25;
        }
    }

    this.ringerLFOBuffer = arrayBuffer;
}

Now that we've created our audio data and stored it in a buffer, we can load it into a buffer source node to complete the LFO. Once that's done, we need to connect the LFO to the gain param of our gain node, and and start the LFO playing to get the desired effect.

Tone.prototype.startRinging = function(){
    this.start();
    // set our gain node to 0, because the LFO is calibrated to this level
    this.gainNode.gain.value = 0;
    this.status = 1;

    this.createRingerLFO();

    this.ringerLFOSource = this.context.createBufferSource();
    this.ringerLFOSource.buffer = this.ringerLFOBuffer;
    this.ringerLFOSource.loop = true;
    // connect the ringerLFOSource to the gain Node audio param
    this.ringerLFOSource.connect(this.gainNode.gain);
    this.ringerLFOSource.start(0);
}

We'll also create a stopRinging method that calls the stop method, and also stops the LFO from playing.

Tone.prototype.stopRinging = function(){
    this.stop();
    this.ringerLFOSource.stop(0);
}	

Finally, as was the case with the dial tone, we'll create a toggle button, an instance of the new object, and set up an event listener which will easily allow us to start and stop the ringing.

<button id='js-toggle-ringback-tone'>Toggle Dial Tone</button>

<script>
    var context = new AudioContext();
    var ringbackTone = new Tone(context, 400, 450);

    $("#js-toggle-ringback-tone").click(function(e){
        e.preventDefault();
        if (ringbackTone.status === 0){
            ringbackTone.startRinging();
        } else {
            ringbackTone.stopRinging();
        }
    });
</script>

The CodePen below demonstrates the finished demo.

See the Pen Ringback tone in Web Audio by Ed Ball (@edball) on CodePen.

If these telephone sounds have sparked your imagination, you may be interested in this Spotify playlist I'm gradually compiling of telephone sounds in popular music.

Header image: Phone Booth by Alexander Rabb