Skip to content

Instantly share code, notes, and snippets.

@harunpehlivan
Created May 30, 2021 16:53
Show Gist options
  • Save harunpehlivan/7c8431b7bf5fe9850a10b195c113bfb6 to your computer and use it in GitHub Desktop.
Save harunpehlivan/7c8431b7bf5fe9850a10b195c113bfb6 to your computer and use it in GitHub Desktop.
SoundCloud Audio Vis: SVG Polygon
<div id=bg></div>
<svg id=svg></svg>
<header>
<input type=text id=playlist placeholder="SoundCloud Playlist URL" />
<button id=load>Load Playlist</button>
</header>
<footer>
<a href="http://harunpehlivan.fm.tc/">
<img src="https://res.cloudinary.com/tercuman-b-l-m-merkez/image/upload/v1606077286/favicon_bi8c66.png" />
</a>
</footer>
console.clear();
var user_id = "harun-pehl-van";
var list_id = "206730089";
var client_id = "e20d2e2dfe7b0084b6851cb0f7610a48";
var set = new SoundCloudSet({
client_id: client_id,
list_id: list_id
});
var input = document.getElementById("playlist");
var load = document.getElementById("load");
load.addEventListener("click", function() {
var url = input.value;
if(url) {
var get_list = listFromUrl(client_id, url);
get_list.then(function(r) {
console.log(r);
if(r.kind === "playlist") {
set.update(r.id);
} else {
alert("Must be a SoundCloud Playlist URL");
}
}, function(e) {
alert(e);
});
} else {
alert("Must be a SoundCloud Playlist URL");
}
});
function listFromUrl(client_id, value) {
var url = "https://api.soundcloud.com/resolve.json?url="+ encodeURI(value) +"&client_id=" + client_id;
var xmlhttp = new XMLHttpRequest();
return new Promise(function(res, rej) {
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
var r = JSON.parse(xmlhttp.responseText);
res(r);
} else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) {
rej("Could not load playlist \"" + value + "\"");
}
};
xmlhttp.open("GET", url, true);
xmlhttp.send();
});
}
function SoundCloudSet(params) {
var SCS = {};
init();
return SCS;
function init() {
SCS.SC = SC;
SCS.client_id = params.client_id;
SCS.SC.initialize({
client_id: SCS.client_id
});
SCS.update = update;
update(params.list_id);
}
function update(list_id) {
SCS.list_id = list_id;
if(!SCS.svg) {
setupVisualizer();
}
var load_list = loadList();
load_list.then(function(list) {
SCS.list = list;
generateInfo();
generateArt();
generateList();
}, function(err) {
alert(err);
});
}
function loadList() {
return new Promise(function(res, rej) {
SCS.SC.get("/playlists/" + SCS.list_id).then(function(list) {
if(list.embeddable_by === "all") {
res(list);
} else {
rej("You aren't allowed to embed this playlist");
}
});
});
}
function generateInfo() {
if(!SCS.info) {
SCS.info = document.createElement("section");
document.body.appendChild(SCS.info);
SCS.info.addEventListener("click", function(e) {
var el = e.target;
if(e.target.getAttribute("data-playing") === "true") {
pause();
}
});
} else {
SCS.info.innerHTML = "";
}
var title = document.createElement("h1");
var link = document.createElement("a");
link.href = SCS.list.permalink_url; console.log(SCS.list);
link.innerHTML = SCS.list.title + "<br><small>" + SCS.list.user.username + "</small>";
title.appendChild(link);
SCS.info.appendChild(title);
}
function generateArt() {
var image_source = SCS.list.artwork_url ? SCS.list.artwork_url : SCS.list.tracks[0].artwork_url;
var image_url = image_source.replace(/-large/, "-t500x500");
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = 500; canvas.height = 500;
var image = new Image();
image.crossOrigin = "anonymous";
image.src = image_url;
image.onload = function() {
SCS.info.style.backgroundImage = "url(" + image_url + ")";
context.drawImage(image, 0, 0);
SCS.image_data = context.getImageData(0, 0, 500, 500);
processImageData();
}
SCS.bg = document.getElementById("bg");
SCS.bg.style.backgroundImage = "url(" + image_url + ")";
}
function generateList() {
var list = document.createElement("ul");
for(var t = 0; t < SCS.list.tracks.length; t++) {
var track = SCS.list.tracks[t];
var $track = document.createElement("li");
var $artist = document.createElement("a");
$artist.href = track.user.permalink_url;
$artist.title = track.user.username;
$artist.style.backgroundImage = "url(" + track.user.avatar_url + ")";
var $song = document.createElement("span");
$song.innerHTML = track.title;
$track.appendChild($artist);
$track.appendChild($song);
list.appendChild($track);
$song.setAttribute("data-id", track.id);
$song.addEventListener("click", function(e) {
var curr = document.querySelector(".active");
if(curr) curr.className = "";
e.target.className = "active";
togglePlay(e.target.getAttribute("data-id"));
});
}
SCS.info.appendChild(list);
}
function processImageData() {
SCS.hsls = [];
var data = SCS.image_data.data;
for(var i = 0; i < data.length; i += 4) {
var hsl = RGBtoHSL(data[i], data[i + 1], data[i + 2]);
SCS.hsls.push(hsl);
}
}
function togglePlay(track_id) {
if(SCS.playing) {
if(SCS.current_track === track_id) {
pause();
} else {
play(track_id);
}
} else {
if(SCS.current_track === track_id) {
play();
} else {
play(track_id);
}
}
}
function pause() {
SCS.player.pause();
SCS.playing = false;
document.body.className = "";
SCS.info.setAttribute("data-playing", "false");
}
function play(track_id) {
document.body.className = "playing";
SCS.info.setAttribute("data-playing", "true");
SCS.playing = true;
updateVisualizer();
if(track_id) {
SCS.current_track = track_id;
var url = "https://api.soundcloud.com/tracks/" + track_id + "/stream?client_id=" + SCS.client_id;
loadAnalyser(url);
} else {
SCS.player.play();
}
}
function setupVisualizer() {
SCS.cvs = document.createElement("canvas");
SCS.ctx = SCS.cvs.getContext("2d");
SCS.cvs.width = 1024;
SCS.cvs.height = 200;
document.body.appendChild(SCS.cvs);
SCS.svg = document.getElementById("svg");
SCS.svgNS = SCS.svg.namespaceURI;
// SCS.polyline = document.createElementNS(SCS.svgNS, "polyline");
SCS.polygon_1 = document.createElementNS(SCS.svgNS, "polygon");
var di = Math.min(window.innerWidth, window.innerHeight);
SCS.di = di;
SCS.rad = di / 2;
SCS.max_height = di / 2;
SCS.tilt = -40;
SCS.choke = 130;
SCS.svg.setAttribute("width", SCS.di+"px");
SCS.svg.setAttribute("height", SCS.di+"px");
SCS.svg.setAttribute("viewBox", "0 0 " + SCS.di + " " + SCS.di);
// SCS.svg.appendChild(SCS.polyline);
SCS.svg.appendChild(SCS.polygon_1);
}
function loadAnalyser(url) {
SCS.player = SCS.player || new Audio();
SCS.player.src = url;
SCS.player.crossOrigin = "anonymous";
SCS.player.addEventListener("canplaythrough", function() {
if(!SCS.player_ctx) {
var AudioContext = window.AudioContext || window.webkitAudioContext;
SCS.player_ctx = new AudioContext();
var fftSize = 512;
SCS.framerate = 0;
SCS.analyser = (SCS.analyser || SCS.player_ctx.createAnalyser());
SCS.analyser.minDecibels = -90;
SCS.analyser.maxDecibels = -10;
SCS.analyser.smoothingTimeConstant = 0.3;//0.75;
SCS.analyser.fftSize = fftSize;
SCS.sourceNode = SCS.player_ctx.createMediaElementSource(SCS.player);
SCS.sourceNode.connect(SCS.analyser);
SCS.sourceNode.connect(SCS.player_ctx.destination);
}
SCS.player.play();
updateVisualizer();
});
}
function getPoints(freq_value, freq_sequence, freq_count) {
var freq_ratio = freq_sequence/freq_count,
x = (SCS.di - (SCS.tilt * 2)) * freq_ratio + SCS.tilt,
y = SCS.di / 2;
var // using power to increase highs and decrease lows
freq_ratio = freq_value / 255,
throttled_ratio = (freq_value - SCS.choke) / (255 - SCS.choke),
stroke_width = SCS.di / freq_count * 0.6 * throttled_ratio,
throttled_y = Math.max(throttled_ratio, 0) * SCS.max_height;
var loc_x = x - stroke_width / 2,
loc_y1 = y - throttled_y / 2,
loc_y2 = y + throttled_y / 2,
x_offset = SCS.tilt * throttled_ratio;
if (throttled_ratio > 0) {
var point_1 = (loc_x - x_offset) + "," + loc_y1,
point_2 = (loc_x + x_offset) + "," + loc_y2;
if(freq_sequence % 2 == 0) {
return point_1;
} else {
return point_2;
}
} else {
return loc_x + "," + y
}
}
function getPolygonPoints(freq_value, freq_sequence, freq_count, colorSequence) {
var freq_ratio = freq_sequence/freq_count,
x1 = (SCS.rad + Math.cos(freq_sequence * Math.PI / freq_count) * freq_value / 0.85),
y1 = (SCS.rad + Math.sin(freq_sequence * Math.PI / freq_count) * freq_value / 0.85),
x2 = (SCS.rad + Math.cos(freq_sequence * Math.PI / -freq_count) * freq_value / 0.85),
y2 = (SCS.rad + Math.sin(freq_sequence * Math.PI / -freq_count) * freq_value / 0.85);
// x = y, y = x to rotate 90deg.
// css rotation killed the frame rate.
return [y1 + "," + x1, y2 + "," + x2];
}
function updateVisualizer() {
if(SCS.framerate % 1 === 0) {
var buffer = new Uint8Array(SCS.analyser.frequencyBinCount);
SCS.analyser.getByteTimeDomainData(buffer);
// clear points array for polyline
// var points = [];
// clear points array for polygon
var points1 = [],
points2 = [];
var average = 0;
for (var i = 0; i < buffer.length; i++) {
var v = buffer[i];
// points.push(getPoints(v, i+1, buffer.length));
var ps = getPolygonPoints(buffer[i], i + 1, buffer.length, SCS.framerate);
points1.push( ps[0] );
points2.push( ps[1] );
average += buffer[i];
}
average /= buffer.length;
var avg_ratio = (average - 100) / (255 - 100);
// SCS.rms = Math.sqrt(average); // avg ratio should cover this.
var p = [points1, points2.reverse()].join(" ");
SCS.polygon_1.setAttribute("points", p);
// SCS.polyline.setAttribute("points", points.join(" "));
var hsl = SCS.hsls[Math.floor(Math.random() * SCS.hsls.length)];
var lit = Math.min(hsl[2] + avg_ratio * 60, 100);
var dark = Math.max(0, lit - 50);
var alpha = avg_ratio * 0.5 + 0.5;
var light_color = "hsla(" + hsl[0] + "," + hsl[1] + "%," + lit + "%, " + alpha + ")";
var dark_color = "hsl(" + hsl[0] + "," + hsl[1] + "%," + dark + "%)";
if(SCS.framerate % 16 === 0) {
// SCS.polyline.setAttribute("stroke", light_color);
SCS.polygon_1.setAttribute("stroke", light_color);
SCS.polygon_1.setAttribute("fill", light_color);
// SCS.bg.style.backgroundColor = dark_color;
}
// clear the current state
SCS.ctx.clearRect(0, 0, 1024, 200);
// set the fill style
SCS.ctx.fillStyle = "rgba(255,255,255,0.2)";
drawSpectrum(buffer);
// blur the bg
var blur = "blur(" + (avg_ratio * 8) + "px)";
SCS.bg.style.webkitFilter = blur;
SCS.bg.style.filter = blur;
}
if(SCS.playing) {
SCS.framerate += 1;
requestAnimationFrame(updateVisualizer);
}
}
function drawSpectrum(buffer) {
for(var i = 0; i < buffer.length; i += 2 ) {
var value = buffer[i];
var rat_x = i / (buffer.length - 2);
var rat_y = (value + 40) / 255;
var w = 4;
var x = (1024 - w) * rat_x;
SCS.ctx.fillRect(x, 100 - 50 * rat_y, w, 50 * rat_y * 2);
}
}
// Source: http://stackoverflow.com/questions/24218783/javascript-canvas-pixel-manipulation
function RGBtoHSL(r, g, b) {
var hsl = [];
var K = 0.0,
swap = 0;
if (g < b) {
swap = g;
g = b;
b = swap;
K = -1.0;
}
if (r < g) {
swap = r;
r = g;
g = swap;
K = -2.0 / 6.0 - K;
}
var chroma = r - (g < b ? g : b);
hsl[0] = Math.abs(K + (g - b) / (6.0 * chroma + 1e-20)) * 100;
hsl[1] = chroma / (r + 1e-20) * 100;
hsl[2] = r;
return hsl;
}
}
<script src="https://connect.soundcloud.com/sdk/sdk-3.0.0.js"></script>

