Signal Amplitude, Amplification and Attenuation in HTML5/JavaScript

 

Amplitude

The amplitude of a signal is the power at any given time. In the context of an electric signal, the top of the graph is a positive voltage and the bottom negative. In the context of a sound in air, the top is high pressure and the bottom low pressure.

Amplitude was briefly mentioned in this post about sine waves. Let’s look at one of the graphs from that post again.

The graph shows a sine wave in the time domain so the Y-axis is the time the X-axis is amplitude. We can find the amplitude at any point by following the wave horizontally until reaching that specific time and then checking where this point is in line with the Y-axis.

This graph does not have a scale on the X-axis. It just shows a sine wave completing 2 full cycles.  The Y-axis ranges from -1 to +1 because that is the range of results from the sin function, but it also doesn’t have an actual unit.

The graph is divided into 8 sections horizontally. The first 4 and the last 4 sections are identical because the wave is repeated. For each of these cycles, you can see that the maximum positive amplitude on the curve is on the first vertical line. The curve then intercepts the X-axis on the next vertical line (in the middle of the cycle). The minimum amplitude is then on the 3rd vertical line.
 

Amplification and Attenuation

Amplification is an increase in power applied to a signal constantly, rather than the power at a particular time. Amplifying a signal affects the amplitude.

An example of this power increase is when pressing up on the volume rocker on the side of your phone. (I managed to keep modern and not write about the volume dial on a Hi-Fi). The power of the signal from the source stays the same but in the context of the output, it has more power – thus sounding louder.

Attenuation is the opposite of amplification, the process of decreasing the power of a signal.
 
Electronics/signal processing domains use these words but in the context of sound engineering other words are more frequently used. Gain, volume or loudness is more common when talking about the actual sound. Amplification is still a common word because amplifiers (or amps) are the piece of kit used to make electric guitars, car stereos, and Hi-Fis loud.
 

Amplification and Attenuation in Sound Engineering

The image below shows a mixing desk (also called mixing console or just mixer). It has many sliders on it which are known as faders. The faders control the gain for each sound, or group of sounds, within a mix e.g. guitar part, piano, etc. When mixing a song, the engineer adjusts the levels until the mix sounds how they want it to. Reducing the volume of the background sounds and increasing the volume of the foreground sounds, like the vocals, to give them more presence.

These gain controls can attenuate or amplify the sound. There is a zero line near the top of the range of the fader where the signal in the mix is the same power as it is received by the mixer. Above that line, the signal will be amplified and below the line, it will be attenuated. The point at the very bottom of the fader attenuates the signal to nothing so the signal is not at all present in the mix.

The faders are the black things starting from just left of middle bottom going upwards at around 1 o’clock. Each of fader can be pushed back and forth along the black line (at around 10 o’clock). – That is actually away from and towards the engineer when they are looking at the desk.
 
Gain/amplification can take place at any point after generation of a signal until the time it is converted into sound. Each of these gain controls on the mixer is the last step before it is all ‘mixed’ together, meaning just before it is converted into sound (there are exceptions here but this explanation will do for now).
 

How does it work?

There are many different types of amplifiers including valve and magnetic which have different implementations all but serve the purpose of receiving a signal and increasing its power.

Magnetic amplifiers are like transformers which are found in many other places in electronics. A very common place is in the wall adaptor of electrical equipment. The voltage from the mains is 100v/230v/240v (depending on where you are), which is far more than required by most consumer electronics. ie. a laptop. The transformer is usually in a block somewhere in the cable and it attenuates the voltage from the wall socket to a usable voltage for the appliance.
 

Gain in Action

The demonstration below shows the sine wave signal again but this time it has a slider to control the gain.

As you can see, the sine wave gets squashed vertically as the gain decreases. The wave eventually decreases until it is no more and only a flat line is left. The flat line signal has no amplitude so there would be no sound.
Notice that the features of the wave discussed earlier (maximum amplitude, intercept and the minimum amplitude) still occur in exactly the same places. Crossing the zero line, the positive and negative peaks all take place at exactly same pint in time as they did before, albeit closer to the zero line. This is a simple sine wave but no matter the shape of the wave, these rules would apply.

