Sine Waves in HTML5/JavaScript

 

The Sine Wave

Sine waves are curves that plot the graph of the mathematical sine function. They look like this;

 

The wave can be stretched or compressed horizontally or vertically by any amount and it will still be a sine wave.

The sine wave and sine (or sin) function are used in many branches of maths and science and are also very prominent in signal processing. The reason they are prominent in signal processing is that a sine wave signal has only one frequency, which is the least amount of spectral (frequency) information possible.

The graph above is a sine wave signal in the time-domain. The wave in the frequency-domain looks like the graph below.

Sine Waves in the Frequency Domain

Sinusoidal waves

The sine wave starts its cycle at 0 amplitude. The sine wave and all the other waves are shaped like sine waves but do not start at 0 amplitude are called sinusoidal waves. The cosine wave is the most common of these after the sine wave. It is +90° out of ‘phase’ with the sine wave which means that it starts 1/4 cycle earlier. The phase is the point in the oscillation/cycle. When x = 0 on a cosine graph then y = 1.

Using both of these functions we can plot a circle. When you pull a guitar string down and away from the guitar body it produces get sinusoidal waves on both the x and y axises. Both axises are cosine waves so the string usually just travels back and forth, on a 45o angle to the guitar body, on roughly the same path.

 

Sine Waves in Audio Signal Processing

Sound engineers refer to sinusoidal waves as pure tones because they are made up of only 1 frequency. The waves are repeated to generate constant tones. Hear a sine wave below.

It is not the most interesting sound but this will become very useful later in this series.

 

JavaScript Sine Wave Generator

For this post, I created a sine wave generator module which is used in the demonstrations of this post and will also be heavily used later. The mathematical sine function is available in all programming languages with a maths library and even on a scientific calculator.

The mathematical function takes a phase argument in radians and returns values between the maximum and minimum peaks, -1 and +1 inclusive. Since the function is readily available then the generator module does not have to do too much. It does allow for different instances to be created so that many sinusoidal waves can be managed. Each instance tracks the state of its own wave and can return to the caller the amplitude of the next sample using the getSample function. It also has a reset function to set it back to its original state for reuse. Like the sine function, The created the module also returns values between -1 and +1 but the original phase can be offset using an argument in degrees during instantiation.

 

Sine Wave Generator in Action

The image below, and the image of the sine wave from earlier in the article, are not actually images at all. They are HTML5 canvases which use the Sine Wave generator module.

The sine wave is in blue
The cosine wave is in red

If the following demonstrations do not run smoothly then it is because there are in running in Iframes. You can stop them both using the buttons and there are direct links to the pages where they should run much better.

 

Naturally Occurring Sine Saves

Link to Page

Click on the canvas for full screen
This illustrates one on the numerous naturally occurring sinusoidal waves. It should look something like ripples in water. The sine wave affects the height of the water and each point. Sine and Cosine waves are also put to other uses here, though. The shape of each ripple is drawn using a sine and cos. The slider allows for specifying the number of sides the ripple shape should have. Each ripple is the regular form (when the angle is from above) of the shape with the same number of sides as specified by the slider. The largest amount sides/points is 100. When it gets to around 50+ sides, at this size, the shape appears to be a circle. The least number of sides is 3 which draws a triangle with 3 equally spaces points on the perimeter of a circle (when viewing from above).

The angle slider is just for fun.

I also could have also used sin and cos to give more of a realistic feel when changing the viewing angle.

 

Sine Waves and Circles

The demo below uses a sine and a cosine to plot the perimeter of a circle. The sine function is used for the circle y-axis and is then used to plot the sine wave. So continuously moving around on a sine wave on the y-axis and a cosine wave on the x-axis will produce a circle. Applying this Sine function to a graph against time on the y-axis will produce a sine wave. A moving picture is worth 1,000 words per frame so click the start button to see what I mean.

Link to Page

 

The Sound of Sine Waves

Ok, time for me to come clean. The only demonstration on the page that does not actually use the sine wave generator is the audio sine wave. It uses the Web Audio API OscillatorNode, because it’s simple and was easy to implement. There will be plenty opportunity to use the generator to actually make sound later on.

 

Change Log

  • Sine Generator module.
  • Full-Screen module.
  • Sine and Cosine graphs.
  • Ripple demonstration.
  • Sine and cosine relationship with circle demonstration.

Source Code (click to expand)

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

Scripts/com/littleDebugger/ui/fullScreenEvent.js

com.littleDebugger.namespacer.createNamespace("com.littleDebugger.ui");

