Skip to content
Browse files

Spectrum analyzer

  • Loading branch information...
1 parent 087b758 commit 39b3dcc986fc21b99539bb021144665b04e2c091 @tooxie committed Jan 27, 2013
Showing with 188 additions and 7 deletions.
  1. +6 −2 index.html
  2. +1 −1 js/controller.js
  3. +93 −0 js/fft.js
  4. +22 −4 js/playlist.js
  5. +61 −0 js/spectrum.js
  6. +5 −0 styles.css
View
8 index.html
@@ -11,6 +11,8 @@
<script type="text/javascript" src="js/playlist.js"></script>
<script type="text/javascript" src="js/controller.js"></script>
+ <script type="text/javascript" src="js/spectrum.js"></script>
+ <script type="text/javascript" src="js/fft.js"></script>
<script type="text/javascript" src="js/ngsortable.js"></script>
</head>
@@ -59,9 +61,11 @@ <h4 class="subheader">
</ul>
</div>
- <div class="panel radius" ng-show="playlist.config.spectrum && playlist.audio">
- AWESOME SPECTRUM MEGA ANALYZER
+ <div class="alert-box alert" ng-show="playlist.config.spectrum && playlist.audio && !playlist.config.canSpectrum">
+ Please use Mozilla Firefox to enable the spectrum analyzer.
</div>
+ <canvas id="spectrumcanvas" class="panel radius" ng-show="playlist.config.spectrum && playlist.audio && playlist.config.canSpectrum">
+ </canvas>
<h2 class="subheader">
Playlist
View
2 js/controller.js
@@ -38,7 +38,7 @@ Beyer.controller = function ($scope) {
}, false)
document.getElementById('seeker').addEventListener('click', function(e) {
- var offset = e.offsetX || e.offsetLeft,
+ var offset = e.target.offsetX || e.target.offsetLeft,
width = e.target.offsetWidth
percentage = ((offset / width) * 100).toFixed(2);
View
93 js/fft.js
@@ -0,0 +1,93 @@
+// FFT from dsp.js, see below
+Beyer.Spectrum.FFT = function(bufferSize, sampleRate) {
+ this.bufferSize = bufferSize;
+ this.sampleRate = sampleRate;
+ this.spectrum = new Float32Array(bufferSize/2);
+ this.real = new Float32Array(bufferSize);
+ this.imag = new Float32Array(bufferSize);
+ this.reverseTable = new Uint32Array(bufferSize);
+ this.sinTable = new Float32Array(bufferSize);
+ this.cosTable = new Float32Array(bufferSize);
+
+ var limit = 1,
+ bit = bufferSize >> 1;
+
+ while ( limit < bufferSize ) {
+ for ( var i = 0; i < limit; i++ ) {
+ this.reverseTable[i + limit] = this.reverseTable[i] + bit;
+ }
+
+ limit = limit << 1;
+ bit = bit >> 1;
+ }
+
+ for (var i = 0; i < bufferSize; i++) {
+ this.sinTable[i] = Math.sin(-Math.PI/i);
+ this.cosTable[i] = Math.cos(-Math.PI/i);
+ }
+};
+
+Beyer.Spectrum.FFT.prototype.forward = function(buffer) {
+ var bufferSize = this.bufferSize,
+ cosTable = this.cosTable,
+ sinTable = this.sinTable,
+ reverseTable = this.reverseTable,
+ real = this.real,
+ imag = this.imag,
+ spectrum = this.spectrum;
+
+ if (bufferSize !== buffer.length) {
+ throw "Supplied buffer is not the same size as defined FFT. FFT Size: " + bufferSize + " Buffer Size: " + buffer.length;
+ }
+
+ for (var i = 0; i < bufferSize; i++) {
+ real[i] = buffer[reverseTable[i]];
+ imag[i] = 0;
+ }
+
+ var halfSize = 1,
+ phaseShiftStepReal,
+ phaseShiftStepImag,
+ currentPhaseShiftReal,
+ currentPhaseShiftImag,
+ off,
+ tr,
+ ti,
+ tmpReal,
+ i;
+
+ while (halfSize < bufferSize) {
+ phaseShiftStepReal = cosTable[halfSize];
+ phaseShiftStepImag = sinTable[halfSize];
+ currentPhaseShiftReal = 1.0;
+ currentPhaseShiftImag = 0.0;
+
+ for (var fftStep = 0; fftStep < halfSize; fftStep++) {
+ i = fftStep;
+
+ while (i < bufferSize) {
+ off = i + halfSize;
+ tr = (currentPhaseShiftReal * real[off]) - (currentPhaseShiftImag * imag[off]);
+ ti = (currentPhaseShiftReal * imag[off]) + (currentPhaseShiftImag * real[off]);
+
+ real[off] = real[i] - tr;
+ imag[off] = imag[i] - ti;
+ real[i] += tr;
+ imag[i] += ti;
+
+ i += halfSize << 1;
+ }
+
+ tmpReal = currentPhaseShiftReal;
+ currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) - (currentPhaseShiftImag * phaseShiftStepImag);
+ currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) + (currentPhaseShiftImag * phaseShiftStepReal);
+ }
+
+ halfSize = halfSize << 1;
+ }
+
+ i = bufferSize/2;
+ while(i--) {
+ spectrum[i] = 2 * Math.sqrt(real[i] * real[i] + imag[i] * imag[i]) / bufferSize;
+ }
+};
View
26 js/playlist.js
@@ -8,7 +8,8 @@ Beyer.Playlist = {
loop: true,
shuffle: false,
muted: false,
- spectrum: false
+ spectrum: true,
+ canSpectrum: false
},
duration: {
minutes: 0,
@@ -80,22 +81,31 @@ Beyer.Playlist = {
this.setIndex(index);
}
+ if (this.audio) {
+ url.revokeObjectURL(this.audio.src);
+ }
+
this.audio = new Audio();
this.audio.src = src;
this.audio.load();
this.audio.muted = this.config.muted;
- this.audio.addEventListener('loadedmetadata', function(){
+ this.audio.addEventListener('loadedmetadata', function(e){
var minutes = Math.floor(this.duration / 60),
seconds = (Math.floor(this.duration) % 60).toFixed();
Beyer.Playlist.duration.seconds = (seconds.length < 2) ? "0" + seconds : seconds;
Beyer.Playlist.duration.minutes = minutes;
+ Beyer.Playlist.config.canSpectrum = Boolean(this.mozChannels);
+ if (Beyer.Playlist.config.canSpectrum) {
+ Beyer.Spectrum.init(this);
+ }
+
Beyer.scope.$apply();
}, false);
- this.audio.addEventListener('timeupdate', function() {
+ this.audio.addEventListener('timeupdate', function(e) {
var minutes = Math.floor(this.currentTime / 60),
seconds = (Math.floor(this.currentTime) % 60).toFixed() + '',
elapsed = (this.currentTime / this.duration) * 100;
@@ -107,6 +117,12 @@ Beyer.Playlist = {
Beyer.scope.$apply();
}, false);
+ this.audio.addEventListener('MozAudioAvailable', function(e) {
+ if (Beyer.Playlist.config.canSpectrum) {
+ Beyer.Spectrum.draw(e);
+ }
+ }, false);
+
this.audio.addEventListener('ended', function(){
Beyer.Playlist.next();
}, false);
@@ -205,8 +221,10 @@ Beyer.Playlist = {
}
},
reload: function() {
+ var url = window.URL || window.webkitURL;
+
this.pause();
- this.audio.src = '';
+ url.revokeObjectURL(this.audio.src);
this.audio = undefined;
this.load();
this.play();
View
61 js/spectrum.js
@@ -0,0 +1,61 @@
+Beyer.Spectrum = {
+ draw: function(e) {
+ var fb = e.frameBuffer,
+ t = e.time,
+ signal = new Float32Array(fb.length / this.channels),
+ spectrumLength = this.fft.spectrum.length,
+ barWidth = 2, // Hardcoded values for optimization
+ maxHeight = 200,
+ magnitude;
+
+ for (var i = 0, fbl = this.frameBufferLength / 2; i < fbl; i++) {
+ // Assuming interlaced stereo channels,
+ // need to split and merge into a stero-mix mono signal
+ signal[i] = (fb[2*i] + fb[2*i+1]) / 2;
+ }
+
+ this.fft.forward(signal);
+
+ // Clear the canvas before drawing spectrum
+ this.ctx.clearRect(0,0, this.canvas.offsetWidth, this.canvas.offsetHeight);
+
+ for (var i = 0; (i/2) * 6 < this.canvas.offsetWidth; i = i + 2) {
+ // multiply spectrum by a zoom value
+ magnitude = this.fft.spectrum[i] * 4000;
+
+ // Draw rectangle bars for each frequency bin
+ this.ctx.fillRect((i/2) * 6, this.canvas.offsetHeight, 4, -(magnitude > maxHeight ? maxHeight : magnitude));
+ }
+ },
+ init: function(audio) {
+ this.canvas = document.getElementById('spectrumcanvas');
+ this.ctx = this.canvas.getContext('2d');
+ this.channels = audio.mozChannels;
+ this.rate = audio.mozSampleRate;
+ this.frameBufferLength = audio.mozFrameBufferLength;
+ this.fft = new this.FFT(this.frameBufferLength / this.channels, this.rate);
+ }
+};
+
+/*
+var canvas = document.getElementById('spectrum'),
+ ctx = canvas.getContext('2d'),
+ channels,
+ rate,
+ frameBufferLength,
+ fft;
+
+function loadedMetadata() {
+ channels = audio.mozChannels;
+ rate = audio.mozSampleRate;
+ frameBufferLength = audio.mozFrameBufferLength;
+
+ fft = new FFT(frameBufferLength / channels, rate);
+}
+
+function audioAvailable(event) {
+}
+
+var audio = document.getElementById('audio-element');
+audio.addEventListener('MozAudioAvailable', audioAvailable, false);
+*/
View
5 styles.css
@@ -1,3 +1,8 @@
+canvas {
+ width: 100%;
+ height: 200px;
+}
+
#playlist .empty, #player {
text-align: center;
}

0 comments on commit 39b3dcc

Please sign in to comment.
Something went wrong with that request. Please try again.