├── LICENSE ├── README.md ├── index.html └── visualizer.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sammy Sammon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # audio-visualizer 2 | A simple canvas visualizer for frequencies from an input audio file or stream, scaled logarithmically as _perceived frequency_ and not as _absolute frequency_. 3 | 4 | [Click here for a demo!](https://blog.kumo.dev/audio-visualizer/) 5 | 6 | # Summary 7 | This visualiser is a simple bar representation of the frequencies of an audio input. Initialisation is simple, you just call `initializeVisualizer(canvas, audio)`, passing in the canvas you want to render to and the audio element with the source audio. Source audio can be anything that HTML5 audio supports, including web streams (like radios). This control runs on top of the Web Audio API, so support is limited to browsers that support that API. If unsure, test it out. 8 | 9 | Background colour, bar colour, number of bars ('frequency bins') and text font is configurable inside visualizer.js, modify to your taste. 10 | Call `updateSongText(text)` to change the text on the control. 11 | 12 | See index.html for example usage. Currently only 1 control is supported at a time, but I'll refactor it to fix that when I'm not feeling so lazy. 13 | 14 | ![](https://my.mixtape.moe/hyigns.gif) 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /visualizer.js: -------------------------------------------------------------------------------- 1 | //Note: bins needs to be a power of 2 2 | let displayBins = 512; 3 | let backgroundColour = "#2C2E3B"; 4 | let barColour = "#EC1A55"; 5 | let songFont = "15px 'Open Sans'"; 6 | //Where the bottom of the waveform is rendered at (out of 255). I recommend 7 | //leaving it at 96 since it seems to work well, basically any volume will push 8 | //it past 96. If your audio stream is quiet though, you'll want to reduce this. 9 | let floorLevel = 96; 10 | 11 | //Whether to draw the frequencies directly, or scale the x-axis logarithmically and show pitch instead. 12 | let drawPitch = true; 13 | //Whether to draw the visualisation as a curve instead of discrete bars 14 | let drawCurved = true; 15 | //If drawCurved is enabled, this flag fills the area beneath the curve (the same colour as the line) 16 | let drawFilled = false; 17 | //Whether to draw text the songText on top of the visualisation 18 | let drawText = false; 19 | 20 | //Can't touch this 21 | let audioContext; 22 | let audioBuffer; 23 | let audioAnalyserNode; 24 | let audioVisualizerInitialized = false; 25 | let songText = ""; 26 | let textSize; 27 | let canvasContext; 28 | let canvasWidth; 29 | let canvasHeight; 30 | let multiplier; 31 | let finalBins = []; 32 | let logLookupTable = []; 33 | let logBinLengths = []; 34 | let binWidth; 35 | let magicConstant = 42; //Meaning of everything. I don't know why this works. 36 | 37 | function initializeVisualizer(canvasElement, audioElement) { 38 | try { 39 | let ctxt = window.AudioContext || window.webkitAudioContext; 40 | if (ctxt) { 41 | initCanvas(canvasElement); 42 | audioContext = new ctxt(); 43 | setupAudioApi(audioElement); 44 | } 45 | } catch(e) { 46 | console.log(e); 47 | } 48 | } 49 | 50 | function updateSongText(newText) { 51 | songText = newText; 52 | if (canvasContext) 53 | textSize = canvasContext.measureText(songText); 54 | } 55 | 56 | function setupAudioApi(audioElement) { 57 | let src = audioContext.createMediaElementSource(audioElement); 58 | 59 | audioAnalyserNode = audioContext.createAnalyser(); 60 | //FFT node takes in 2 samples per bin, and we internally use 2 samples per bin 61 | audioAnalyserNode.fftSize = drawPitch ? displayBins * 8 : displayBins * 2; 62 | multiplier = Math.pow(22050, 1 / displayBins) * Math.pow(1 / magicConstant, 1 / displayBins); 63 | finalBins = []; 64 | logLookupTable = []; 65 | logBinLengths = []; 66 | for (let i = 0; i < displayBins; i++) { 67 | finalBins.push(0); 68 | logLookupTable.push(0); 69 | } 70 | createLookupTable(audioAnalyserNode.frequencyBinCount, logBinLengths, logLookupTable); 71 | binWidth = Math.ceil(canvasWidth / (displayBins - 1)); 72 | 73 | src.connect(audioAnalyserNode); 74 | audioAnalyserNode.connect(audioContext.destination); 75 | 76 | audioVisualizerInitialized = true; 77 | } 78 | 79 | function initCanvas(canvasElement) { 80 | canvasContext = canvasElement.getContext('2d'); 81 | canvasWidth = canvas.width; 82 | canvasHeight = canvas.height; 83 | requestAnimationFrame(paint); 84 | canvasContext.font = songFont; 85 | canvasContext.strokeStyle = barColour; 86 | 87 | textSize = canvasContext.measureText(songText); 88 | } 89 | 90 | //Render some fancy bars 91 | function paint() { 92 | requestAnimationFrame(paint); 93 | 94 | if(!audioVisualizerInitialized) 95 | return; 96 | 97 | canvasContext.fillStyle = backgroundColour; 98 | canvasContext.fillRect(0, 0, canvasWidth, canvasHeight); 99 | 100 | let bins = audioAnalyserNode.frequencyBinCount; 101 | let data = new Uint8Array(bins); 102 | audioAnalyserNode.getByteFrequencyData(data); 103 | canvasContext.fillStyle = barColour; 104 | 105 | if (drawPitch) 106 | updateBinsLog(logLookupTable, data); 107 | else 108 | updateBins(bins, logBinLengths, data); 109 | 110 | if (!drawCurved) { 111 | for (let i = 0; i < displayBins; i++) { 112 | paintSingleBin(i); 113 | } 114 | } else { 115 | canvasContext.fillStyle = barColour; 116 | canvasContext.beginPath(); 117 | canvasContext.moveTo(0, canvasHeight - getBinHeight(0)); 118 | let i; 119 | for (i = 0; i < displayBins - 2;) { 120 | var thisX = i * binWidth; 121 | var nextX = (i + logBinLengths[i]) * binWidth; //First subbin of the next bin 122 | var x = (thisX + nextX) / 2; 123 | 124 | var thisY = canvasHeight - getBinHeight(i); 125 | var nextY = canvasHeight - getBinHeight(i + logBinLengths[i]); 126 | var y = (thisY + nextY) / 2; 127 | 128 | canvasContext.quadraticCurveTo(thisX, thisY, x, y); 129 | 130 | i += logBinLengths[i]; 131 | } 132 | canvasContext.quadraticCurveTo(i * binWidth, canvasHeight - getBinHeight(i), (i + 1) * binWidth, canvasHeight - getBinHeight(i + 1)); 133 | if (drawFilled) { 134 | canvasContext.lineTo(canvasWidth, canvasHeight); 135 | canvasContext.lineTo(0, canvasHeight); 136 | canvasContext.fill(); 137 | } else { 138 | canvasContext.stroke(); 139 | } 140 | } 141 | 142 | if (drawText) { 143 | canvasContext.fillStyle = 'white'; 144 | //Note: the 15's here need to be changed if you change the font size 145 | canvasContext.fillText(songText, canvasWidth / 2 - textSize.width / 2, canvasHeight / 2 - 15 / 2 + 15); 146 | } 147 | } 148 | 149 | //Inclusive lower, exclusive upper except with stop == start 150 | function averageRegion(data, start, stop) { 151 | if (stop <= start) 152 | return data[start]; 153 | 154 | let sum = 0; 155 | for (let i = start; i < stop; i++) { 156 | sum += data[i]; 157 | } 158 | return sum / (stop - start); 159 | } 160 | 161 | function updateBins(bins, binLengths, data) { 162 | let step = bins / displayBins; 163 | for (let i = 0; i < displayBins; i++) { 164 | let lower = i * step; 165 | let upper = (i + 1) * step - 1; 166 | let binValue = averageRegion(data, lower, upper); 167 | binLengths.push(1); 168 | finalBins[i] = binValue; 169 | } 170 | } 171 | 172 | function createLookupTable(bins, binLengths, lookupTable) { 173 | if (drawPitch) { 174 | let lastFrequency = magicConstant / multiplier; 175 | let currentLength = 0; 176 | let lastBinIndex = 0; 177 | for (let i = 0; i < displayBins; i++) { 178 | let thisFreq = lastFrequency * multiplier; 179 | lastFrequency = thisFreq; 180 | let binIndex = Math.floor(bins * thisFreq / 22050); 181 | lookupTable[i] = binIndex; 182 | currentLength++; 183 | 184 | if (binIndex != lastBinIndex) { 185 | for (let j = 0; j < currentLength; j++) 186 | binLengths.push(currentLength); 187 | currentLength = 0; 188 | } 189 | 190 | lastBinIndex = binIndex; 191 | } 192 | } else { 193 | for (let i = 0; i < displayBins; i++) { 194 | lookupTable[i] = i; 195 | } 196 | } 197 | } 198 | 199 | function updateBinsLog(lookupTable, data) { 200 | for (let i = 0; i < displayBins; i++) { 201 | finalBins[i] = data[lookupTable[i]]; 202 | } 203 | } 204 | 205 | function getBinHeight(i) { 206 | let binValue = finalBins[i]; 207 | 208 | //Pretty much any volume will push it over [floorLevel] so we set that as the bottom threshold 209 | //I suspect I should be doing a logarithmic space for the volume as well 210 | let height = Math.max(0, (binValue - floorLevel)); 211 | //Scale to the height of the bar 212 | //Since we change the base level in the previous operations, 256 should be changed to 160 (i think) if we want it to go all the way to the top 213 | height = (height / (256 - floorLevel)) * canvasHeight * 0.8; 214 | return height; 215 | } 216 | 217 | function paintSingleBin(i) { 218 | let height = getBinHeight(i); 219 | canvasContext.fillRect(i * binWidth, canvasHeight - height, binWidth, height); 220 | } 221 | --------------------------------------------------------------------------------