The range of the gain, in this case, is 0 to 1. To achieve the gain we multiply the signal by the gain value. Multiplying by the full gain of 1 results in exactly the same value so the signal is unaffected. Multiplying by the minimum of 0 will always result in 0 at any point so the signal will be a flat line. The values in between will affect the signal somewhere between these extremes.

 

Below is the visualiser created for this post.

I will mention again that these demonstrations are running in Iframes so click the link here if you experience any performance issues.

Clicking/touching the visualiser screen applies different levels of gain to the signal. The gain ranges from 0 at the bottom of the visualiser up to 2 at the top.  The current gain is displayed to the right of the stop button. The red wave is the input signal and the blue wave is the signal with the gain. The input signal has already been attenuated so that the gain factor of 2 does not cause any distortion.

Try using the fit checkbox so you can see the difference in the waves when zoomed in and out.
 

Change Log

  • Changes to the visualiser to support mobile devices
  • Visualiser refactoring
  • Visualiser mouse/touch controls
  • Amplifier signal processor
  • Changes to sine wave graph demonstration to support a gain control

 

Source Code (click to expand)

Scripts/index.js (For the visualiser)

var audioSourceIsFileSystem = false;
// Audio volume warning is shown the first time audio is played only.
var showAudioVolumeWarning = true;

// Reference to audioLoader module.
var audioLoader = com.littleDebugger.daw.audioLoader;
// Reference to controlHelpers module.
var controlHelpers = com.littleDebugger.utility.ui.controlWrapper;

// Reference to the audio processor used for this workshop.
var audioProcessor;

var loadingMessage = document.getElementById('loadingMessage');

var audioSourceText = {
    1: "Filesystem",
    0: "Server"
};

// Array of objects with colour and alpha (opacity) properties.
// The first object represented the configuration for the input buffer and the second for the output.
// This is configurable so that the visualiser can show many different waves at the same time.
var waveDisplayConfigs = [{
        colour: "rgb(205,0,40)",
        alpha: 1
    },
    {
        colour: "rgb(0, 225,255)",
        alpha: 1
    }
];

var canvas = document.getElementById('visualiserCanvas');
// Initialise visualiser.
var visualiser = com.littleDebugger.daw.dsp.visualiser(
    waveDisplayConfigs,
    canvas,
    document.getElementById('lineWidth'),
    document.getElementById('fitToCanvasCheckbox'),
    document.getElementById('refreshRate'));

var playControl = document.getElementById('playButton');
var stopControl = document.getElementById('stopButton');
var filesystemFileControl = document.getElementById('file');
var audioFileControl = document.getElementById('fileToPlay');
var fileSourceControl = document.getElementById('audioSourceSwitch');

// The callback for the audioProcessingEvent from the audio player.
// The code is not in the audio player because its currently doing more than it should be
//  due to calling the visualiser.
var processAudio = function (audioProcessingEvent) {
    var inputBuffer = audioProcessingEvent.inputBuffer;
    var outputBuffer = audioProcessingEvent.outputBuffer;

    var updateVisualiser = true;
    for (var channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
        var inputData = inputBuffer.getChannelData(channel);

        // Reduce the original gain so amplification can be demonstrated too.
        for (var sample = 0; sample < inputBuffer.length; sample++) {
            inputData[sample] = inputData[sample] * 0.5;
        }

        var outputData = outputBuffer.getChannelData(channel);

        audioProcessor(inputData, outputData);

        // Visualiser should only be updated for 1 channel.
        if (updateVisualiser) {
            visualiser.drawWave([inputData, outputData]);
            updateVisualiser = false;
        }
    }
};

// Wire up control events.
// Some of the following event handling could be contained in a module.
// I am not exactly sure how it will all be grouped and split yet so it is just in the main page JS file. 

fileSourceControl.onclick = function () {
    this.value = audioSourceText[audioSourceIsFileSystem * 1];
    if (audioSourceIsFileSystem) {
        filesystemFileControl.style.display = 'none';
        audioFileControl.style.display = 'inline';
        audioSourceIsFileSystem = false;
        audioFileControl.onchange();
    } else {
        filesystemFileControl.style.display = 'inline';
        audioFileControl.style.display = 'none';
        audioSourceIsFileSystem = true;
        filesystemFileControl.value = null;
        filesystemFileControl.click();
    }
};

