Started out as a way to draw overlapping circles. Turned into some audio visualization. Enjoy!
A Pen by HARUN PEHLİVAN on CodePen.
Started out as a way to draw overlapping circles. Turned into some audio visualization. Enjoy!
A Pen by HARUN PEHLİVAN on CodePen.
| <div class="modal"> | |
| <h1>Select an audio source</h1> | |
| <div class="form-field"> | |
| <label>SoundCloud URL</label> | |
| <input type="text" id="soundcloud-input" value="https://soundcloud.com/harun-pehl-van/dogumgunumarsi"> | |
| </div> | |
| <div class="form-field"> | |
| <button class="button" id="load-sound-button">Load SoundCloud Audio</button> | |
| </div> | |
| <hr> | |
| <div class="form-field"> | |
| <label>Use Microphone</label> | |
| <button class="button" id="microphone-button">Use Microphone</button> | |
| </div> | |
| <hr> | |
| <div class="form-field"> | |
| <label>Use Local Audio File</label> | |
| <p> | |
| Drag file anywhere onto this pen | |
| </p> | |
| </div> | |
| <div class="drop-message"> | |
| <p> | |
| Go ahead, drop it! | |
| </p> | |
| </div> | |
| </div> | |
| <div class="overlay"></div> | |
| <canvas id="canvas"></canvas> |
| var canvas, | |
| context, | |
| audio, | |
| audioContext, | |
| analyserNode, | |
| audioStats, | |
| lowBeatDetector, | |
| time, | |
| scenes = [], | |
| settings = { | |
| fft : 1024, | |
| lowFreq : 20, | |
| midFreq : 150, | |
| highFreq : 511, | |
| debug : false | |
| }, | |
| debug = {}, | |
| spectrogram, | |
| spectrogramCanvas, | |
| spectrogramPos = 0, | |
| soundCloudClientID = "746146445791746357ff119cff0dbf04", | |
| modal, | |
| soundCloudInput, | |
| soundCloudButton, | |
| microphoneButton; | |
| console.clear(); | |
| navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; | |
| document.addEventListener("DOMContentLoaded", function() { | |
| // | |
| // Time Setup | |
| // | |
| time = { | |
| now : +(new Date()), | |
| dt : 0 | |
| }; | |
| // | |
| // UI | |
| // | |
| modal = document.querySelector(".modal"); | |
| soundCloudInput = document.querySelector("#soundcloud-input"); | |
| soundCloudButton = document.querySelector("#load-sound-button"); | |
| microphoneButton = document.querySelector("#microphone-button"); | |
| // | |
| // Graphics Setup | |
| // | |
| canvas = document.querySelector("#canvas"); | |
| context = canvas.getContext("2d"); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| canvas.style.width = canvas.width + 'px'; | |
| canvas.style.height = canvas.height + 'px'; | |
| spectrogramCanvas = document.createElement("canvas"); | |
| spectrogramCanvas.width = canvas.width; | |
| spectrogramCanvas.height = canvas.height; | |
| spectrogram = spectrogramCanvas.getContext('2d'); | |
| spectrogram.fillStyle = "#000"; | |
| spectrogram.fillRect(0, 0, canvas.width, canvas.height); | |
| // | |
| // Audio Setup | |
| // | |
| audio = new Audio(); | |
| audio.crossOrigin = "anonymous"; | |
| audioContext = new AudioContext(); | |
| lowBeatDetector = new BeatDetector(128); | |
| // | |
| // Event Listeners | |
| // | |
| canvas.addEventListener("click", function() { | |
| if(lowBeatDetector) console.log( JSON.stringify(lowBeatDetector.beats) ); | |
| }); | |
| document.body.addEventListener("dragenter", function(e) { | |
| stopEvent(e); | |
| document.body.classList.add("is-dragging"); | |
| }); | |
| document.body.addEventListener("dragleave", function(e) { | |
| stopEvent(e); | |
| document.body.classList.remove("is-dragging"); | |
| }); | |
| document.body.addEventListener("dragover", stopEvent); | |
| document.body.addEventListener("drop", function(e) { | |
| stopEvent(e); | |
| var url = window.URL.createObjectURL( e.dataTransfer.files[ 0 ] ); | |
| audio.src = url; | |
| audio.play(); | |
| modal.classList.add("is-hidden"); | |
| document.body.classList.remove("is-dragging"); | |
| setupAudioNodes(); | |
| }); | |
| soundCloudButton.addEventListener("click", function() { | |
| modal.classList.add("is-hidden"); | |
| setupAudioNodes(); | |
| loadSoundCloudUrl( soundCloudInput.value ); | |
| }); | |
| microphoneButton.addEventListener("click", function() { | |
| navigator.getUserMedia({audio: true}, function(mediaStream) { | |
| var source = audioContext.createMediaStreamSource(mediaStream); | |
| setupAudioNodes(source); | |
| modal.classList.add("is-hidden"); | |
| }, function(error) { | |
| alert("Well fine then."); | |
| }); | |
| }); | |
| scenes.push( new Scene({ cx : canvas.width / 2, cy : canvas.height / 2 }) ); | |
| loop(); | |
| }); | |
| function loop() { | |
| context.clearRect(0, 0, canvas.width, canvas.height); | |
| updateTime( time ); | |
| var freqs = (audioStats) ? audioStats.getStats() : null; | |
| context.globalCompositeOperation = "screen"; | |
| if(freqs) drawSpectrogram( freqs, audioStats.getAverageVolume(freqs) ); | |
| context.globalCompositeOperation = "source-over"; | |
| for(var i = 0; i < scenes.length; i++) { | |
| if(freqs) { | |
| updateSceneWithAudio( scenes[i], time.dt ); | |
| }else{ | |
| updateSceneWithoutAudio( scenes[i], time.dt ); | |
| } | |
| drawScene( context, scenes[i] ); | |
| } | |
| if(freqs) drawFreqs( freqs ); | |
| if(settings.debug) drawDebug(); | |
| requestAnimationFrame( loop ); | |
| } | |
| function updateTime(time) { | |
| var now = +(new Date()); | |
| time.dt = now - time.now; | |
| time.now = now; | |
| } | |
| function stopEvent(e) { | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| } | |
| function get(url, callback) { | |
| var xhr = new XMLHttpRequest(); | |
| xhr.onreadystatechange = function() { | |
| if(xhr.readyState == 4 && xhr.status == 200) { | |
| callback.call(null, xhr.responseText); | |
| } | |
| } | |
| xhr.open("GET", url, true); | |
| xhr.send(null); | |
| } | |
| function loadSoundCloudUrl(url) { | |
| get("https://api.soundcloud.com/resolve.json?url=" + url + "&client_id=" + soundCloudClientID, function(response) { | |
| var trackData = JSON.parse( response ); | |
| audio.src = trackData.stream_url + "?client_id=" + soundCloudClientID; | |
| audio.play(); | |
| }); | |
| } | |
| function setupAudioNodes(source) { | |
| var sourceNode; | |
| // Source Node | |
| if(source) { | |
| sourceNode = source; | |
| }else{ | |
| sourceNode = audioContext.createMediaElementSource( audio ); | |
| } | |
| // Analyser Node | |
| analyserNode = audioContext.createAnalyser(); | |
| analyserNode.smoothingTimeConstant = 0.8; | |
| analyserNode.fftSize = settings.fft; | |
| // Gain Node | |
| gainNode = audioContext.createGain(); | |
| gainNode.gain.value = 1; | |
| // Node Connections | |
| sourceNode.connect( analyserNode ); | |
| analyserNode.connect( gainNode ); | |
| gainNode.connect( audioContext.destination ); | |
| // Audio Stats | |
| audioStats = new AudioStats( analyserNode ); | |
| } | |
| function Scene(values) { | |
| values = values || {}; | |
| this.rotation = values.rotation || 0; | |
| this.innerRadius = values.innerRadius || 100; | |
| this.circleRadius = values.circleRadius || 150; | |
| this.number = values.number || 100; | |
| this.cx = values.cx || 0; | |
| this.cy = values.cy || 0; | |
| this.color = values.color || "#fff"; | |
| this.minNumber = values.number || 50; | |
| this.distortion = 0; | |
| } | |
| function updateSceneWithoutAudio(scene, dt) { | |
| dt /= 1000; | |
| scene.color = "#000"; | |
| scene.rotation += dt; | |
| scene.innerRadius = Math.sin(scene.rotation * 6) * 5 + 120; | |
| scene.circleRadius = Math.sin(scene.rotation * 3.74928) * 5 + 85; | |
| scene.distortion = Math.sin(scene.rotation) + 2; | |
| } | |
| function updateSceneWithAudio(scene, dt) { | |
| var freqs = audioStats.getStats(); | |
| var volume = audioStats.getAverageVolume( freqs ); | |
| var superLowFreqs = audioStats.getRangeVolume( freqs, 0, 10 ); | |
| var lowFreqs = audioStats.getRangeVolume( freqs, 0, settings.lowFreq ); | |
| var midFreqs = audioStats.getRangeVolume( freqs, settings.lowFreq + 1, settings.midFreq ); | |
| var highFreqs = audioStats.getRangeVolume( freqs, settings.midFreq + 1, 511 ); | |
| var lowEnergy = audioStats.getRangeEnergy( freqs, 0, settings.lowFreq, true ); | |
| var midEnergy = audioStats.getRangeEnergy( freqs, settings.lowFreq + 1, settings.midFreq, true ); | |
| var highEnergy = audioStats.getRangeEnergy( freqs, settings.midFreq + 1, 511, true ); | |
| var lowBeat = lowBeatDetector.tick( volume ); | |
| if(lowBeat) { | |
| // markSpectrogram("rgba(255, 255, 255, 0.2)"); | |
| } | |
| var rotationSpeed = Math.max((volume - 30) / 160, 0) * 6; | |
| setDebug("Volume", volume); | |
| setDebug("Rotation", rotationSpeed); | |
| setDebug("Beats", lowBeatDetector.beats.length); | |
| setDebug("BPM", lowBeatDetector.bpm); | |
| setDebug("Bth", lowBeatDetector.threshold); | |
| setDebug("Dst", scene.distortion); | |
| setDebug("Low", lowEnergy); | |
| setDebug("Mid", midEnergy); | |
| setDebug("Hgh", highEnergy); | |
| scene.circleRadius = (highFreqs / 150) * 100 + 5; | |
| scene.innerRadius = (midFreqs / 200) * 175 + 100; | |
| scene.color = spectrogramColor( 0.7, volume / 255 ); | |
| scene.number = Math.floor( scene.minNumber + (superLowFreqs / 255) * 100 ); | |
| scene.distortion = (lowEnergy + midEnergy + highEnergy); | |
| scene.rotation += rotationSpeed; | |
| } | |
| function drawScene(ctx, scene) { | |
| // Draw Circles | |
| if(scene.number == 0) return; | |
| var angle = scene.rotation; | |
| var angleOffset = 360 / scene.number; | |
| var DEGTORAD = Math.PI / 180; | |
| var _cx, _cy, distortionOffset; | |
| for(var i = 0; i < scene.number; i++) { | |
| distortionOffset = Math.cos( (i / scene.number) * Math.PI * 2 * Math.floor(scene.distortion * 4) + (time.now / 1000)) * scene.distortion * 1.5; | |
| _cx = scene.cx + Math.cos( angle * DEGTORAD ) * (scene.innerRadius + distortionOffset); | |
| _cy = scene.cy + Math.sin( angle * DEGTORAD ) * (scene.innerRadius + distortionOffset); | |
| ctx.strokeStyle = scene.color; | |
| ctx.beginPath(); | |
| ctx.arc(_cx, _cy, scene.circleRadius, 0, Math.PI * 2, true); | |
| ctx.lineStyle = "#000"; | |
| ctx.stroke(); | |
| angle += angleOffset; | |
| } | |
| } | |
| function setDebug(key, value) { | |
| debug[key] = value; | |
| } | |
| function drawDebug() { | |
| context.fillStyle = "#fff"; | |
| context.font = "16px sans-serif"; | |
| var index = 1; | |
| for(var key in debug) { | |
| var value = debug[ key ]; | |
| context.fillText(key + ": " + value, 10, index * 30); | |
| index++; | |
| } | |
| } | |
| function drawFreqs(freqs) { | |
| var color; | |
| var colors = { | |
| low: "#0c0", | |
| mid: "#f90", | |
| high: "#f00" | |
| }; | |
| var barWidth = canvas.width * 2 / settings.fft; | |
| for(var i = 0; i < freqs.length; i++) { | |
| if(i < settings.lowFreq) { | |
| color = colors["low"]; | |
| }else if(i < settings.midFreq) { | |
| color = colors["mid"]; | |
| }else{ | |
| color = colors["high"]; | |
| } | |
| var barHeight = freqs[i] / 4; | |
| context.fillStyle = color; | |
| context.fillRect(i * barWidth, canvas.height - barHeight - 3, barWidth, barHeight + 3); | |
| } | |
| } | |
| function gradientValue(grad, pos) { | |
| var s, e; | |
| for(var i = 1; i < grad.length; i++) { | |
| if( pos <= grad[i].value ) { | |
| s = grad[i-1]; | |
| e = grad[i]; | |
| break; | |
| } | |
| } | |
| var p = 1 - ( (pos - s.value) / (e.value - s.value) ); | |
| return (s.output * p) + (e.output * (1 - p)); | |
| } | |
| function spectrogramColor(pos, volume) { | |
| pos = Math.min( Math.max(pos, 0), 1 ); | |
| var hue = [ | |
| { value : 0, output : 300 }, | |
| { value : 0.25, output : 200 }, | |
| { value : 1, output : 100 } | |
| ]; | |
| var lum = [ | |
| { value : 0, output : 0 }, | |
| { value : 0.8, output : 19.5 }, | |
| { value : 1, output : 50 } | |
| ]; | |
| var h = gradientValue(hue, volume); | |
| var s = 100; | |
| var l = gradientValue(lum, pos); | |
| return "hsl(" + Math.floor(h) + "," + s + "%," + l + "%)"; | |
| } | |
| function drawSpectrogram(freqs, volume) { | |
| var height = canvas.height / freqs.length; | |
| // Draw | |
| for(var i = 0; i < freqs.length; i++) { | |
| spectrogram.fillStyle = spectrogramColor( freqs[i] / 255, volume / 255 ); | |
| spectrogram.fillRect(spectrogramPos, i * height, 1, height); | |
| } | |
| // Increment Pos | |
| spectrogramPos = (spectrogramPos + 1) % canvas.width; | |
| context.drawImage(spectrogramCanvas, 0, 0); | |
| } | |
| function markSpectrogram(color) { | |
| spectrogram.fillStyle = color; | |
| spectrogram.fillRect(spectrogramPos - 1, 0, 1, canvas.height); | |
| } | |
| function AudioStats(analyser) { | |
| this.analyser = analyser; | |
| } | |
| AudioStats.prototype.getStats = function() { | |
| var freqs = new Uint8Array( this.analyser.frequencyBinCount ); | |
| this.analyser.getByteFrequencyData( freqs ); | |
| return freqs; | |
| } | |
| AudioStats.prototype.getAverageVolume = function(freqs) { | |
| var sum = 0; | |
| for(var i = 0; i < freqs.length; i++) sum += freqs[i]; | |
| return sum / freqs.length; | |
| } | |
| AudioStats.prototype.getRangeVolume = function(freqs, low, high) { | |
| var volume = 0; | |
| for(var i = low; i <= high; i++) { | |
| volume += freqs[i]; | |
| } | |
| return volume / (high - low); | |
| } | |
| AudioStats.prototype.getRangeEnergy = function(freqs, low, high, norm) { | |
| var rangeSum = 0, | |
| sum = 0, | |
| energy = 0; | |
| for(var i = 0; i < freqs.length; i++) { | |
| if(i >= low && i <= high) rangeSum += freqs[i]; | |
| sum += freqs[ i ]; | |
| } | |
| energy = (sum > 0) ? (rangeSum / sum) : 0; | |
| if(norm) { | |
| energy /= (high - low) / freqs.length; | |
| } | |
| return energy; | |
| } | |
| AudioStats.prototype.getHighestFreq = function( freqs ) { | |
| var maxFreq = 0; | |
| var maxValue = 0; | |
| for(var i = 0; i < freqs.length; i++) { | |
| if(freqs[i] > maxValue) { | |
| maxFreq = i; | |
| maxValue = freqs[i]; | |
| } | |
| } | |
| return { | |
| freq : maxFreq, | |
| value : maxValue | |
| }; | |
| } | |
| function BeatDetector() { | |
| this.threshold = 0; | |
| this.decayRate = 1.01; | |
| this.lastBeatTime = time.now; | |
| this.beats = []; | |
| this.bpm = 0; | |
| this.sensitivity = 0.1; | |
| this.minSensitivity = 0.01; | |
| this.threshold = 0; | |
| this.decay = 0.9; | |
| } | |
| BeatDetector.prototype.calculateBPM = function() { | |
| if(this.beats.length == 0) return 0; | |
| if(this.beats.length == 1) return this.beats[0]; | |
| var diffs = []; | |
| for(var i = 1; i < this.beats.length; i++) { | |
| diffs.push( this.beats[i] - this.beats[i - 1] ); | |
| } | |
| var avg = diffs.reduce(function(sum, val) { return sum + val; }, 0) / diffs.length; | |
| return 60 / (avg / 1000); | |
| } | |
| BeatDetector.prototype.beat = function() { | |
| this.beats.push( time.now ); | |
| this.decay = 0; | |
| this.lastBeatTime = time.now; | |
| this.bpm = this.calculateBPM(); | |
| } | |
| BeatDetector.prototype.tick = function(value) { | |
| var beatDetected = false; | |
| var beatFactor = Math.abs(this.lastValue - value) / 255; | |
| if(beatFactor > this.threshold) { | |
| this.beat(); | |
| this.threshold = beatFactor; | |
| beatDetected = true; | |
| } | |
| this.applyDecay(); | |
| this.lastValue = value; | |
| return beatDetected; | |
| } | |
| BeatDetector.prototype.applyDecay = function() { | |
| var dt = time.now - this.lastBeatTime; | |
| this.threshold = this.sensitivity * (1 - (dt / 1000)); | |
| this.threshold = Math.max( this.threshold, this.minSensitivity ); | |
| } | |
| function normalizedSin(value) { | |
| return (Math.sin(value) + 1) / 2; | |
| } | |
| function normalizedCos(value) { | |
| return (Math.cos(value) + 1) / 2; | |
| } |
| *, | |
| *:before, | |
| *:after { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: sans-serif; | |
| } | |
| .modal { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| max-width: 90%; | |
| min-width: 400px; | |
| transform: translate(-50%, -50%); | |
| padding: 20px; | |
| background: #111; | |
| color: #bbb; | |
| z-index: 20; | |
| } | |
| .modal.is-hidden { | |
| display: none; | |
| } | |
| .modal h1 { | |
| margin: 0 0 0.5em 0; | |
| color: #fff; | |
| } | |
| .modal hr { | |
| display: block; | |
| background: #555; | |
| border: 0; | |
| height: 1px; | |
| margin: 1em 0; | |
| } | |
| .modal label { | |
| display: block; | |
| color: #999; | |
| margin-bottom: 4px; | |
| font-weight: 300; | |
| } | |
| .modal input { | |
| display: block; | |
| width: 100%; | |
| padding: 4px; | |
| font-size: 14px; | |
| font-weight: lighter; | |
| } | |
| .modal .button { | |
| display: inline-block; | |
| background: hsl(200, 100%, 50%); | |
| border: 0; | |
| font-size: 14px; | |
| padding: 8px 16px; | |
| cursor: pointer; | |
| } | |
| .modal .form-field + .form-field { | |
| margin-top: 10px; | |
| } | |
| .modal .drop-message { | |
| display: none; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: hsl(200, 100%, 50%); | |
| } | |
| .drop-message p { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 100%; | |
| text-align: center; | |
| margin: 0; | |
| transform: translate(-50%, -50%); | |
| color: #111; | |
| font-size: 2em; | |
| font-weight: 300; | |
| } | |
| .is-dragging .modal .drop-message { | |
| display: block; | |
| } | |
| .overlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.9); | |
| z-index: 10; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: 200ms ease-in opacity; | |
| } | |
| .is-dragging .overlay { | |
| opacity: 1; | |
| } | |
| #canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } |