Signal Frequency in HTML5/JavaScript

Frequency is a technical term used to describe the pitch of a sound. Pitch relates to our perception of sound but we can measure frequency. When we talk about the pitch or frequency of sound it is usually in the context of human hearing so they are basically synonymous.

It is challenging to explain pitch in words without relating it to real sounds, so I am hoping that you have a concept of it already. Nevertheless, I will explain it anyway. If you are able to hear then I can tell you that a (big) dog barking makes a low pitch/frequency sound. A bird tweeting makes a high pitch/frequency sound. Those particular sounds were intentionally chosen. Speaker systems often consist of several individual drivers (speakers) for handling different frequency ranges. subwoofers are the speakers that handle the low frequencies  (woof-woof). Tweeters are the speakers that handle the highest frequencies (tweet-tweet).

If you had never heard any sound before then it would be hard to understand pitch. Again, I will explain it. The pitch of a sound ranges from low to high. Low pitch sounds are very calm and mellow and high pitch sounds are frantic. This is a very rudimentary description and not always the case. Although, it does make sense that high pitch sounds seem much more frantic and excited because the number of cycles per second made by a wave is what is actually measured when we talk about frequency. The higher the frequency, the more cycles per second. The lower the frequency, the fewer the cycles. Frequency is measured in hertz (Hz) or kilohertz (kHz), which is 1,000 hertz.

Objects vibrating causes fluctuations in the air pressure which in turn causes sound. The speed of the vibration determines the frequency of the sound. I will use an example of a washing machine to explain this. When a washing machine first starts a spin cycle the drum (appropriately named) spins very slowly. You see the clothes lazily flailing about as it turns and it makes a low pitch sound. As the speed of the spinning increases, you can hear the pitch of the sound increase until it is very high. It will probably also get louder and the clothes will start to look like a blur (some of this depends on the age of your washing machine). The pitch of the sound that the machine makes is directly related to the speed of the drum spinning.

 

 Hearing Test

The canvas below demonstrates different audio frequencies in the form of a hearing test. After pressing the start button your speakers will emit individual sine waves at frequencies ranging from 20Hz to 20kHz. Notice the frequency of the first and last sine waves that you hear. Check that you can hear the actual tone, not just a click at the end of the tone. The sine wave stops abruptly after 0.8 seconds, regardless of the position within the cycle, meaning it returns to 0 immediately. This wouldn’t happen naturally and the abrupt change in amplitude causes the click. – This will be discussed further in a later post.

This demonstration is actually using the sine wave generator created for this post and not the Web Audio API component, as in my previous post.

If possible, use wired speakers or headphones. A wireless headset that I used during testing did not handle the high range audio very well. I thought there was something wrong with the sine wave generator so I wasted time trying to fix it until I tried it on other speakers.

 

Are you losing your hearing?

If you are over 25 years of age then the answer to that is probably, yes, but do not be alarmed by this. It happens to the best of us and it is one of the first things in humans to degrade as we age.
 
How did you do no the test? If you couldn’t hear the full range then don’t worry too much, it might not even be your hearing. There are several other possible reasons including the frequency response of your audio equipment.
 
The spectrum of human hearing is said to be from 20Hz to 20kHz (20,000Hz). Apparently, babies can hear this high pitch but most adults can not. There is very little ‘useful’ information for us in frequencies above about 9 or 10kHz. – If you can hear frequencies up to there then you are fine.

If you can’t hear the low 20Hz signal then it’s very likely your speakers that can not reproduce such low frequencies.

On that hearing test, I can hear up to between about 13,000Hz and 15,000Hz depending on the speakers that I use and the loudness.

 

Hearing Degradation in Adluts

The chart above shows part of the spectrum of human hearing and how that degrades as we age. The chart is from this page. I can’t find the original source of this data but the chart looks similar to others that I have seen before.

 

Frequency in the Time-Domain

The demonstration below shows how frequency affects a sine wave in the time-domain. The scale shows a distance of 1/100 second. I haven’t gone crazy and I do know that distances can not be measured in seconds. This is the distance that sound travels in 1/100 second. The speed of sound is around 340m/second so the distance represents 3.4m.

 

 
As the frequency increases, the wave is compressed horizontally. As the frequency decreases, the wave contracts. At 100Hz (cycles per second) then the wave completes 1 cycle in the distance on the scale. – If it completes 100 cycles in 1 second then it completes 1 cycle in 1/100th a second. At 50Hz it completes 1/2 a cycle and at 1,000Hz it completes 10.

A single cycle at 100Hz takes 10 seconds so the wave is generated 1,000 times slower than sound.

 

Frequency in the Frequency-Domain