window.addEventListener(audioLoader.audioLoadingStartedEventName, function () {
    loadingMessage.style.display = 'inline';
    playControl.disabled = true;
    playControl.style.color = "grey";
});

window.addEventListener(audioLoader.audioLoadingCompletedEventName, function () {
    loadingMessage.style.display = 'none';
    playControl.disabled = false;
    playControl.style.color = "green";
});

playControl.onclick = function () {
    if (showAudioVolumeWarning) {
        alert('Please make sure the audio volume is set to an appropriate level!');
        showAudioVolumeWarning = false;
    }
    player.startAudio();
};

stopControl.onclick = function () {
    player.stopAudio();
};

audioFileControl.onchange = function () {
    player.stopAudio();
    playControl.disabled = true;
    player.cueAudioFile(this.selectedOptions[0].getAttribute('data-audioFile'));
};

filesystemFileControl.onchange = function () {
    player.stopAudio();
    var localFile = window.URL.createObjectURL(this.files[0]);
    player.cueAudioFile(localFile);
};

resolutionControl = document.getElementById('resolutionSelect');

// Add event to change the canvas resolution when the resolution select is changed.
resolutionControl.onchange = function () {
    var width = this.options[this.selectedIndex].getAttribute('data-width');
    var height = this.options[this.selectedIndex].getAttribute('data-height');
    visualiser.setDimensions(width, height);
};

resolutionControl.onchange();

// Initialise the player.
var player = com.littleDebugger.daw.player(
    document.getElementById('bufferSizeSelect'),
    processAudio);

var gain = document.getElementById('gain');

audioProcessor = com.littleDebugger.daw.dsp.gain(
    gain);

var gainDisplay = document.getElementById('gainDisplay');

gain.onchange = function () {
    gainDisplay.innerHTML = "Gain: " + gain.value;
};

var canvasContainer = document.getElementById('canvasContainer');
var visualiserCanvas = document.getElementById('visualiserCanvas');
var controls = document.getElementById('controls');

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

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

setDimensions();

// Button to enter full screen.
com.littleDebugger.ui.fullScreenEvent(
    canvasContainer,
    document.getElementById('maximise'));

// Handle hiding block of controls.
var controlsHidden = false;
var hidableControls = document.getElementById('innerControls');
document.getElementById('showHideControls').onclick = function () {
    if (controlsHidden) {
        controlsHidden = false;
        hidableControls.style.display = "block";
        setDimensions();
    } else {
        controlsHidden = true;
        hidableControls.style.display = "none";
        setDimensions();
    }
};

// Cue the audio.
audioFileControl.onchange();

// http://www.html5canvastutorials.com/advanced/html5-canvas-mouse-coordinates/
function getMousePos(canvas, evt) {
    var rect = canvas.getBoundingClientRect();
    return {
        x: evt.clientX - rect.left,
        y: evt.clientY - rect.top
    };
}

// The following code handles the canvas click events. This will be moved out into a module.
var update = false;
var updateGain = function (evt) {
    var mousePos = getMousePos(canvas, evt);

    // neeed actual dimensions http://stackoverflow.com/a/4032188/6830533
    var y = mousePos.y / canvas.scrollHeight;
    gain.value = parseInt(gain.max) - (y * (gain.max - gain.min));
    gain.onchange();
};

canvas.addEventListener('mousedown',
    function (evt) {
        update = true;
        updateGain(evt);
        // Stop dragging element;
        event.preventDefault();
    }, false);

window.addEventListener('mouseup',
    function () {
        update = false;
    }, false);


canvas.addEventListener('mousemove',
    function (evt) {
        if (update)
            updateGain(evt);
    }, false);

canvas.addEventListener('touchstart',
    function (evt) {
        update = true;
        updateGainTouch(evt);
    }, false);

canvas.addEventListener('touchend',
    function () {
        update = false;
    }, false);

canvas.addEventListener('touchmove',
    function (evt) {
        if (update)
            updateGainTouch(evt);
    }, false);