com.littleDebugger.ui.fullScreenEvent = function (
    elementToFillScreen,
    controlElement,
    enterFullScreenCallback,
    exitFullScreenCallback) {
    // Based on https://www.sitepoint.com/use-html5-full-screen-api/
    controlElement.onclick = function() {
        if (elementToFillScreen.requestFullscreen) {
            elementToFillScreen.requestFullscreen();
        } else if (elementToFillScreen.webkitRequestFullscreen) {
            elementToFillScreen.webkitRequestFullscreen();
        } else if (elementToFillScreen.mozRequestFullScreen) {
            elementToFillScreen.mozRequestFullScreen();
        } else if (elementToFillScreen.msRequestFullscreen) {
            elementToFillScreen.msRequestFullscreen();
        }

        if (typeof (enterFullScreenCallback) !== 'undefined') {
            enterFullScreenCallback();
        }
    };

    //  Based on answers from http://stackoverflow.com/questions/10706070/how-to-detect-when-a-page-exits-fullscreen
    if (typeof (exitFullScreenCallback) !== "undefined") {

        var changeHandler = function () {
            if (document.webkitIsFullScreen === false
                || document.mozFullScreen === false
                || document.msFullscreenElement === null) {
                exitFullScreenCallback();
            }
        };

        document.addEventListener("webkitfullscreenchange", changeHandler, false);
        document.addEventListener("mozfullscreenchange", changeHandler, false);
        document.addEventListener("fullscreenchange", changeHandler, false);
        document.addEventListener("MSFullscreenChange", changeHandler, false);
    }
};

Styles/ripple.css

/* All styles are for full screen. This will be moved with the full screen module */
#container:-webkit-full-screen canvas {
    height: 100%;
    width: 100%;
}

#container:-full-screen canvas {
    height: 100%;
    width: 100%;
}

#canvas { background-color: white; }

#container:-webkit-full-screen {
    height: 100%;
    margin-top: 0;
    padding-top: 0;
    width: 100%;
}

Ripple.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Ripple</title>
    <script src="Scripts/com/littleDebugger/namespacer.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/generator/sineWave.js"></script>
    <script src="Scripts/com/littleDebugger/ui/fullScreenEvent.js"></script>
    <link rel="stylesheet" type="text/css" href="Styles/ripple.css"/>
</head>

<body>
<div id="container">
    <input type="button" value="Stop" id="stopStart"/>
    Angle:<input id="angle" type="range" min="1" max="5" value="4" step="0.1"/> Points:
    <input id="points" type="range" min="3" max="50" value="50" step="1"/>
    <div>
        <canvas id="canvas" height="450" width="1080">
            Browser does not support canvas
        </canvas>
    </div>
</div>

<script src="Scripts/ripple.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"/>
</div>
<canvas id="canvas" height="210" width="1080">
    Browser does not support canvas
</canvas>
<script src="Scripts/sineCircle.js"></script>
</body>

</html>

SineCosineGraph.html

<!DOCTYPE html>
<html>

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

<body>
<canvas id="canvas" height="450" width="1080">
    Browser does not support canvas
</canvas>
<script src="Scripts/sineGraph.js"></script>
<script>drawWave(90,'red');drawWave(0,'blue');</script>
</body>

</html>

SineGraph.html

<!DOCTYPE html>
<html>

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

<body>
<canvas id="canvas" height="450" width="1080">
    Browser does not support canvas
</canvas>
<script src="Scripts/sineGraph.js"></script>
<script>drawWave(0,'blue');</script>
</body>

</html>

SineSound.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>SineWave Sound</title>
</head>

<body>
<div>
    <input type="button" value="Start" id="start"/>
    <input type="button" value="Stop" id="stop"/>
</div>
<script src="Scripts/sineSound.js"></script>
</body>

</html>

Scripts/ripple.js

// js file for Ripple.html

// Assign variables.
var running = true,
    setInt,
    container = document.getElementById("container"),
    canvas = document.getElementById("canvas"),
    points = document.getElementById("points"),
    angle = document.getElementById("angle"),
    stopStart = document.getElementById("stopStart"),
    ctx = canvas.getContext("2d");

// Click canvas enters full screen.
com.littleDebugger.ui.fullScreenEvent(container, canvas);

// Ripple class.
// Instances contain state of a single ripple.
// <sin> 
var Ripple = function (sineWaveGeneratorInstance) {
    this.sineWaveGeneratorInstance = sineWaveGeneratorInstance;
    this.radius = 0;
};

var yCenter = canvas.height / 2;
var xCenter = canvas.width / 2;
var depth = 20;
var visibleWaves = 1;
var maxVisibleWaves = 18;
var spawnFrequency = 18;
var speed = 22;
var ripples = [];
var respawnCount = 0;
var growthSpeed = 3;
var waveGenerator = com.littleDebugger.daw.dsp.generator.sineWave;

// Add the first ripple to array;
ripples.push(new Ripple(waveGenerator(0, speed)));