The graph below shows a sine wave in the frequency domain. As the frequency increases, the vertical red bar will move towards the right and as the frequency decreases, it will move towards the left. Since the signal amplitude is not changing then the bar will stay the same height.

Sine wave in the Frequency-Domain

There is room for improvement in my frequency-domain graphs so I will code a frequency-domain visualiser for a post soon.

 

Change Log

  • Hearing test created
  • Sine wave circle demonstration changed to support a range of frequencies

 

Source Code (click to expand)

HearingTest.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>SineWave Hearing Test</title>
    <script src="Scripts/com/littleDebugger/namespacer.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/generator/sineWave.js"></script>
</head>

<body>
    <div>
        <input type="button" value="Start" id="start"/>
        <input type="button" value="Stop" id="stop"/>
        <div>
            <canvas id="frequencyHz" width="600" height="230">
                Browser does not support the HTML5 canvas.
            </canvas>
        </div>
    </div>
    <script src="Scripts/hearingTest.js"></script>
</body>

</html>

SineCircle.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>SineWave</title>
    <script src="Scripts/com/littleDebugger/namespacer.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/generator/sineWave.js"></script>
</head>

<body>
    <div>
        <input type="button" value="Start" id="stopStart"/>
        <input type="range" id="frequency" min="50" max="2000" value="100" step="25" style="width:50%"/>
        <span id="freqDisplay"><span>
    </div>
    <canvas id="canvas" height="210" width="680">
        Browser does not support the HTML5 canvas.
    </canvas>
    <div>
        <canvas id="canvasScale" height="30" width="680">
        </canvas>
    </div>
    <script src="Scripts/sineCircle.js"></script>
</body>

</html>

Scripts/HearingTest.js

// js file for SineSound.html
var start = document.getElementById("start");
var stop = document.getElementById("stop");

// Added last minute because I forgot that IE does not support Web Audio API.
if (window.AudioContext == null && window.webkitAudioContext == null) {
    start.onclick = function () {
        alert("Browser does not support Web Audio API.");
    };
}

var audioCtx = new(window.AudioContext || window.webkitAudioContext)();

var showWarning = true;
var scriptNode = audioCtx.createScriptProcessor(2048, 1, 1);

var sineWaveGenerator = com.littleDebugger.daw.dsp.generator.sineWave;

var sineWaveGeneratorInstance = sineWaveGenerator(0, audioCtx.sampleRate);

var canvas = document.getElementById('frequencyHz');
var canvasCtx = canvas.getContext("2d");

console.log("Sample Rate:" + audioCtx.sampleRate);

var frequencyValue = 0;

var timeout;

// These will be the frequencies included in the test. 
// Also the adjustment amplitudes. This is roughly taken from the equal loudness contour.
var frequencies = [{
        freq: 15,
        amp: 0.8
    },
    {
        freq: 20,
        amp: 0.8
    },
    {
        freq: 30,
        amp: 0.2
    },
    {
        freq: 40,
        amp: 0.1111
    },
    {
        freq: 50,
        amp: 0.05555
    },
    {
        freq: 65,
        amp: 0.03703
    },
    {
        freq: 85,
        amp: 0.18518
    },
    {
        freq: 100,
        amp: 0.12345
    },
    {
        freq: 150,
        amp: 0.06172
    },
    {
        freq: 200,
        amp: 0.04115
    },
    {
        freq: 500,
        amp: 0.03822
    },
    {
        freq: 1000,
        amp: 0.04115
    },
    {
        freq: 2000,
        amp: 0.03822
    },
    {
        freq: 3000,
        amp: 0.03200
    },
    {
        freq: 4000,
        amp: 0.03200
    },
    {
        freq: 5000,
        amp: 0.03822
    },
    {
        freq: 6000,
        amp: 0.04115
    },
    {
        freq: 7000,
        amp: 0.04600
    },
    {
        freq: 8000,
        amp: 0.053
    },
    {
        freq: 9000,
        amp: 0.059
    },
    {
        freq: 10000,
        amp: 0.064
    },
    {
        freq: 11000,
        amp: 0.068
    },
    {
        freq: 12000,
        amp: 0.072
    },
    {
        freq: 13000,
        amp: 0.074
    },
    {
        freq: 14000,
        amp: 0.076
    },
    {
        freq: 15000,
        amp: 0.079
    },
    {
        freq: 16000,
        amp: 0.082
    },
    {
        freq: 17000,
        amp: 0.086
    },
    {
        freq: 18000,
        amp: 0.087
    },
    {
        freq: 19000,
        amp: 0.09
    },
    {
        freq: 20000,
        amp: 0.12345
    }
];