var updateGainTouch = function (evt) {
    var touchPos = getTouchPos(canvas, evt);

    var y = touchPos.y / canvas.scrollHeight;
    gain.value = parseInt(gain.max) - (y * (gain.max - gain.min));
    gain.onchange();
};

function getTouchPos(canvas, evt) {
    var rect = canvas.getBoundingClientRect();
    return {
        x: evt.touches[0].clientX - rect.left,
        y: evt.touches[0].clientY - rect.top
    };
}

// Show the initial gain.
gain.onchange();

Scripts/sineGraph.js (For the sine wave graph)

// 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 gain = document.getElementById('gain');

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 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();

    var gainValue = gain.value;

    for (var i = 0; i < canvas.width + 1; i++) {
        // get sine wave samples at 2 cycles per sample rate.
        var y = sineWaveGeneratorInstance.getSample(2);
        var yWithGain = y * gainValue;

        ctx.lineTo(i, canvas.height - ((yWithGain * noneCroppingHeight / 2) + (canvas.height / 2)));
    }

    ctx.stroke();
};

var redrawCanvas = function () {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 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);

    drawWave(0, 'blue');
};

// Update the canvas 8 times per second. Each redraw gets the value from the gain range control.
// setInterval was used rather than a change event because the change event is only fired once the change is complete.
window.setInterval(redrawCanvas, 1000 / 8);

redrawCanvas();

var gainControlContainer = document.getElementById('gainControlContainer');

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

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

setDimensions();

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>JavaScript Audio Visualiser</title>
    <script src="Scripts/com/littleDebugger/namespacer.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/gain.js"></script>
    <script src="Scripts/com/littleDebugger/daw/audioContext.js"></script>
    <script src="Scripts/com/littleDebugger/daw/audioLoader.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/visualiser.js"></script>
    <script src="Scripts/com/littleDebugger/daw/player.js"></script>
    <script src="Scripts/com/littleDebugger/utility/ui/controlHelpers.js"></script>
    <script src="Scripts/com/littleDebugger/ui/fullScreenEvent.js"></script>
    <link rel="stylesheet" type="text/css" href="Styles/index.css"/>
</head>

<body class="non-selectable">
    <div id="container">
        <div id="canvasContainer">
            <div id="controls">
                <div id="topControls">
                    <div>
                        <button id="maximise" style="font-size: x-small">Full Screen</button>
                        <button id="showHideControls" style="font-size: x-small">Show controls</button>
                        <input id="playButton" type="button" value="Play" style="font-weight: bold"/>
                        <input id="stopButton" type="button" value="Stop" style="color: red"/>
                        <span id="gainDisplay"></span>
                        <span class="message" id="loadingMessage">Loading...</span>
                    </div>
                    <div>
                        <input id="gain" type="range" min="0" max="2" value="1" step="0.01" style="display:none"/>
                    </div>
                </div>
                <div id="innerControls">
                    <div class="inline">
                        <span>Buffer:</span>
                        <span>
                            <select id="bufferSizeSelect">
                            <option>256</option>
                            <option>512</option>
                            <option>1024</option>
                            <option>2048</option>
                            <option>4096</option>
                            <option>8192</option>
                            <option selected>16384</option>
                        </select>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                            Resolution:
                        </span>
                        <span>
                            <select id="resolutionSelect">
                            <option data-width="1920" data-height="1000">V</option>
                            <option data-width="1024" data-height="864">IV</option>
                            <option data-width="800" data-height="640">III</option>
                            <option data-width="640" data-height="480" selected>II</option>
                            <option data-width="320" data-height="200">I</option>
                        </select>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                            Width:
                        </span>
                        <span>
                            <input id="lineWidth" type="range" min="1" max="15" value="1"/>
                        </span>
                    </div>
                    <div class="inline">
                        <span>Fit:</span>
                        <span>
                            <input id="fitToCanvasCheckbox" type="checkbox"/>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                        Refresh:
                    </span>
                        <span>
                        <input id="refreshRate" type="range" min="1" max="20" value="1"/>
                    </span>
                    </div>
                    <div class="inline">
                        <span><input id="audioSourceSwitch" type="button" value="Filesystem"/></span>
                        <select id="fileToPlay">
                            <option value="1" data-audioFile='Audio/bensound-funnysong.mp3'>
                                Funny Song
                            </option>
                        </select>
                        <input id="file" type="file" accept="audio/*" style="display: none">
                        </span>
                    </div>
                </div>
            </div>
            <canvas id="visualiserCanvas"></canvas>
        </div>
    </div>
    </div>
