The code below is for a real-time audio visualisation experiment using the Web Audio API and HTML5 Canvas element. An accompanying blog post can be found at http://inmosystems.com/blog/html5-canvas-and-the-web-audio-api
(function () {
"use strict";
// Adjust jQuery prefix to avoid namespace object.
jQuery.noConflict();
var $ = {
/**
* Initialize everything
*/
initialize: function () {
// Set source of audio element.
$.Audio.Element.src = $.Audio.Track;
// Call load method of audio element (required by mobile browsers)
$.Audio.Element.load();
// Wait for it to load sufficiently before firing subsequent initialize functions (required to create AudioNodes)
$.Audio.Element.addEventListener("canplay", function() {
$.Audio.initialize();
$.Visualiser.initialize();
$.Controls.initialize();
});
// On track end stop animation loop and clear Levels array to avoid persistent final frame.
$.Audio.Element.addEventListener("ended", function() {clearTimeout($.Visualiser.animationLoopTimeOutId);});
$.Audio.Element.addEventListener("ended", $.Audio.clearLevels);
},
/**
* Audio related...
*/
Audio: {
Track: "song.mp3",
Element: document.createElement("audio"),
Context: new (window.AudioContext || window.webAudioContext || window.webkitAudioContext)(),
/**
* Initialize Audio related values
*/
initialize: function () {
// Create Audio node
$.Audio.Source = $.Audio.Context.createMediaElementSource($.Audio.Element);
// Create Analyser node and set fftSize
$.Audio.Analyser = $.Audio.Context.createAnalyser();
$.Audio.Analyser.fftSize = 2048;
// Connect audio source node to analyser node and output destination
$.Audio.Source.connect($.Audio.Analyser);
$.Audio.Source.connect($.Audio.Context.destination);
// Create data array to pass through analyser node
$.Audio.Data = new Uint8Array(1);
// Create Levels array to store a data discreet value for each shape instance
$.Audio.Levels = [];
// Call $.Audio.clearLevels() once to initialize $.Audio.Levels array
$.Audio.clearLevels();
},
/**
* Clears Levels array (fills with 0.1 values to clear screen)
*/
clearLevels: function () {
var Index;
for (Index = 0; Index < $.Visualiser.NumShapes; Index += 1) {
$.Audio.Levels[Index] = 0.1;
}
},
/**
* Update Levels array.
*/
updateLevels: function () {
var Index, Volume;
// Get current Time Domain value from Analyser node.
$.Audio.Analyser.getByteTimeDomainData($.Audio.Data);
// Add amplitude value for current frame to the end of the array. -128 centers the 0db value around zero, (as opposed to 128 of 256).
// Use Math.abs to negate a negative values, and *0.25 to scale resulting values appropriately (for line width drawing).
$.Audio.Levels.push(Math.abs($.Audio.Data[0]-128)*0.25);
// Trim the oldest value from the front of the array
$.Audio.Levels.splice(0, 1);
}
},
/**
* Visualiser Related...
*/
Visualiser: {
Shape1: true,
Sides1: 4,
Shape2: false,
Sides2: 8,
Perspective: 0,
ColorCycle: true,
Hyper: false,
StepInc: 24,
NumShapes: 48,
FPS: 24,
Canvas: document.getElementById("mainCanvas"),
/**
* Initialize Visualiser related items
*/
initialize: function () {
var Index;
// Create canvas context & set x/y values
$.Visualiser.Context = $.Visualiser.Canvas.getContext("2d");
$.Visualiser.XCenter = $.Visualiser.Canvas.width / 2;
$.Visualiser.YCenter = $.Visualiser.Canvas.height / 2;
// Set cycle frequency, phase, center point and modulation width for sine waves
$.Visualiser.ColorFreq = 0.036;
$.Visualiser.ColorPhase1 = 2; // phase..
$.Visualiser.ColorPhase2 = 4; // shift..
$.Visualiser.ColorPhase3 = 0; // ...with these
$.Visualiser.ColorCenter = 128;
$.Visualiser.ColorWidth = 127; // reducing this causes desaturation
// Initialize Colors array and create counter for cycling
$.Visualiser.Colors = [];
for (Index = 0; Index < $.Visualiser.NumShapes; Index += 1) {
$.Visualiser.Colors[Index] = "rgba(0,0,0,1.0)";
}
$.Visualiser.ColorCounter = 0;
},
/**
* Main loop function to play animation
*/
animationLoop: function () {
$.Visualiser.animationLoopTimeOutId = setTimeout(function() {
requestAnimationFrame($.Visualiser.animationLoop);
$.Audio.updateLevels();
$.Visualiser.animateFrame();
}, 1000 / $.Visualiser.FPS);
},
/**
* Draw a single frame of the visualisation
*/
animateFrame: function () {
var Index, LineWidth1, LineWidth2, Size, Shade;
if ($.Visualiser.ColorCycle == true) {
// Update Colors array
$.Visualiser.updateColours();
}
// Clear previous frame
$.Visualiser.Context.fillRect(0, 0, $.Visualiser.Canvas.width, $.Visualiser.Canvas.height);
// for loop for each shape instance (per frame)
for (Index = 1; Index <= $.Visualiser.NumShapes; Index += 1) {
// Map corresponding value from amplitude data array to path width for current shape instance,
// if 'Hyper' is on the multiply by inverse of array position
if ($.Visualiser.Hyper == true) {
LineWidth1 = $.Audio.Levels[$.Audio.Levels.length - Index]*($.Visualiser.NumShapes-Index);
} else {
LineWidth1 = $.Audio.Levels[$.Audio.Levels.length - Index];
}
// Multiply path width value by 0.25 for use in accompanying white 'highlight' instance
LineWidth2 = LineWidth1*0.25;
// Increase size value for each concentric shape instance (based on current perspective mode).
if ($.Visualiser.Perspective == 0) {
Size = $.Visualiser.StepInc * Index;
} else {
Size = Index * Index;
}
// Traverse Color array to reference appropriate color shade for currunt shape instance
Shade = $.Visualiser.Colors[$.Visualiser.Colors.length - Index];
// Draw current concentric shape instance(s)
if ($.Visualiser.Shape1) {
$.Visualiser.drawPolygon(LineWidth2, $.Visualiser.Sides1, Size, "rgba(256,256,256,0.6)");
$.Visualiser.drawPolygon(LineWidth1, $.Visualiser.Sides1, Size, Shade);
}
if ($.Visualiser.Shape2) {
$.Visualiser.drawPolygon(LineWidth2, $.Visualiser.Sides2, Size, "rgba(256,256,256,0.6)");
$.Visualiser.drawPolygon(LineWidth1, $.Visualiser.Sides2, Size, Shade);
}
}
},
/**
* Function to draw a single polygon shape instance
*/
drawPolygon: function (Width, NumberOfSides, Size, Shade) {
var Index;
$.Visualiser.Context.beginPath();
$.Visualiser.Context.moveTo($.Visualiser.XCenter + Size * Math.cos(0), $.Visualiser.YCenter + Size * Math.sin(0));
for (Index = 1; Index <= NumberOfSides; Index += 1) {
$.Visualiser.Context.lineTo($.Visualiser.XCenter + Size * Math.cos(Index * 2 * Math.PI / NumberOfSides),
$.Visualiser.YCenter + Size * Math.sin(Index * 2 * Math.PI / NumberOfSides));
}
$.Visualiser.Context.lineWidth = Width;
$.Visualiser.Context.strokeStyle = Shade;
$.Visualiser.Context.closePath();
$.Visualiser.Context.stroke();
},
/**
* Update Colours array.
*/
updateColours: function () {
// Get current colour value from cycle and add to end of array.
$.Visualiser.Colors.push($.Visualiser.getColor($.Visualiser.ColorFreq, $.Visualiser.ColorPhase1, $.Visualiser.ColorPhase2,
$.Visualiser.ColorPhase3, $.Visualiser.ColorCenter, $.Visualiser.ColorWidth));
// Trim the oldest value from the front of the array
$.Visualiser.Colors.splice(0, 1);
},
/**
* Generates a single colour step in the colour cycle
*/
getColor: function (frequency, phase1, phase2, phase3, center, width) {
var r = Math.sin(frequency*$.Visualiser.ColorCounter + phase1) * width + center;
var g = Math.sin(frequency*$.Visualiser.ColorCounter + phase2) * width + center;
var b = Math.sin(frequency*$.Visualiser.ColorCounter + phase3) * width + center;
$.Visualiser.ColorCounter++;
return "rgba("+Math.round(r)+","+Math.round(g)+","+Math.round(b)+",0.5)";
}
},
/**
* Control related...
*/
Controls: {
Message: "",
/**
* Initialize Control related items
*/
initialize: function () {
var Buttons, Index;
// Add event listeners for main control elements on page
document.getElementById("play").addEventListener("click", $.Controls.play);
document.getElementById("pause").addEventListener("click", $.Controls.pause);
document.getElementById("rewind").addEventListener("click", $.Controls.rewind);
document.getElementById("info").addEventListener("click", $.Controls.info);
document.getElementById("preset1").addEventListener("click", function(){$.Controls.restorePreset(1)});
document.getElementById("preset2").addEventListener("click", function(){$.Controls.restorePreset(2)});
document.getElementById("perspectiveMode").addEventListener("click", $.Controls.switchPerspective);
document.getElementById("shape1Active").addEventListener("click", $.Controls.shape1Active);
document.getElementById("shape1Up").addEventListener("click", function(){$.Controls.shape1Sides("up")});
document.getElementById("shape1Down").addEventListener("click", function(){$.Controls.shape1Sides("down")});
document.getElementById("shape2Active").addEventListener("click", $.Controls.shape2Active);
document.getElementById("shape2Up").addEventListener("click", function(){$.Controls.shape2Sides("up")});
document.getElementById("shape2Down").addEventListener("click", function(){$.Controls.shape2Sides("down")});
document.getElementById("colorCycleActive").addEventListener("click", $.Controls.colorCycleActive);
document.getElementById("hyperActive").addEventListener("click", $.Controls.hyperActive);
// Add event listeners to fade controls in/out on hover
Buttons = document.querySelectorAll("div.btn-group");
for (Index = 0; Index < Buttons.length; Index++) {
Buttons[Index].addEventListener("mouseenter", function() { jQuery("#controls").stop().animate({opacity:1}, 350) });
Buttons[Index].addEventListener("mouseleave", function() { $.Controls.fadeOut(3000) });
// Fade out controls by default after initial load
$.Controls.fadeOut(6000);
}
// Event Listener for info panel close button
document.getElementById("closeInfo").addEventListener("click", $.Controls.info);
},
/**
* Play Button
*/
play: function () {
if ($.Audio.Element.paused) {
$.Audio.Context.resume();
$.Audio.Element.play();
clearTimeout($.Visualiser.animationLoopTimeOutId);
$.Visualiser.animationLoop();
$.Controls.consoleWrite("Play");
}
},
/**
* Pause Button
*/
pause: function () {
if (!$.Audio.Element.paused) {
$.Audio.Element.pause();
clearTimeout($.Visualiser.animationLoopTimeOutId);
$.Controls.consoleWrite("Pause");
}
},
/**
* Reset Button
*/
rewind: function () {
$.Audio.Element.currentTime = 0;
$.Audio.clearLevels();
$.Visualiser.ColorCounter = 0
$.Visualiser.Context.clearRect(0, 0, $.Visualiser.Canvas.width, $.Visualiser.Canvas.height);
$.Controls.consoleWrite("Restart");
},
/**
* Info Button
*/
info: function () {
jQuery("#infoPanel").fadeToggle();
},
/**
* Preset Buttons
*/
restorePreset: function (num) {
if (num == 1) {
$.Visualiser.Sides1 = 4;
$.Visualiser.Sides2 = 8;
$.Visualiser.Perspective = 0;
$.Visualiser.Shape2 = true;
$.Visualiser.Hyper = false;
$.Visualiser.animateFrame();
$.Controls.consoleWrite("Preset 1");
}
if (num == 2) {
$.Visualiser.Sides1 = 32;
$.Visualiser.Sides2 = 4;
$.Visualiser.Perspective = 1;
$.Visualiser.Shape2 = true;
$.Visualiser.Hyper = true;
$.Visualiser.animateFrame();
$.Controls.consoleWrite("Preset 2");
}
},
/**
* Zoom Button
*/
switchPerspective: function () {
$.Visualiser.Perspective = ($.Visualiser.Perspective == 0) ? 1: 0;
$.Visualiser.animateFrame();
$.Controls.Message = ($.Visualiser.Perspective == 0) ? "Zoom: Off": "Zoom: On<br />...less effective at lower volumes.";
$.Controls.consoleWrite($.Controls.Message);
},
/**
* Shape 1 Buttons: Show/Hide, +/- Sides.
*/
shape1Active: function () {
$.Visualiser.Shape1 = ($.Visualiser.Shape1 == true) ? false: true;
$.Visualiser.animateFrame();
$.Controls.Message = ($.Visualiser.Shape1 == true) ? "Shape 1: Visible": "Shape 1: Hidden";
$.Controls.consoleWrite($.Controls.Message);
},
shape1Sides: function (change) {
if ($.Visualiser.Sides1 <= 30 && change == "up") {
$.Visualiser.Sides1 += 2;
} else if ($.Visualiser.Sides1 >= 6 && change == "down") {
$.Visualiser.Sides1 -= 2;
}
$.Visualiser.animateFrame();
$.Controls.Message = "Shape 1 Sides: " + $.Visualiser.Sides1;
if ($.Visualiser.Sides1 == 4) $.Controls.Message += " (min value)";
if ($.Visualiser.Sides1 == 32) $.Controls.Message += " (max value)";
$.Controls.consoleWrite($.Controls.Message);
},
/**
* Shape 2 Buttons: Show/Hide, +/- Sides.
*/
shape2Active: function () {
$.Visualiser.Shape2 = ($.Visualiser.Shape2 == true) ? false: true;
$.Visualiser.animateFrame();
$.Controls.Message = ($.Visualiser.Shape2 == true) ? "Shape 2: Visible": "Shape 2: Hidden";
$.Controls.consoleWrite($.Controls.Message);
},
shape2Sides: function (change) {
if ($.Visualiser.Sides2 <= 30 && change == "up") {
$.Visualiser.Sides2 += 2;
} else if ($.Visualiser.Sides2 >= 6 && change == "down") {
$.Visualiser.Sides2 -= 2;
}
$.Visualiser.animateFrame();
$.Controls.Message = "Shape 2 Sides: " + $.Visualiser.Sides2;
if ($.Visualiser.Sides2 == 4) $.Controls.Message += " (min value)";
if ($.Visualiser.Sides2 == 32) $.Controls.Message += " (max value)";
$.Controls.consoleWrite($.Controls.Message);
},
/**
* Hue Button: Cycle/Freeze
*/
colorCycleActive: function () {
$.Visualiser.ColorCycle = ($.Visualiser.ColorCycle == true) ? false: true;
$.Controls.Message = ($.Visualiser.ColorCycle == true) ? "Hue: Cycle": "Hue: Freeze";
$.Controls.consoleWrite($.Controls.Message);
},
/**
* Hyper Button: On/Off
*/
hyperActive: function () {
$.Visualiser.Hyper = ($.Visualiser.Hyper == true) ? false: true;
$.Visualiser.animateFrame();
$.Controls.Message = ($.Visualiser.Hyper == true) ? "Hyper: On": "Hyper: Off";
$.Controls.consoleWrite($.Controls.Message);
},
/**
* Fade Out Button Controls
*/
fadeOut: function (duration) {
jQuery("#controls").stop().animate({opacity:0.4},duration);
},
/**
* Write Button Feedback To Console
*/
consoleWrite: function (message) {
jQuery("#console").prepend('<li><span><i class="fa fa-terminal"></i> ' + message + '</span></li>');
jQuery("#console li").fadeOut(11000);
}
}
}
$.initialize();
}());