// Start hearing test click event.
start.onclick = function () {
    if (showWarning) {
        showWarning = false;
        alert("Set your speakers/headphones to a low volume. You can retry the test if is too quiet.");
    }

    frequencyValue = 0;

    // # New variables to handle fade out of tone to stop clicking and the end of each tone.
    var maxAmp = 1;
    var amp = maxAmp;
    var fadeout = false;
    var fadeoutValue = 0.01;
    var samplesToProcessBeforeAmpAdjustment = 100;
    var adjustmentFactor = 1;
    // #

    // ScriptNpde. There will be no input as no source is attached to it.
    // The output will be wave from the sine wave generator.
    scriptNode.onaudioprocess = function (audioProcessingEvent) {
        var outputBuffer = audioProcessingEvent.outputBuffer;
        var output = outputBuffer.getChannelData(0);

        var sample = 0;
        do {
            // More for fadeout of tone.
            if (fadeout) {
                amp -= fadeoutValue;
                if (amp <= 0) {
                    amp = 0;
                    fadeout = false;
                }
            }

            var samplesToProcess = Math.min(outputBuffer.length - sample, samplesToProcessBeforeAmpAdjustment);
            for (var ii = 0; ii < samplesToProcess; ii++) {
                output[sample + ii] = sineWaveGeneratorInstance.getSample(frequencyValue) * amp * adjustmentFactor;
            }

            sample += samplesToProcess;
        } while (sample < outputBuffer.length);
    };

    scriptNode.connect(audioCtx.destination);

    var toneLength = 800;
    var silenceLength = 1000;

    // Add the frequency to the canvas.
    var updateCanvas = function (text, colour) {
        canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
        canvasCtx.textAlign = "center";
        canvasCtx.fillStyle = colour;
        canvasCtx.font = "82px Arial";
        canvasCtx.fillText(text, canvas.width / 2, canvas.height / 2);
    };

    // Schedule tone to play. i is the index of the frequencies array to play.
    var scheduleTone = function (i) {
        timeout = setTimeout(function () {
            frequencyValue = frequencies[i].freq;
            adjustmentFactor = frequencies[i].amp;
            sineWaveGeneratorInstance.reset();
            amp = maxAmp;
            updateCanvas(frequencyValue.toLocaleString('en-UK') + "Hz", "red");
            // Scedule stop the tone.
            timeout = setTimeout(function () {
                updateCanvas(frequencyValue.toLocaleString('en-UK') + "Hz", "black");
                // just set the frequency to 0 rather than try to stop it and start it again for the next tone.
                fadeout = true;
                i++;
                if (i < frequencies.length) {
                    scheduleTone(i);
                } else {
                    stop.onclick();
                }
            }, toneLength);
        }, silenceLength);
    };

    scheduleTone(0);
};

// Stop hearing test click event.
stop.onclick = function () {
    clearTimeout(timeout);
    scriptNode.disconnect(audioCtx.destination);
};

// This displays the page better when in a blog post Iframe.
// It will be modularised or replaced (hopefully with CSS only).
var setDimensions = function (el) {
    el.style.width = (window.innerWidth - 15) + "px";
    el.style.height = el.style.width * (el.height / el.width) + "px";
};

window.onresize = function () {
    setDimensions(canvas);
};

window.onresize();

Scripts/sineCircle.js

// js file for SineCircle.html

// Variable assignments.
var canvas = document.getElementById("canvas"),
    ctx = canvas.getContext("2d"),
    stopStart = document.getElementById("stopStart"),
    waveGenerator = com.littleDebugger.daw.dsp.generator.sineWave;

var yCenter = canvas.height / 2,
    xCenter = 100,
    xStartOfWave = 300,
    radius = 99,
    freq = 50,
    scaleWidth = 200,
    sampleRate = scaleWidth * 100,
    waveSamples = [],
    running = false,
    setInt;

// Get wave generator instances.
var sineWaveGeneratorInstance = waveGenerator(0, sampleRate);
var cosineWaveGeneratorInstance = waveGenerator(90, sampleRate);

// Draw circle.
var drawCircle = function () {
    ctx.beginPath();
    ctx.arc(xCenter, yCenter, radius, 0, 2 * Math.PI, false);
    ctx.fillStyle = "green";
    ctx.fill();
    ctx.stroke();
};

var canvasScale = document.getElementById('canvasScale');
var scaleCtx = canvasScale.getContext("2d");

// Draw the scale on a different canvas so it doesnt have to be refreshed.
scaleCtx.lineWidth = 3;
scaleCtx.moveTo(xStartOfWave, 0);
scaleCtx.lineTo(xStartOfWave, 30);
scaleCtx.moveTo(xStartOfWave, 15);
scaleCtx.lineTo(xStartOfWave + scaleWidth, 15);
scaleCtx.moveTo(xStartOfWave + scaleWidth, 0);
scaleCtx.lineTo(xStartOfWave + scaleWidth, 30);
scaleCtx.stroke();
scaleCtx.textAlign = "center";
scaleCtx.fillText("1/100 second", xStartOfWave + (scaleWidth / 2), 10);