// Draw a single ripple
// <ripple> The ripple to draw.
var drawRipple = function(ripple) {
    // Get wave generators at the same sample rate as n points for the ripple shapes.
    // This means n getSample calls will be 1 full cycle.
    var drawSin = waveGenerator(0, points.value),
        drawCos = waveGenerator(90, points.value),
        height = ripple.sineWaveGeneratorInstance.getSample(1) * (depth - (depth / angle.value)),
        radius = ripple.radius;

    ctx.beginPath();

    // Draw line to each point on ripple shape.
    var lineTo = function() {
        var x = xCenter + drawSin.getSample(1) * radius;
        var y = yCenter + height + drawCos.getSample(1) * (radius / angle.value);
        ctx.lineTo(x, y);
    };

    for (var i = 0; i < points.value; i++) {
        lineTo();
    }

    lineTo();
    ctx.stroke();
};

// Update canvas.
var updateCanvas = function () {
    respawnCount++;

    // Periodically add new ripple.
    if (respawnCount > spawnFrequency) {
        if (visibleWaves < maxVisibleWaves) {
            visibleWaves++;
        } else {
            ripples.shift();
        }

        ripples.push(new Ripple(waveGenerator(0, speed)));
        respawnCount = 0;
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw all ripples.
    ripples.forEach(function (ripple) {
        ripple.radius += growthSpeed;
        drawRipple(ripple);
    });
};

// Start annimation.
window.onload = function() {
    // set timer to update screen
    setInt = window.setInterval(updateCanvas, 1000 / 24);
};

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

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,
    sampleRate = 10000,
    waveSamples = [],
    running = false,
    setInt;

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

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

var updateScreen = function () {
    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 / 23);
    } else {
        running = false;
        stopStart.value = "Start";
        clearInterval(setInt);
    }
};

drawCircle();

Scripts/sineGraph.js

// js file for SineGraph.html

// Reference the sineWave generator.
var sineWaveGenerator = com.littleDebugger.daw.dsp.generator.sineWave;

// Setup the canvas.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var noneCroppingHeight = canvas.height - 2;

// Set the grid colours.
var axisColor = "Black";
var divideColor = "grey";

// Draw a horizonal line of the canvas
// <y> The point on the Y-axis where the line should be draw.
// <color> Color of the line.
function drawHorizonalLine(y, color) {
    ctx.beginPath();
    ctx.strokeStyle = color;
    ctx.moveTo(0, y);
    ctx.lineTo(canvas.width, y);
    ctx.stroke();
}

// Draw a virtical line of the canvas
// <x> The point on the X-axis where the line should be draw.
// <color> Color of the line.
function drawVirticalLine(x, color) {
    ctx.beginPath();
    ctx.strokeStyle = color;
    ctx.moveTo(x, 0);
    ctx.lineTo(x, canvas.height);
    ctx.stroke();
}

// Draw grid.
drawHorizonalLine(canvas.height / 4, divideColor);
drawHorizonalLine(canvas.height / 4 * 3, divideColor);
drawHorizonalLine(canvas.height / 2, axisColor);

drawVirticalLine(canvas.width / 8, divideColor);
drawVirticalLine(canvas.width / 8 * 2, divideColor);
drawVirticalLine(canvas.width / 8 * 3, divideColor);
drawVirticalLine(canvas.width / 8 * 5, divideColor);
drawVirticalLine(canvas.width / 8 * 6, divideColor);
drawVirticalLine(canvas.width / 8 * 7, divideColor);
drawVirticalLine(canvas.width / 2, axisColor);

// Draw a wave.
// <waveOffset> The offset of the wave.
// <color> Color of the wave.
var drawWave = function (waveOffset, color) {
    ctx.strokeStyle = color;
    var sineWaveGeneratorInstance = sineWaveGenerator(waveOffset, canvas.width);

    ctx.beginPath();

    for (var i = 0; i < canvas.width + 1; i++) {
        // get sine wave samples at 2 cycles per sample rate.
        var y = sineWaveGeneratorInstance.getSample(2);
        ctx.lineTo(i, canvas.height - ((y * noneCroppingHeight / 2) + (canvas.height / 2)));
    }

    ctx.stroke();
};

// So the Iframes don't overflow on mobile devices.
var setDimensions = function(){
    canvas.style.width = (window.innerWidth - 20) + "px";
    var height = window.innerHeight > window.innerWidth
        ? window.innerWidth
        : window.innerHeight;
    canvas.style.height = (height - 30) + "px";
};

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

setDimensions();

Scripts/sineSound.js

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

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

var oscillator;
var showWarning = true;

// Mainly pillaged from https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode
start.onclick = function () {
    if (showWarning) {
        showWarning = false;
        alert("Please set your speaker volume to an appropriate level.");
    }

    oscillator = audioCtx.createOscillator();
    oscillator.type = "sine";
    oscillator.frequency.value = 440;
    oscillator.connect(audioCtx.destination);
    oscillator.start();
};
stop.onclick = function () {
    oscillator.stop();
};

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 (var i = 1; i < namespaces.length; i++) {
        var namespace = namespaces[i];
        if (typeof (currentSpace[namespace]) === "undefined") {
            currentSpace[namespace] = {};
        }

        currentSpace = currentSpace[namespace];
    }
};

Leave a Reply