</body>

</html>
<script src="Scripts/index.js"></script>

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>
<div id="gainControlContainer">
    Gain:<input id="gain" type="range" min="0" max="1" step="0.01" value="1"/>
</div>
<canvas id="canvas" height="600" width="1080">
    Browser does not support canvas
</canvas>
<script src="Scripts/sineGraph.js"></script>
</body>

</html>

images/index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>JavaScript Audio Visualiser</title>
    <script src="Scripts/com/littleDebugger/namespacer.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/sampleRateReduction.js"></script>
    <script src="Scripts/com/littleDebugger/daw/audioContext.js"></script>
    <script src="Scripts/com/littleDebugger/daw/audioLoader.js"></script>
    <script src="Scripts/com/littleDebugger/daw/dsp/visualiser.js"></script>
    <script src="Scripts/com/littleDebugger/daw/player.js"></script>
    <script src="Scripts/com/littleDebugger/utility/ui/controlHelpers.js"></script>
    <script src="Scripts/com/littleDebugger/ui/fullScreenEvent.js"></script>
    <link rel="stylesheet" type="text/css" href="Styles/index.css"/>
</head>

<body class="non-selectable">
    <div id="test"></div>
    <div id="container">
        <div id="canvasContainer">
            <div id="topControls">
                <div style="float:right">
                    <span class="message" id="loadingMessage">Loading...</span>
                    <button id="maximise" style="font-size: x-small">Maximise</button>
                    <button id="showHideControls" style="font-size: x-small">Show controls</button>
                    <input id="playButton" type="button" value="Play" style="font-weight: bold"/>
                    <input id="stopButton" type="button" value="Stop" style="color: red"/>
                </div>
                <div>
                    <input id="bitReduction" type="range" min="1" max="140" value="1" style="display:none"/>
                    <button id="bitReductionDecrease" style="display:none">-</button>
                    <button id="bitReductionIncrease" style="display:none">+</button>
                    <span id="sampleRate"></span>
                    <span><a href="https://goo.gl/toxBhH">Instructions</a></span>
                </div>
            </div>
            <div id="controls">
                <div id="innerControls">
                    <div class="inline">
                        <span>Buffer:</span>
                        <span>
                            <select id="bufferSizeSelect">
                            <option>256</option>
                            <option>512</option>
                            <option>1024</option>
                            <option>2048</option>
                            <option>4096</option>
                            <option>8192</option>
                            <option selected>16384</option>
                        </select>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                            Resolution:
                        </span>
                        <span>
                            <select id="resolutionSelect">
                            <option data-width="1920" data-height="1000">V</option>
                            <option data-width="1024" data-height="864">IV</option>
                            <option data-width="800" data-height="640">III</option>
                            <option data-width="640" data-height="480" selected>II</option>
                            <option data-width="320" data-height="200">I</option>
                        </select>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                            Width:
                        </span>
                        <span>
                            <input id="lineWidth" type="range" min="1" max="15" value="1"/>
                        </span>
                    </div>
                                        <div class="inline">
                        <span>Fit:</span>
                        <span>
                            <input id="fitToCanvasCheckbox" type="checkbox"/>
                        </span>
                    </div>
                    <div class="inline">
                        <span>
                        Refresh:
                    </span>
                        <span>
                        <input id="refreshRate" type="range" min="1" max="20" value="1"/>
                    </span>
                    </div>
                    <div class="inline">
                        <span><input id="audioSourceSwitch" type="button" value="Filesystem"/></span>
                        <select id="fileToPlay">
                            <option value="1" data-audioFile='Audio/bensound-funnysong.mp3'>
                                Funny Song
                            </option>
                        </select>
                        <input id="file" type="file" accept="audio/*" style="display: none">
                    </span>
                    </div>
                </div>
            </div>
            <canvas id="visualiserCanvas"></canvas>
        </div>
    </div>
    </div>
</body>

</html>
<script src="Scripts/index.js"></script>

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/audioContext.js

// Create namespace.
com.littleDebugger.namespacer.createNamespace("com.littleDebugger.daw");

// The is the visualiser.
com.littleDebugger.daw.getAudioContext = (function () {
    // Support Web Audio API in different supported broswers.
    // Taken from http://chimera.labs.oreilly.com/books/1234000001552/ch01.html#s01_2
    var getAudioContext = function () {
        var ContextClass = (
            window.AudioContext ||
            window.webkitAudioContext ||
            window.mozAudioContext ||
            window.oAudioContext ||
            window.msAudioContext);
        if (ContextClass) {
            return new ContextClass();
        } else {
            alert("Web Audio API is not available. Please use a supported browser.");
            throw new Exception();
        }
    };

    return getAudioContext;
})();

Scripts/com/littleDebugger/daw/audioLoader.js

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

com.littleDebugger.daw.audioLoader = function () {
    this.audioLoadingStartedEventName = 'audio-loading-started';
    this.audioLoadingCompletedEventName = 'audio-loading-completed';

    // Load audio file.
    // <fileName> Name of the file to be loaded. This can be on local machine if it has been loaded correctly.
    // <audioCtx> Audio context on which the audio file should be played.
    // <sourceReturnCallback> Callback to attach the audio the context when loaded.
    this.loadAudioFile = function (fileName, audioCtx, sourceReturnCallback) {
        this.loadAudioBufferFromFile(fileName, audioCtx, function (buffer) {
            sourceReturnCallback(this.createBuffer(buffer, audioCtx));
        })
    };

    // Load audio file and return the buffer.
    // This function is public but is not yet called from outside of this module. 
    // It will be though, which might give you an idea about how I plan to play audio later on
    // in the series.

    // Function was based on the example here: 
    // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/decodeAudioData
    this.loadAudioBufferFromFile = function (fileName, audioCtx, bufferReturnCallback) {
        fireEvent(audioLoadingStartedEventName, fileName);

        var request = new XMLHttpRequest();
        request.open('GET', fileName, true);
        request.responseType = 'arraybuffer';
        request.onload = function () {
            var audioData = request.response;
            audioCtx.decodeAudioData(audioData, function (buffer) {
                    fireEvent(audioLoadingCompletedEventName, fileName);
                    bufferReturnCallback(buffer);
                },
                function (e) {
                    "Error decoding audio file." + e.err
                });
        }

        request.send();
    };

    // Creates an audio buffer.
    this.createBuffer = function (buffer, audioCtx) {
        source = audioCtx.createBufferSource();
        source.buffer = buffer;
        return source;
    };

    // Fires event.
    var fireEvent = function (eventName, detail) {
        var event = new CustomEvent(eventName, {
            'detail': detail
        });
        window.dispatchEvent(event);
    };

    return this;
}();

Scripts/com/littleDebugger/daw/player.js

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

// This is the audio player.
// It handles the audio context for loading, playing and stopping the audio.
// <audioLoader>Reference to the audioLoader.js module.
// <getAudioContext>Reference to the audioContext.js module.
com.littleDebugger.daw.player = (function (audioLoader, getAudioContext) {
    var initialise = function (
        bufferSizeControl,
        processAudioCallback) {
        var that = {};
        that.audioPlayingEventName = "audio-playing";
        that.audioStoppedEventName = "audio-stopped";

        var playingAudio = false;
        var audioCtx = getAudioContext();
        var source = audioCtx.createBufferSource();
        var scriptNode;

        // Reloads the audio file.
        that.cueAudioFile = function (fileName) {
            audioLoader.loadAudioFile(fileName, audioCtx, setSource);
        };

        // Starts the audio playing.
        that.startAudio = function () {
            if (!playingAudio) {
                fireEvent(that.audioPlayingEventName);
                bufferSizeControl.disabled = true;
                scriptNode = audioCtx.createScriptProcessor(bufferSizeControl.value, 1, 1);
                scriptNode.onaudioprocess = function (audioProcessingEvent) {
                    processAudioCallback(audioProcessingEvent);
                }

                playingAudio = true;
                source.connect(scriptNode);
                scriptNode.connect(audioCtx.destination);
                source.start();
            }
        };

        // Stops the audio.
        that.stopAudio = function () {
            if (playingAudio) {
                fireEvent(that.audioStoppedEventName);
                source.stop();
                playingAudio = false;
                bufferSizeControl.disabled = false;
                source.disconnect(scriptNode);
                scriptNode.disconnect(audioCtx.destination);
                setSource(audioLoader.createBuffer(source.buffer, audioCtx));
            }
        };

        var fireEvent = function (eventName) {
            var event = new Event(that.audioStoppedEventName);
            window.dispatchEvent(event);
        };

        // Used as a callback to set the local source variable. 
        var setSource = function (src) {
            source = src;
            setOnended();
        };

        // When the buffer source stops playControling, disconnect everything.
        var setOnended = function () {
            source.onended = that.stopAudio;
        };

        that.sampleRate = audioCtx.sampleRate;

        return that;
    };

    return initialise;
})(
    com.littleDebugger.daw.audioLoader,
    com.littleDebugger.daw.getAudioContext);

