2
0
Эх сурвалжийг харах

Added SoundCloud song visualizer.

KrisVos130 9 жил өмнө
parent
commit
ae43f85d4f
3 өөрчлөгдсөн 396 нэмэгдсэн , 28 устгасан
  1. 0 1
      app/app.css
  2. 30 24
      app/app.js
  3. 366 3
      app/templates/room.html

+ 0 - 1
app/app.css

@@ -654,7 +654,6 @@ footer a:hover{
 }
 .hidden {
   visibility: hidden;
-  height: 0;
 }
 .footerButtons {
   background:none!important;

+ 30 - 24
app/app.js

@@ -215,16 +215,14 @@ if (Meteor.isClient) {
         },
         "click #toggle-video": function(e){
             e.preventDefault();
-            if (Session.get("videoHidden")) {
-                $("#player").removeClass("hidden");
+            if (Session.get("mediaHidden")) {
+                $("#media-container").removeClass("hidden");
                 $("#toggle-video").text("Hide video");
-                var player = document.getElementById("player");
-                player.style.height = (player.offsetWidth / 16 * 9) + "px";
-                Session.set("videoHidden", false);
+                Session.set("mediaHidden", false);
             } else {
-                $("#player").addClass("hidden");
+                $("#media-container").addClass("hidden");
                 $("#toggle-video").text("Show video");
-                Session.set("videoHidden", true);
+                Session.set("mediaHidden", true);
             }
         },
         "click #return": function(e){
@@ -588,25 +586,29 @@ if (Meteor.isClient) {
 
                 var volume = localStorage.getItem("volume") || 20;
 
+                $("#media-container").empty();
                 if (currentSong.type === "SoundCloud") {
-                  $("#player").attr("src", "")
-                  getSongInfo(currentSong);
-                  SC.stream("/tracks/" + currentSong.id + "#t=20s", function(sound){
-                    _sound = sound;
-                    sound.setVolume(volume / 100);
-                    sound.play();
-                    var interval = setInterval(function() {
-                        if (sound.getState() === "playing") {
-                            sound.seek(getTimeElapsed());
-                            window.clearInterval(interval);
-                        }
-                    }, 200);
-                    // Session.set("title", currentSong.title || "Title");
-                    // Session.set("artist", currentSong.artist || "Artist");
-                    Session.set("duration", currentSong.duration);
-                    resizeSeekerbar();
-                  });
+                    // Change id from visualizer to media-container
+                    $("#player").attr("src", "");
+                    getSongInfo(currentSong);
+                    SC.stream("/tracks/" + currentSong.id, function(sound){
+                        _sound = sound;
+                        sound.setVolume(volume / 100);
+                        startVisualizer(sound._player._html5Audio);
+                        sound.play();
+                        var interval = setInterval(function() {
+                            if (sound.getState() === "playing") {
+                                sound.seek(getTimeElapsed());
+                                window.clearInterval(interval);
+                            }
+                        }, 200);
+                        // Session.set("title", currentSong.title || "Title");
+                        // Session.set("artist", currentSong.artist || "Artist");
+                        Session.set("duration", currentSong.duration);
+                        resizeSeekerbar();
+                    });
                 } else {
+                    $("#media-container").append('<div id="player" class="embed-responsive-item"></div>');
                     if (yt_player === undefined) {
                         yt_player = new YT.Player("player", {
                             height: 540,
@@ -1071,6 +1073,10 @@ Router.route("/admin", {
     }
 });
 
+Router.route("/vis", {
+    template: "visualizer"
+});
+
 Router.route("/:type", {
     template: "room"
 });

+ 366 - 3
app/templates/room.html

@@ -16,9 +16,9 @@
               </div>
               <div class="row" id="song-media">
                 <div class="col-md-8">
-                  <div class="embed-responsive embed-responsive-16by9">
-                    <div id="player" class="embed-responsive-item"></div>
-                  </div>
+                    <div class="embed-responsive embed-responsive-16by9" id="media-container">
+                        <!--div id="player" class="embed-responsive-item"></div-->
+                    </div>
                 </div>
                 <div class="col-md-4" style="margin-top:15px">
                   <img class="song-img" id="song-img"/>
@@ -147,4 +147,367 @@
             <li></li>
         </ul>
     </div>
+    <script>
+        /**
+         * Created by Michael on 31/12/13.
+         */
+
+        /**
+         * The *AudioSource object creates an analyzer node, sets up a repeating function with setInterval
+         * which samples the input and turns it into an FFT array. The object has two properties:
+         * streamData - this is the Uint8Array containing the FFT data
+         * volume - cumulative value of all the bins of the streaData.
+         *
+         * The MicrophoneAudioSource uses the getUserMedia interface to get real-time data from the user's microphone. Not used currently but included for possible future use.
+         */
+
+        var SoundCloudAudioSource = function(player) {
+            var self = this;
+            var analyser;
+            var audioCtx = new (window.AudioContext || window.webkitAudioContext);
+            analyser = audioCtx.createAnalyser();
+            analyser.fftSize = 256;
+            player.crossOrigin = "anonymous";
+            var source = audioCtx.createMediaElementSource(player);
+            source.connect(analyser);
+            analyser.connect(audioCtx.destination);
+            var sampleAudioStream = function() {
+                analyser.getByteFrequencyData(self.streamData);
+                // calculate an overall volume value
+                var total = 0;
+                for (var i = 0; i < 80; i++) { // get the volume from the first 80 bins, else it gets too loud with treble
+                    total += self.streamData[i];
+                }
+                self.volume = total;
+            };
+            setInterval(sampleAudioStream, 20);
+            // public properties and methods
+            this.volume = 0;
+            this.streamData = new Uint8Array(128);
+        };
+        /**
+         * The Visualizer object, after being instantiated, must be initialized with the init() method,
+         * which takes an options object specifying the element to append the canvases to and the audiosource which will
+         * provide the data to be visualized.
+         */
+        var Visualizer = function() {
+            var tileSize;
+            var tiles = [];
+            var stars = [];
+            // canvas vars
+            var fgCanvas;
+            var fgCtx;
+            var fgRotation = 0.001;
+            var sfCanvas;
+            var sfCtx;
+            var audioSource;
+
+            function Polygon(sides, x, y, tileSize, ctx, num) {
+                this.sides = sides;
+                this.tileSize = tileSize;
+                this.ctx = ctx;
+                this.num = num; // the number of the tile, starting at 0
+                this.high = 0; // the highest colour value, which then fades out
+                this.decay = this.num > 42 ? 1.5 : 2; // increase this value to fade out faster.
+                this.highlight = 0; // for highlighted stroke effect;
+                // figure out the x and y coordinates of the center of the polygon based on the
+                // 60 degree XY axis coordinates passed in
+                var step = Math.round(Math.cos(Math.PI/6)*tileSize*2);
+                this.y = Math.round(step * Math.sin(Math.PI/3) * -y  );
+                this.x = Math.round(x * step + y * step/2 );
+
+                // calculate the vertices of the polygon
+                this.vertices = [];
+                for (var i = 1; i <= this.sides;i += 1) {
+                    x = this.x + this.tileSize * Math.cos(i * 2 * Math.PI / this.sides + Math.PI/6);
+                    y = this.y + this.tileSize * Math.sin(i * 2 * Math.PI / this.sides + Math.PI/6);
+                    this.vertices.push([x, y]);
+                }
+            }
+            Polygon.prototype.rotateVertices = function() {
+                // rotate all the vertices to achieve the overall rotational effect
+                var rotation = fgRotation;
+                rotation -= audioSource.volume > 10000 ? Math.sin(audioSource.volume/800000) : 0;
+                for (var i = 0; i <= this.sides-1;i += 1) {
+                    this.vertices[i][0] = this.vertices[i][0] -  this.vertices[i][1] * Math.sin(rotation);
+                    this.vertices[i][1] = this.vertices[i][1] +  this.vertices[i][0] * Math.sin(rotation);
+                }
+            };
+            var minMental = 0, maxMental = 0;
+            Polygon.prototype.calculateOffset = function(coords) {
+                var angle = Math.atan(coords[1]/coords[0]);
+                var distance = Math.sqrt(Math.pow(coords[0], 2) + Math.pow(coords[1], 2)); // a bit of pythagoras
+                var mentalFactor = Math.min(Math.max((Math.tan(audioSource.volume/6000) * 0.5), -20), 2); // this factor makes the visualization go crazy wild
+                /*
+                 // debug
+                 minMental = mentalFactor < minMental ? mentalFactor : minMental;
+                 maxMental = mentalFactor > maxMental ? mentalFactor : maxMental;*/
+                var offsetFactor = Math.pow(distance/3, 2) * (audioSource.volume/2000000) * (Math.pow(this.high, 1.3)/300) * mentalFactor;
+                var offsetX = Math.cos(angle) * offsetFactor;
+                var offsetY = Math.sin(angle) * offsetFactor;
+                offsetX *= (coords[0] < 0) ? -1 : 1;
+                offsetY *= (coords[0] < 0) ? -1 : 1;
+                return [offsetX, offsetY];
+            };
+            Polygon.prototype.drawPolygon = function() {
+                var bucket = Math.ceil(audioSource.streamData.length/tiles.length*this.num);
+                var val = Math.pow((audioSource.streamData[bucket]/255),2)*255;
+                val *= this.num > 42 ? 1.1 : 1;
+                // establish the value for this tile
+                if (val > this.high) {
+                    this.high = val;
+                } else {
+                    this.high -= this.decay;
+                    val = this.high;
+                }
+
+                // figure out what colour to fill it and then draw the polygon
+                var r, g, b, a;
+                if (val > 0) {
+                    this.ctx.beginPath();
+                    var offset = this.calculateOffset(this.vertices[0]);
+                    this.ctx.moveTo(this.vertices[0][0] + offset[0], this.vertices[0][1] + offset[1]);
+                    // draw the polygon
+                    for (var i = 1; i <= this.sides-1;i += 1) {
+                        offset = this.calculateOffset(this.vertices[i]);
+                        this.ctx.lineTo (this.vertices[i][0] + offset[0], this.vertices[i][1] + offset[1]);
+                    }
+                    this.ctx.closePath();
+
+                    if (val > 128) {
+                        r = (val-128)*2;
+                        g = ((Math.cos((2*val/128*Math.PI/2)- 4*Math.PI/3)+1)*128);
+                        b = (val-105)*3;
+                    }
+                    else if (val > 175) {
+                        r = (val-128)*2;
+                        g = 255;
+                        b = (val-105)*3;
+                    }
+                    else {
+                        r = ((Math.cos((2*val/128*Math.PI/2))+1)*128);
+                        g = ((Math.cos((2*val/128*Math.PI/2)- 4*Math.PI/3)+1)*128);
+                        b = ((Math.cos((2.4*val/128*Math.PI/2)- 2*Math.PI/3)+1)*128);
+                    }
+                    if (val > 210) {
+                        this.cubed = val; // add the cube effect if it's really loud
+                    }
+                    if (val > 120) {
+                        this.highlight = 100; // add the highlight effect if it's pretty loud
+                    }
+                    // set the alpha
+                    var e = 2.7182;
+                    a = (0.5/(1 + 40 * Math.pow(e, -val/8))) + (0.5/(1 + 40 * Math.pow(e, -val/20)));
+
+                    this.ctx.fillStyle = "rgba(" +
+                            Math.round(r) + ", " +
+                            Math.round(g) + ", " +
+                            Math.round(b) + ", " +
+                            a + ")";
+                    this.ctx.fill();
+                    // stroke
+                    if (val > 20) {
+                        var strokeVal = 20;
+                        this.ctx.strokeStyle =  "rgba(" + strokeVal + ", " + strokeVal + ", " + strokeVal + ", 0.5)";
+                        this.ctx.lineWidth = 1;
+                        this.ctx.stroke();
+                    }
+                }
+                // display the tile number for debug purposes
+                /*this.ctx.font = "bold 12px sans-serif";
+                 this.ctx.fillStyle = 'grey';
+                 this.ctx.fillText(this.num, this.vertices[0][0], this.vertices[0][1]);*/
+            };
+            Polygon.prototype.drawHighlight = function() {
+                this.ctx.beginPath();
+                // draw the highlight
+                var offset = this.calculateOffset(this.vertices[0]);
+                this.ctx.moveTo(this.vertices[0][0] + offset[0], this.vertices[0][1] + offset[1]);
+                // draw the polygon
+                for (var i = 0; i <= this.sides-1;i += 1) {
+                    offset = this.calculateOffset(this.vertices[i]);
+                    this.ctx.lineTo (this.vertices[i][0] + offset[0], this.vertices[i][1] + offset[1]);
+                }
+                this.ctx.closePath();
+                var a = this.highlight/100;
+                this.ctx.strokeStyle =  "rgba(255, 255, 255, " + a + ")";
+                this.ctx.lineWidth = 1;
+                this.ctx.stroke();
+                this.highlight -= 0.5;
+            };
+
+            var makePolygonArray = function() {
+                tiles = [];
+                /**
+                 * Arrange into a grid x, y, with the y axis at 60 degrees to the x, rather than
+                 * the usual 90.
+                 * @type {number}
+                 */
+                var i = 0; // unique number for each tile
+                tiles.push(new Polygon(6, 0, 0, tileSize, fgCtx, i)); // the centre tile
+                i++;
+                for (var layer = 1; layer < 7; layer++) {
+                    tiles.push(new Polygon(6, 0, layer, tileSize, fgCtx, i)); i++;
+                    tiles.push(new Polygon(6, 0, -layer, tileSize, fgCtx, i)); i++;
+                    for(var x = 1; x < layer; x++) {
+                        tiles.push(new Polygon(6, x, -layer, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -x, layer, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, x, layer-x, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -x, -layer+x, tileSize, fgCtx, i)); i++;
+                    }
+                    for(var y = -layer; y <= 0; y++) {
+                        tiles.push(new Polygon(6, layer, y, tileSize, fgCtx, i)); i++;
+                        tiles.push(new Polygon(6, -layer, -y, tileSize, fgCtx, i)); i++;
+                    }
+                }
+            };
+
+            function Star(x, y, starSize, ctx) {
+                this.x = x;
+                this.y = y;
+                this.angle = Math.atan(Math.abs(y)/Math.abs(x));
+                this.starSize = starSize;
+                this.ctx = ctx;
+                this.high = 0;
+            }
+            Star.prototype.drawStar = function() {
+                var distanceFromCentre = Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2));
+
+                // stars as lines
+                var brightness = 200 + Math.min(Math.round(this.high * 5), 55);
+                this.ctx.lineWidth= 0.5 + distanceFromCentre/2000 * Math.max(this.starSize/2, 1);
+                this.ctx.strokeStyle='rgba(' + brightness + ', ' + brightness + ', ' + brightness + ', 1)';
+                this.ctx.beginPath();
+                this.ctx.moveTo(this.x,this.y);
+                var lengthFactor = 1 + Math.min(Math.pow(distanceFromCentre,2)/30000 * Math.pow(audioSource.volume, 2)/6000000, distanceFromCentre);
+                var toX = Math.cos(this.angle) * -lengthFactor;
+                var toY = Math.sin(this.angle) * -lengthFactor;
+                toX *= this.x > 0 ? 1 : -1;
+                toY *= this.y > 0 ? 1 : -1;
+                this.ctx.lineTo(this.x + toX, this.y + toY);
+                this.ctx.stroke();
+                this.ctx.closePath();
+
+                // starfield movement coming towards the camera
+                var speed = lengthFactor/20 * this.starSize;
+                this.high -= Math.max(this.high - 0.0001, 0);
+                if (speed > this.high) {
+                    this.high = speed;
+                }
+                var dX = Math.cos(this.angle) * this.high;
+                var dY = Math.sin(this.angle) * this.high;
+                this.x += this.x > 0 ? dX : -dX;
+                this.y += this.y > 0 ? dY : -dY;
+
+                var limitY = fgCanvas.height/2 + 500;
+                var limitX = fgCanvas.width/2 + 500;
+                if ((this.y > limitY || this.y < -limitY) || (this.x > limitX || this.x < -limitX)) {
+                    // it has gone off the edge so respawn it somewhere near the middle.
+                    this.x = (Math.random() - 0.5) * fgCanvas.width/3;
+                    this.y = (Math.random() - 0.5) * fgCanvas.height/3;
+                    this.angle = Math.atan(Math.abs(this.y)/Math.abs(this.x));
+                }
+            };
+
+            var makeStarArray = function() {
+                var x, y, starSize;
+                stars = [];
+                var limit = fgCanvas.width / 15; // how many stars?
+                for (var i = 0; i < limit; i ++) {
+                    x = (Math.random() - 0.5) * fgCanvas.width;
+                    y = (Math.random() - 0.5) * fgCanvas.height;
+                    starSize = (Math.random()+0.1)*3;
+                    stars.push(new Star(x, y, starSize, sfCtx));
+                }
+            };
+
+            this.resizeCanvas = function() {
+                if (fgCanvas) {
+                    // resize the foreground canvas
+                    fgCanvas.width = window.innerWidth;
+                    fgCanvas.height = window.innerHeight;
+                    fgCtx.translate(fgCanvas.width/2,fgCanvas.height/2);
+
+                    // resize the starfield canvas
+                    sfCanvas.width = window.innerWidth;
+                    sfCanvas.height = window.innerHeight;
+                    sfCtx.translate(fgCanvas.width/2,fgCanvas.height/2);
+
+                    tileSize = fgCanvas.width > fgCanvas.height ? fgCanvas.width / 25 : fgCanvas.height / 25;
+
+                    makePolygonArray();
+                    makeStarArray()
+                }
+            };
+
+            var rotateForeground = function() {
+                tiles.forEach(function(tile) {
+                    tile.rotateVertices();
+                });
+            };
+
+            var draw = function() {
+                fgCtx.clearRect(-fgCanvas.width, -fgCanvas.height, fgCanvas.width*2, fgCanvas.height *2);
+                sfCtx.clearRect(-fgCanvas.width/2, -fgCanvas.height/2, fgCanvas.width, fgCanvas.height);
+
+                stars.forEach(function(star) {
+                    star.drawStar();
+                });
+                tiles.forEach(function(tile) {
+                    tile.drawPolygon();
+                });
+                tiles.forEach(function(tile) {
+                    if (tile.highlight > 0) {
+                        tile.drawHighlight();
+                    }
+                });
+
+                // debug
+                /* fgCtx.font = "bold 24px sans-serif";
+                 fgCtx.fillStyle = 'grey';
+                 fgCtx.fillText("minMental:" + minMental, 10, 10);
+                 fgCtx.fillText("maxMental:" + maxMental, 10, 40);*/
+                requestAnimationFrame(draw);
+            };
+
+            this.init = function(options) {
+                audioSource = options.audioSource;
+                var container = document.getElementById(options.containerId);
+
+                // foreground hexagons layer
+                fgCanvas = document.createElement('canvas');
+                fgCanvas.setAttribute('style', 'position: absolute; z-index: 10');
+                fgCanvas.setAttribute('class', 'embed-responsive-item');
+                fgCtx = fgCanvas.getContext("2d");
+                container.appendChild(fgCanvas);
+
+                // middle starfield layer
+                sfCanvas = document.createElement('canvas');
+                sfCtx = sfCanvas.getContext("2d");
+                sfCanvas.setAttribute('style', 'position: absolute; z-index: 5');
+                sfCanvas.setAttribute('class', 'embed-responsive-item');
+                container.appendChild(sfCanvas);
+
+                makePolygonArray();
+                makeStarArray();
+
+                this.resizeCanvas();
+                draw();
+
+                setInterval(rotateForeground, 20);
+                // resize the canvas to fill browser window dynamically
+                window.addEventListener('resize', this.resizeCanvas, false);
+            };
+        };
+
+        function startVisualizer(audioElement) {
+            // sound._player._html5Audio
+            var audioSource = new SoundCloudAudioSource(audioElement);
+            var visualizer = new Visualizer();
+            visualizer.init({
+                containerId: 'media-container',
+                audioSource: audioSource
+            });
+        }
+    </script>
 </template>