diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a03b5f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sketch/* \ No newline at end of file diff --git a/css/index.css b/css/index.css new file mode 100644 index 0000000..031ca26 --- /dev/null +++ b/css/index.css @@ -0,0 +1,115 @@ +/* +Quentin Mazars-Simon +@quentinms +*/ + +*{ + margin: 0; + padding: 0; +} + +body{ + font-family: "Helvetica Neue", Helvetica, Arial; + font-weight: 100; +} + +#main_panel{ + +} + + +nav { + width: 250px; + float: left; + display: inline; + margin: 10px 10px 10px 100px; + padding: 0; + +} + +nav ul { + list-style: none; + text-align: center; +} + +nav ul li { + border-bottom: 1px solid black; +} + +nav ul li a{ + text-decoration: none; + color: inherit; + font-size: 30px; + +} + +h1{ + font-size: 45px; + font-weight: 100; + text-align: center; + margin: 20px; +} + +#newSongForm{ + float: left; + font-size: 20px; + margin: 5%; + display: none; +} + +#newSongForm input{ + font-size: 20px; + width: 400px; + font-family: "Helvetica Neue"; + font-weight: 100; + +} +#newSongForm button{ + + font-size: 20px; + width: 100px; + font-family: "Helvetica Neue"; + font-weight: 100; + margin: 20px auto 20px auto; + + position: relative; + left: 25%; +} + +#content{ + + +} + +#illustration{ + width: 500px; + height: 500px; + margin: auto; + background: url('http://lorempixel.com/500/500/abstract/'); + + background-repeat: no-repeat; + background-position: center center; + + /* + If do not want to always be squared + background-size: contain; + */ +} + +#playImage{ + margin: 200px; +} + +#time{ + width: 500px; + height: 20px; + margin: auto; + text-align: right; +} + +#waveform { + width: 500px; + height: 75px; + margin: auto; +} + diff --git a/img/pause.png b/img/pause.png new file mode 100644 index 0000000..f768ed5 Binary files /dev/null and b/img/pause.png differ diff --git a/img/play.png b/img/play.png new file mode 100644 index 0000000..3925aa5 Binary files /dev/null and b/img/play.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..d8e7f6e --- /dev/null +++ b/index.html @@ -0,0 +1,49 @@ + + + + + + +SoundCloud Video Clip + + + + + + + + + + + + +

SoundCloud Video Clip