Scripts/com/littleDebugger/daw/dsp/gain.js

com.littleDebugger.namespacer.createNamespace("com.littleDebugger.daw.dsp");

// Applies gain to signal.
com.littleDebugger.daw.dsp.gain = function (gainControl) {
    // Process audio buffer.
    // <inputBuffer> The buffer to processed.
    // <outputBuffer> The processed buffer.
    return function (inputBuffer, outputBuffer) {
        var gain = gainControl.value;
        for (var sample = 0; sample < inputBuffer.length; sample++) {
            outputBuffer[sample] = inputBuffer[sample] * gain;
        }
    }
};

Scripts/com/littleDebugger/daw/dsp/visualiser.js

com.littleDebugger.namespacer.createNamespace("com.littleDebugger.daw.dsp");

// Visualiser module.
com.littleDebugger.daw.dsp.visualiser = function () {
    // Function to create a visualiser. 
    // The parameters represent the visualiser controls but since they are passed in then there is no 
    // dependency on the DOM.
    // The controls should not have a dependcy on a specific type of element but just need the 
    // appropriate properties. (child properties indented with '-')

    // <waveDisplayConfigs> Array of objects. Each object has properties related to the configuration of each waveform to 
    // the draw on the visualiser.
    // <canvas> Canvas element which where the visualiser will be drawn.
    // <waveWidthControl> Control for the width of the waveform lines. 
    // -<value> Line width in pixels.
    // <fitToVisualiserWidthControl> Control for stretching/contracting the buffer to fit neatly into the width of the 
    // visualiser. 
    // -<checked> Boolean property.
    // <refreshRateControl> Set how many buffers the visualiser should recieve before updating.
    // -<value> Integer property.
    var initialise = function (
        waveDisplayConfigs,
        canvas,
        waveWidthControl,
        fitToVisualiserWidthControl,
        refreshRateControl) {
        // Setup refresh rate.
        var visualFrame = 1;

        var visualiser = {};

        // Get canvas context.
        var ctx = canvas.getContext('2d');

        // We be the virtical midpoint of the canvas.
        var virticalMidpoint;

        // Get the vertical point on the canvas for amplitude.
        var getVerticalPoint = function (virticalMidpoint, amplitude) {
            return virticalMidpoint + (amplitude * virticalMidpoint);
        };

        // Draw wave on canvas.
        // <inputData> Audio buffer.
        // <ctx> Canvas context.
        // <strokeStlye> Colour of wave line.
        // <alpha> Alpha of wave line.
        var drawLine = function (inputData, ctx, strokeStyle, alpha) {
            var inputLength = inputData.length;

            ctx.globalAlpha = alpha;
            ctx.beginPath();
            ctx.strokeStyle = strokeStyle;
            ctx.lineWidth = waveWidthControl.value;
            ctx.moveTo(0, getVerticalPoint(virticalMidpoint, inputData[0]));

            var fit = fitToVisualiserWidthControl.checked;
            var inputLength = inputData.length;
            var canvasWidth = canvas.width;

            for (var sample = 1; sample < inputLength; sample++) {
                var x = fit ? (sample / inputLength) * canvasWidth : sample;
                ctx.lineTo(x, getVerticalPoint(virticalMidpoint, inputData[sample]))
            }

            ctx.stroke();
        };

        // Refresh the canvas with new buffers
        // <buffers> Array of buffers to display.
        visualiser.drawWave = function (buffers) {
            // Chec if the canvas should be updated.
            if (visualFrame % refreshRateControl.value == 0) {
                visualFrame = 1;
            } else {
                visualFrame++;
                return;
            }

            // Clear the canvas (could be optimised).
            ctx.clearRect(0, 0, canvas.width, canvas.height);

            // Iterate over each buffer and draw the wave.
            var i = 0;
            buffers.forEach(function (buffer) {
                var colour = waveDisplayConfigs[i].colour;
                var alpha = waveDisplayConfigs[i].alpha;
                drawLine(buffer, ctx, colour, alpha);
                i++;
            });
        };

        visualiser.setDimensions = function (width, height) {
            canvas.width = width;
            canvas.height = height;
            virticalMidpoint = canvas.height / 2;
        };

        return visualiser;
    };

    return initialise;
}();

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, exitFullScreenCallback) {
    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 (exitFullScreenCallback) != 'undefined') {

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

        document.addEventListener('webkitfullscreenchange', exitHandler, false);
        document.addEventListener('mozfullscreenchange', exitHandler, false);
        document.addEventListener('fullscreenchange', exitHandler, false);
        document.addEventListener('MSFullscreenChange', exitHandler, false);
    }
};

Scripts/com/littleDebugger/utility/ui/controlHelpers.js

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

// Hides the logic to show/hide elements on the DOM.
com.littleDebugger.utility.ui.controlWrapper = (function () {
    var hiddenClass = 'hidden';
    var that = {};

    var internalWrapControl = function(){

    }

    that.wrapControl = function (control) {
        var wrapper = {};

        var inputType = control.nodeName == "INPUT";

        // Hides a element.
        wrapper.hideControl = function () {
            control.classList.remove(hiddenClass);
        };

        // Shows a element.
        wrapper.showControl = function () {
            control.classList.add(hiddenClass);
        };

        wrapper.setDisabled = function(){
            control.disabled = true;
        }

        wrapper.setEnabled = function(){
            control.disabled = false;
        }        

        wrapper.setValue = function (newValue) {
            if (inputType) {
                control.value = newValue;
            } else {
                control.innerHTML = newValue;
            }
        }

        wrapper.value = control.value;

        control.onchange = function(){
            wrapper.value = control.value;
            wrapper.onchange();
        }

        control.onclick = function(){

            wrapper.onclick();
        }

        wrapper.onchange = function() {};
        wrapper.onclick = function() {};
        wrapper.getValue = function(){
            if (inputType) {
                return control.value;
            } else {
                return control.innerHTML;
            }
        }

        

        return wrapper;
    };

    that.getWrappedControl = function(element){
        return that.wrapControl(document.getElementById(element));
    };

    return that;
})();

Styles/index.css

/* Set the oscilloscope background on canvas on give it a border. */

#visualiserCanvas {
    background: url('../images/osc.jpg');
    background-size: 100% 100%;
}

/* Set the container in the center of the page. */

#container {
    padding: 0 0;
    margin: 0 0;
}

/* Loading message. */
.message {
    color: red;
}


/* Show/hide audio source controls. */
.hidden {
    display: none;
}


/* To stop the controls jumping around when changing audio source. */
#audioFileSelect {
    height: 1.5em;
}

.inline {
    display: inline-block;
    border-right: 2px solid white;
    margin: 0 0;
    padding: 0 1px;
    width:min-content;
    vertical-align: top;
}

#innerControls{
    padding: 0 0;
    margin: auto auto;
    max-width:max-content;
}

.non-selectable {
    -moz-user-select: none;
    -khtml-user-select: none;
    -webkit-user-select: none;
    -o-user-select: none;
    user-select: none;
} 

body {
    margin: 0 !important;
    background: black;
    color: white;
}

Leave a Reply