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