var freqency = document.getElementById('frequency');
var freqDisplay = document.getElementById('freqDisplay');

var updateScreen = function () {
    freq = frequency.value;
    freqDisplay.innerHTML = freq + " Hz";
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // Get sine and cosine graphs at 1 cycle per sample rate.
    var x = xCenter + (cosineWaveGeneratorInstance.getSample(freq) * radius),
        // Subtraction because the 0, 0 on the canvas is top left.
        y = yCenter - (sineWaveGeneratorInstance.getSample(freq) * radius);

    if (waveSamples.length > canvas.width - xStartOfWave) {
        // Remove the last sample from wave sample if it is pushed off the canvas.
        waveSamples.shift();
    }

    // Add the new sample to wave sample array.
    waveSamples.push(y);

    drawCircle();

    // Redraw wave.
    ctx.beginPath();
    waveSamples.forEach(function (waveSample, i) {
        ctx.lineTo(xStartOfWave - i + waveSamples.length, waveSample);
    });

    ctx.stroke();

    // draw radius in circle and line to sine wave
    ctx.beginPath();
    ctx.moveTo(xCenter, yCenter);
    ctx.lineTo(x, y);
    ctx.lineTo(xStartOfWave + 1, y);
    ctx.stroke();
};

// Wire up stop/start button.
stopStart.onclick = function () {
    if (!running) {
        running = true;
        stopStart.value = "Stop";
        setInt = window.setInterval(updateScreen, 1000 / 20);
    } else {
        running = false;
        stopStart.value = "Start";
        clearInterval(setInt);
    }
};

drawCircle();

// This displays the page better when in a blog post Iframe.
// It will be modularised or replaced (hopefully with CSS only).
var setDimensions = function (el) {
    el.style.width = (window.innerWidth - 15) + "px";
    el.style.height = el.style.width * (el.height / el.width) + "px";
};

window.onresize = function () {
    setDimensions(canvas);
    setDimensions(canvasScale);
};

window.onresize();

Scripts/com/littleDebugger/namespacer.js

// Simple pattern used for namespacing in JavaScript.
// The module pattern will be used to group related functionality. 
// Modules are not yet supported in the main browswers natively.

// I do not plan to use any 3rd party libraries. 
// This may mean reinventing the wheel in some cases but I do not want anything 
// going on under the hood which I am not aware of.
// I will 'borrow' functions and snippets where required. This will be referenced.

if (typeof (com) === 'undefined') {
    com = {};
}

if (typeof (com.littleDebugger) === 'undefined') {
    com.littleDebugger = {};
}

if (typeof (com.littleDebugger.namespacer) === 'undefined') {
    com.littleDebugger.namespacer = {};
}

// Creates a namespace in the global space.
// <namespaceText> . seperated namespace to be created.
com.littleDebugger.namespacer.createNamespace = function (namespaceText) {
    var namespaces = namespaceText.split('.');
    if (typeof (window[namespaces[0]]) === 'undefined') {
        window[namespaces[0]] = {};
    }

    var currentSpace = window[namespaces[0]];

    for (i = 1; i < namespaces.length; i++) {
        var namespace = namespaces[i];
        if (typeof (currentSpace[namespace]) === 'undefined') {
            currentSpace[namespace] = {};
        }

        currentSpace = currentSpace[namespace];
    };
};

Scripts/com/littleDebugger/daw/dsp/generator/sineWave.js

// The sine (sinusoidal) wave generator.
com.littleDebugger.namespacer.createNamespace("com.littleDebugger.daw.dsp.generator.sineWave");

// 'Constructor'
// <offset> The offset of the initial phase in degrees. - Starting from 9 o'clock.
// <sampleRate> The sample rate of the audio player. This is required for correct frequency waves.
com.littleDebugger.daw.dsp.generator.sineWave = function (offset, sampleRate) {
    var circleRadians = 2 * Math.PI;
    var circleDegrees = 360;

    var that = {};
    var phase;

    // Reset the state of the generator so it can be reused.
    that.reset = function () {
        phase = circleRadians * (offset / circleDegrees);
    };

    // Get the next sample in the cycle.
    // <freqency> The frequency of the wave.
    that.getSample = function (frequency) {
        var nextValue = Math.sin(phase);
        phase += circleRadians * (frequency / sampleRate);
        if (phase > circleRadians) {
            phase = phase - circleRadians;
        }

        return nextValue;
    };

    that.reset();

    return that;
};

[insert_php]

Leave a Reply