+ + + +
+
+ +
+ +
+ +
+ +
+
+ Play/Pause icon +
+
+
+
+
+
+ + + diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..a13d976 --- /dev/null +++ b/js/index.js @@ -0,0 +1,253 @@ +/* + Quentin Mazars-Simon + @quentinms +*/ + + +/*Initialize Tumblr*/ + var initTumblr = function(tag){ + + //We get the list of post with the specified tag + + $.ajax({url: "http://api.tumblr.com/v2/blog/soundcloudvideoclip.tumblr.com/posts/photo?api_key=QXmLiokbFl8PFldxAlznGCKPIQ44oTf18NW2UqZzsjAIDWjp5h&tag="+tag+"&jsonp=?", dataType: "jsonp", success: function(data){ + + var tumblr_api_read = data.response || null; + + //We add those photos to an array + if (tumblr_api_read != null) { + for (var i = 0; i < tumblr_api_read.posts.length; i++) { + //We get the url of the images with a width of 500px + window.tumblrImages[i]=tumblr_api_read.posts[i]["photos"][0]["alt_sizes"][1]["url"]; + + //We preload the images + var image = $('').attr('src', window.tumblrImages[i]); + } + + } + + if(tumblrImages.length!=0){ + //Kinda random ordering; + tumblrImages.sort(function() {return 0.5 - Math.random();}); + } else { + tumblrImages[0]="http://lorempixel.com/500/500/abstract/"; + } + + //We set the illustration to be the first of the images. + $("#illustration").css("background-image", "url("+window.tumblrImages[0]+")"); }}); + + } + + +/*Initialize SoundCloud*/ + +var initSoundCloud = function(trackId){ + +//We get the track and initialize the waveform (see http://waveformjs.org) + SC.get("/tracks/"+trackId, function(track){ + var waveform = new Waveform({ + container: document.getElementById("waveform") + }); + waveform.dataFromSoundCloudTrack(track); + + /* We set the proper options for the stream*/ + var streamOptions = waveform.optionsForSyncedStream(); + streamOptions["ontimedcomments"] = function(comments){ + /* Each time there is a comment, it will show the next image */ + window.ImgCount=(window.ImgCount+1)%window.tumblrImages.length; + $("#illustration").css("background-image", "url("+window.tumblrImages[ImgCount]+")"); + }; + +//We stream the song + SC.stream(track.uri, streamOptions, function(stream){ + window.stream = stream; + + //Hack to display prelaod the song*/ + stream.play(); + stream.pause(); + + // Play/Pause each time the user clicks somewhere in the #content
(image + waveform). + $("#content").bind('click',function(){stream.togglePause(); togglePause();}) + + //Display time indication when the user hovers the waveform + $('#waveform').bind('mouseenter', function() { + this.iid = setInterval(function() { + + secP = '0'+parseInt(stream.position / 1000)%60; + secP = secP.slice(-2); + minP = parseInt(stream.position / 1000 / 60)%60; + + secD = '0'+parseInt(stream.duration / 1000)%60; + secD = secD.slice(-2); + minD = parseInt(stream.duration / 1000 / 60)%60; + + $("#time").text(minP+':'+secP+' / '+minD+':'+secD); + }, 500); + }).bind('mouseleave', function(){ + $("#time").text(""); + this.iid && clearInterval(this.iid); + }); + + + + + }); +}); + +} + + +var selectSong = function(url, tag){ + + //Reset a bunch of stuff + + window.ImgCount = 0; + window.tumblrImages = []; + window.paused = true; + $("#playImage").attr('src','img/play.png'); + $("#newSongForm").hide(); + $("#content").show(); + $('#waveform').empty(); + $('#waveform').unbind('mouseenter').unbind('mouseleave'); + $("#content").unbind('click'); + + if(window.stream != null){ + window.stream.destruct(); + } + + + //We get the new song id, initialize the stream, the waveform and get the images from Tumblr + SC.get('/resolve', { url: url }, function(track){ + initSoundCloud(track.id); + }) + + initTumblr(tag); + +} + +/*Utility functions*/ + +var togglePause = function(){ + + if(window.paused){ + $("#playImage").attr('src','img/pause.png'); + } else { + $("#playImage").attr('src','img/play.png'); + } + window.paused = ! window.paused; +} + +var showForm = function(){ + $("#content").hide(); + $("#newSongForm").show(); +} + +var hideForm = function(){ + $("#newSongForm").hide(); + $("#content").show(); +} + +//Generates the menu based on the list of songs +var generateNav = function(songs){ + + + $('nav').append($("")); + + for(var i = 0; i < songs.length; i++){ + + var item = $("
  • "+songs[i].title+"
  • "); + $('nav ul').append(item); + + } + + var item = $("
  • New Song...
  • "); + $('nav ul').append(item); + +} + +/*Main*/ + +var songs = [ { + "title": "Color Theory", + "artist": "Tea Leigh", + "url": "https://soundcloud.com/tea-leigh/color-theory-1", + "tag": "Color Theory" + }, + + { + "title": "Clouds", + "artist": "Marekhemmann", + "url": "https://soundcloud.com/marekhemmann/clouds", + "tag": "Clouds" + } , + { + "title": "Yonkers", + "artist": "Tyler The Creator", + "url": "https://soundcloud.com/diamondmedia360/tyler-the-creator-yonkers-prod", + "tag": "Yonkers" + } , + { + "title": "L.O.V.E", + "artist": "Onra", + "url": "https://soundcloud.com/onra/l-o-v-e", + "tag": "Love" + } , + { + "title": "Icarus", + "artist": "Madeon", + "url": "https://soundcloud.com/madeon/madeon-icarus", + "tag": "Icarus" + } , + { + "title": "Hours", + "artist": "Tycho", + "url": "https://soundcloud.com/tycho/tycho-hours", + "tag": "Hours" + } , + { + "title": "New Theory", + "artist": "RAC", + "url": "https://soundcloud.com/rac/washed-out-new-theory-rac-mix", + "tag": "New Theory" + } , + { + "title": "Too Insistent", + "artist": "Inertiamusic", + "url": "https://soundcloud.com/inertiamusic/the-do-too-insistent", + "tag": "Too Insistent" + } , + { + "title": "If U Got It", + "artist": "Chris Malinchak", + "url": "https://soundcloud.com/chrismalinchak/if-u-got-it", + "tag": "If U Got It" + } , + { + "title": "Pharao Black Magic", + "artist": "Future Classic", + "url": "https://soundcloud.com/futureclassic/pharao-black-magic-hermes", + "tag": "Pharao Black Magic" + } + + ]; + + +$(document).ready(function() { + + //Authenticate with SoundCloud + SC.initialize({ + client_id: "16be599a525a2df3fc4b5a20da9927c4" + }); + + var ImgCount = 0; + var tumblrImages = []; + var paused = true; + +$("#illustration").mouseenter(function(){$("#playImage").show();}) +$("#illustration").mouseleave(function(){$("#playImage").hide();}) + + +generateNav(songs); + +selectSong(songs[0].url,songs[0].tag); + + }); \ No newline at end of file diff --git a/js/waveform.js b/js/waveform.js new file mode 100644 index 0000000..5a0482d --- /dev/null +++ b/js/waveform.js @@ -0,0 +1,253 @@ +(function() { + var JSONP, Waveform, + __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + window.Waveform = Waveform = (function() { + + Waveform.name = 'Waveform'; + + function Waveform(options) { + this.redraw = __bind(this.redraw, this); + this.container = options.container; + this.canvas = options.canvas; + this.data = options.data || []; + this.outerColor = options.outerColor || "transparent"; + this.innerColor = options.innerColor || "#000000"; + this.interpolate = true; + if (options.interpolate === false) { + this.interpolate = false; + } + if (this.canvas == null) { + if (this.container) { + this.canvas = this.createCanvas(this.container, options.width || this.container.clientWidth, options.height || this.container.clientHeight); + } else { + throw "Either canvas or container option must be passed"; + } + } + this.patchCanvasForIE(this.canvas); + this.context = this.canvas.getContext("2d"); + this.width = parseInt(this.context.canvas.width, 10); + this.height = parseInt(this.context.canvas.height, 10); + if (options.data) { + this.update(options); + } + } + + Waveform.prototype.setData = function(data) { + return this.data = data; + }; + + Waveform.prototype.setDataInterpolated = function(data) { + return this.setData(this.interpolateArray(data, this.width)); + }; + + Waveform.prototype.setDataCropped = function(data) { + return this.setData(this.expandArray(data, this.width)); + }; + + Waveform.prototype.update = function(options) { + if (options.interpolate != null) { + this.interpolate = options.interpolate; + } + if (this.interpolate === false) { + this.setDataCropped(options.data); + } else { + this.setDataInterpolated(options.data); + } + return this.redraw(); + }; + + Waveform.prototype.redraw = function() { + var d, i, middle, t, _i, _len, _ref, _results; + this.clear(); + this.context.fillStyle = this.innerColor; + middle = this.height / 2; + i = 0; + _ref = this.data; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + d = _ref[_i]; + t = this.width / this.data.length; + if (typeof this.innerColor === "function") { + this.context.fillStyle = this.innerColor(i / this.width, d); + } + this.context.clearRect(t * i, middle - middle * d, t, middle * d * 2); + this.context.fillRect(t * i, middle - middle * d, t, middle * d * 2); + _results.push(i++); + } + return _results; + }; + + Waveform.prototype.clear = function() { + this.context.fillStyle = this.outerColor; + this.context.clearRect(0, 0, this.width, this.height); + return this.context.fillRect(0, 0, this.width, this.height); + }; + + Waveform.prototype.patchCanvasForIE = function(canvas) { + var oldGetContext; + if (typeof window.G_vmlCanvasManager !== "undefined") { + canvas = window.G_vmlCanvasManager.initElement(canvas); + oldGetContext = canvas.getContext; + return canvas.getContext = function(a) { + var ctx; + ctx = oldGetContext.apply(canvas, arguments); + canvas.getContext = oldGetContext; + return ctx; + }; + } + }; + + Waveform.prototype.createCanvas = function(container, width, height) { + var canvas; + canvas = document.createElement("canvas"); + container.appendChild(canvas); + canvas.width = width; + canvas.height = height; + return canvas; + }; + + Waveform.prototype.expandArray = function(data, limit, defaultValue) { + var i, newData, _i, _ref; + if (defaultValue == null) { + defaultValue = 0.0; + } + newData = []; + if (data.length > limit) { + newData = data.slice(data.length - limit, data.length); + } else { + for (i = _i = 0, _ref = limit - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { + newData[i] = data[i] || defaultValue; + } + } + return newData; + }; + + Waveform.prototype.linearInterpolate = function(before, after, atPoint) { + return before + (after - before) * atPoint; + }; + + Waveform.prototype.interpolateArray = function(data, fitCount) { + var after, atPoint, before, i, newData, springFactor, tmp; + newData = new Array(); + springFactor = new Number((data.length - 1) / (fitCount - 1)); + newData[0] = data[0]; + i = 1; + while (i < fitCount - 1) { + tmp = i * springFactor; + before = new Number(Math.floor(tmp)).toFixed(); + after = new Number(Math.ceil(tmp)).toFixed(); + atPoint = tmp - before; + newData[i] = this.linearInterpolate(data[before], data[after], atPoint); + i++; + } + newData[fitCount - 1] = data[data.length - 1]; + return newData; + }; + + Waveform.prototype.optionsForSyncedStream = function(options) { + var innerColorWasSet, that; + if (options == null) { + options = {}; + } + innerColorWasSet = false; + that = this; + return { + whileplaying: this.redraw, + whileloading: function() { + var stream; + if (!innerColorWasSet) { + stream = this; + that.innerColor = function(x, y) { + if (x < stream.position / stream.durationEstimate) { + return options.playedColor || "rgba(255, 102, 0, 0.8)"; + } else if (x < stream.bytesLoaded / stream.bytesTotal) { + return options.loadedColor || "rgba(0, 0, 0, 0.8)"; + } else { + return options.defaultColor || "rgba(0, 0, 0, 0.4)"; + } + }; + innerColorWasSet = true; + } + return this.redraw; + } + }; + }; + + Waveform.prototype.dataFromSoundCloudTrack = function(track) { + var _this = this; + return JSONP.get("http://waveformjs.org/w", { + url: track.waveform_url + }, function(data) { + return _this.update({ + data: data + }); + }); + }; + + return Waveform; + + })(); + + JSONP = (function() { + var config, counter, encode, head, jsonp, key, load, query, setDefaults, window; + load = function(url) { + var done, head, script; + script = document.createElement("script"); + done = false; + script.src = url; + script.async = true; + script.onload = script.onreadystatechange = function() { + if (!done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) { + done = true; + script.onload = script.onreadystatechange = null; + if (script && script.parentNode) { + return script.parentNode.removeChild(script); + } + } + }; + if (!head) { + head = document.getElementsByTagName("head")[0]; + } + return head.appendChild(script); + }; + encode = function(str) { + return encodeURIComponent(str); + }; + jsonp = function(url, params, callback, callbackName) { + var key, query; + query = ((url || "").indexOf("?") === -1 ? "?" : "&"); + params = params || {}; + for (key in params) { + if (params.hasOwnProperty(key)) { + query += encode(key) + "=" + encode(params[key]) + "&"; + } + } + jsonp = "json" + (++counter); + window[jsonp] = function(data) { + callback(data); + try { + delete window[jsonp]; + } catch (_error) {} + return window[jsonp] = null; + }; + load(url + query + (callbackName || config["callbackName"] || "callback") + "=" + jsonp); + return jsonp; + }; + setDefaults = function(obj) { + var config; + return config = obj; + }; + counter = 0; + head = void 0; + query = void 0; + key = void 0; + window = this; + config = {}; + return { + get: jsonp, + init: setDefaults + }; + })(); + +}).call(this); diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..789e2ef --- /dev/null +++ b/readme.md @@ -0,0 +1,17 @@ +# Soundcloud Video Clip + +## A Soundcloud-Tumblr Mashup + +This project is a collaboration between students from ECAL and EPFL. +The image displayed next to the song changes each time a comment occurs during the song. +The illustration are fetched from [this Tumblr](http://soundcloudvideoclip.tumblr.com) according to the tag of the song. + +### Demo +[http://www.quentin.ms/SoundcloudVideo/index.html](http://www.quentin.ms/SoundcloudVideo/index.html) + +### Used libraries +* [jQuery](jquery.com) +* [Soundcloud API](http://developers.soundcloud.com/docs/api/sdks#javascript) +* [waveform.js](waveformjs.org) + +Made by Quentin Mazars-Simon and Belinda \ No newline at end of file