SoundCloud Audio Vis: SVG Polygon

Chrome only. Uses SoundCloud Playlist data to grab an image and tracks. It then uses colors from the playlist image in the visualization. The shape is a single SVG polygon that has its points value updated on animation frame.

Background has a filter: blur relative to general volume to achieve feeling of movement at certain points in the song.

Load your own SoundCloud playlist!

Some fun playlists to load: https://soundcloud.com/amon-tobin/sets/isam https://soundcloud.com/teresasaurus-rex/sets/snap-judgment-favorites

A Pen by HARUN PEHLİVAN on CodePen.

License.

#svg {
display: block;
position: absolute;
width: 100%;
top: 50%; left: 50%;
z-index: 9;
transform-origin: 50% 50%;
transform: translate3d(-50%, -50%, 0);
polygon, polyline {
transition: stroke 200ms ease-in-out,
fill 200ms ease-in-out;
stroke-width: 1px;
}
}
header {
text-align: center;
margin-top: 1rem;
position: absolute;
top: 0; left: 0;
width: 100%;
z-index: 9;
transition: opacity 250ms ease-in-out;
.playing & { opacity: 0; }
input, button {
font-size: 0.8em;
border: none;
padding: 0.5em;
}
button {
background: #f50;
color: white;
}
input {
width: 200px
}
}
canvas {
position: absolute;
width: 100%;
max-width: 512px;
bottom: 0;
left: 50%;
transform: translateX(-50%);
}
h1 a {
color: white;
text-decoration: none;
}
body {
background: black;
}
section {
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: calc(50% - 256px); right: calc(50% - 256px);
z-index: 10;
width: 512px;
height: 512px;
transform-origin: 50% 50%;
transition:
width 500ms ease-out,
height 500ms ease-out,
top 500ms ease-out,
right 500ms ease-out;
> * {
transition: opacity 200ms;
}
.playing & {
right: 1rem;
top: 1rem;
width: 100px;
height: 100px;
overflow: hidden;
cursor: pointer;
&::after,
&::before {
content: "";
background: rgba(255,255,255,0.9);
border: 1px solid rgba(0,0,0,0.2);
position: absolute;
height: 50px;
width: 20px;
top: calc(50% - 25px);
}
&::before {
left: calc(50% - 25px);
}
&::after {
left: calc(50% + 5px);
}
&:hover { opacity: 0.9; }
> * { opacity: 0; pointer-events: none; }
}
background-size: cover;
background-position: center;
color: white;
font-size: 0.8rem;
h1, figure, ul {
width: 100%;
}
figure {
position: absolute;
z-index: 1;
top: 0;
margin: 0;
}
img {
width: 100%;
height: auto;
vertical-align: middle;
}
h1, ul {
z-index: 2;
}
h1 {
position: absolute;
top: 100%;
text-align: center;
}
ul { flex: 1; }
ul {
position: relative;
list-style: none;
margin: 0;
padding: 0;
line-height: 30px;
display: flex;
flex-wrap: wrap;
align-items: stretch;
flex-direction: column;
justify-content: space-between;
li {
width: 100%;
flex: 1;
box-sizing: border-box;
display: flex;
line-height: 1.3;
&:hover {
transform: scale(1.05);
box-shadow: 0px 0px 6px rgba(0,0,0,0.2);
z-index: 9;
}
&:hover span {
background: rgba(#f50,0.9);
}
+ li { border-top: 1px solid rgba(#222,0.9); }
}
a {
display: block;
flex: 1;
background-position: center;
background-size: cover;
vertical-align: center;
position: relative;
&:hover::after {
content: "";
background: rgba(0,0,0,0.5);
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
}
}
span {
display: block;
cursor: pointer;
flex: 11;
padding-left: 0.5rem;
background: rgba(#000,0.7);
display: flex;
justify-content: center;
flex-direction: column;
}
button {
box-sizing: border-box;
border: none;
padding: 0;
}
}
}
footer {
position: absolute;
z-index: 9;
bottom: 1rem; right: 1rem;
a {
}
}
#bg {
background-size: cover;
background-position: center;
position: fixed;
background-blend-mode: hard-light;
top: 0; right: 0; bottom: 0; left: 0;
z-index: -1;
transform: scale(1.1);
opacity: 0.4;
transition: background-color 400ms ease-in-out;
}
html, body {
height: 100%;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment