'use strict'; /* * dancer - v0.4.0 - 2014-02-01 * https://github.com/jsantell/dancer.js * Copyright (c) 2014 Jordan Santell * Licensed MIT */ (function () { var Dancer = function () { this.audioAdapter = Dancer._getAdapter(this); this.events = {}; this.sections = []; this.bind('update', update); }; Dancer.version = 'X.X.X'; Dancer.adapters = {}; Dancer.prototype = { load: function (source, micBoost, useMic) { // Loading an Audio element if (source instanceof HTMLElement) { this.source = source; // Loading an object with src, [codecs] } else if (source instanceof EventTarget) { this.source = source; } else { this.source = window.Audio ? new Audio() : {}; this.source.src = Dancer._makeSupportedPath(source.src, source.codecs); } this.useMic = useMic === true; this.boost = micBoost ? micBoost : 1; this.audio = this.audioAdapter.load(this.source, this.useMic, this.boost); return this; }, /* Controls */ play: function () { this.audioAdapter.play(); return this; }, pause: function () { this.audioAdapter.pause(); return this; }, setVolume: function (volume) { this.audioAdapter.setVolume(volume); return this; }, setBoost: function (boost) { this.audioAdapter.setBoost(boost); return this; }, /* Actions */ createKick: function (options) { return new Dancer.Kick(this, options); }, bind: function (name, callback) { if (!this.events[name]) { this.events[name] = []; } this.events[name].push(callback); return this; }, unbind: function (name) { if (this.events[name]) { delete this.events[name]; } return this; }, trigger: function (name) { var _this = this; if (this.events[name]) { this.events[name].forEach(function (callback) { callback.call(_this); }); } return this; }, /* Getters */ getVolume: function () { return this.audioAdapter.getVolume(); }, getProgress: function () { return this.audioAdapter.getProgress(); }, getTime: function () { return this.audioAdapter.getTime(); }, // Returns the magnitude of a frequency or average over a range of frequencies getFrequency: function (freq, endFreq) { var sum = 0; if (endFreq !== undefined) { for (var i = freq; i <= endFreq; i++) { sum += this.getSpectrum()[i]; } return sum / (endFreq - freq + 1); } else { return this.getSpectrum()[freq]; } }, getWaveform: function () { return this.audioAdapter.getWaveform(); }, getSpectrum: function () { return this.audioAdapter.getSpectrum(); }, isLoaded: function () { return this.audioAdapter.isLoaded; }, isPlaying: function () { return this.audioAdapter.isPlaying; }, /* Sections */ after: function (time, callback) { var _this = this; this.sections.push({ condition: function () { return _this.getTime() > time; }, callback: callback }); return this; }, before: function (time, callback) { var _this = this; this.sections.push({ condition: function () { return _this.getTime() < time; }, callback: callback }); return this; }, between: function (startTime, endTime, callback) { var _this = this; this.sections.push({ condition: function () { return _this.getTime() > startTime && _this.getTime() < endTime; }, callback: callback }); return this; }, onceAt: function (time, callback) { var _this = this, thisSection = null; this.sections.push({ condition: function () { return _this.getTime() > time && !this.called; }, callback: function () { callback.call(this); thisSection.called = true; }, called: false }); // Baking the section in the closure due to callback's this being the dancer instance thisSection = this.sections[this.sections.length - 1]; return this; } }; function update() { for (var i in this.sections) { if (this.sections[i].condition && this.sections[i].condition()) this.sections[i].callback.call(this); } } window.Dancer = Dancer; })(); (function (Dancer) { var CODECS = { 'mp3': 'audio/mpeg;', 'ogg': 'audio/ogg; codecs="vorbis"', 'wav': 'audio/wav; codecs="1"', 'aac': 'audio/mp4; codecs="mp4a.40.2"' }, audioEl = document.createElement('audio'); Dancer.options = {}; Dancer.setOptions = function (o) { for (var option in o) { if (o.hasOwnProperty(option)) { Dancer.options[option] = o[option]; } } }; Dancer.isSupported = function () { if (!window.Float32Array || !window.Uint32Array) { return null; } else if (!isUnsupportedSafari() && (window.AudioContext || window.webkitAudioContext)) { return 'webaudio'; } else { return ''; } }; Dancer.canPlay = function (type) { var canPlay = audioEl.canPlayType; return !!( type.toLowerCase() === 'mp3' || audioEl.canPlayType && audioEl.canPlayType(CODECS[type.toLowerCase()]).replace(/no/, '')); }; Dancer.addPlugin = function (name, fn) { if (Dancer.prototype[name] === undefined) { Dancer.prototype[name] = fn; } }; Dancer._makeSupportedPath = function (source, codecs) { if (!codecs) { return source; } for (var i = 0; i < codecs.length; i++) { if (Dancer.canPlay(codecs[i])) { return source + '.' + codecs[i]; } } return source; }; Dancer._getAdapter = function (instance) { switch (Dancer.isSupported()) { case 'webaudio': return new Dancer.adapters.webaudio(instance); default: return null; } }; Dancer._getMP3SrcFromAudio = function (audioEl) { var sources = audioEl.children; if (audioEl.src) { return audioEl.src; } for (var i = sources.length; i--;) { if ((sources[i].type || '').match(/audio\/mpeg/)) return sources[i].src; } return null; }; // Browser detection is lame, but Safari 6 has Web Audio API, // but does not support processing audio from a Media Element Source // https://gist.github.com/3265344 function isUnsupportedSafari() { var isApple = !!(navigator.vendor || '').match(/Apple/), version = navigator.userAgent.match(/Version\/([^ ]*)/); version = version ? parseFloat(version[1]) : 0; return isApple && version <= 6; } })(window.Dancer); (function (undefined) { var Kick = function (dancer, o) { o = o || {}; this.dancer = dancer; this.frequency = o.frequency !== undefined ? o.frequency : [0, 5]; this.threshold = o.threshold !== undefined ? o.threshold : 0.3; this.decay = o.decay !== undefined ? o.decay : 0.02; this.onKick = o.onKick; this.offKick = o.offKick; this.isOn = false; this.currentThreshold = this.threshold; this.previousMag = 0; this.canUseRatio = true; this.canUseRatioHandle = null; var _this = this; this.dancer.bind('update', function () { _this.onUpdate(); }); }; Kick.prototype = { on: function () { this.isOn = true; return this; }, off: function () { this.isOn = false; return this; }, set: function (o) { o = o || {}; this.frequency = o.frequency !== undefined ? o.frequency : this.frequency; this.threshold = o.threshold !== undefined ? o.threshold : this.threshold; this.decay = o.decay !== undefined ? o.decay : this.decay; this.onKick = o.onKick || this.onKick; this.offKick = o.offKick || this.offKick; }, onUpdate: function () { if (!this.isOn) { return; } var magnitude = this.maxAmplitude(this.frequency); if (magnitude >= this.currentThreshold && magnitude >= this.threshold) { this.currentThreshold = magnitude; this.onKick && this.onKick.call(this.dancer, magnitude); this.canUseRatio = false; if (this.canUseRatioHandle) { clearTimeout(this.canUseRatioHandle); this.canUseRatioHandle = null; } var self = this; this.canUseRatioHandle = setTimeout(function () { self.canUseRatio = true; }, 5000); } else { if (magnitude / this.previousMag > this.threshold * 5 && magnitude > 0.1 && this.canUseRatio) { this.onKick && this.onKick.call(this.dancer, magnitude, magnitude / this.previousMag); } else { this.offKick && this.offKick.call(this.dancer, magnitude); } this.currentThreshold -= this.decay; this.previousMag = (magnitude > 0) ? magnitude : 0.0001; } }, maxAmplitude: function (frequency) { var max = 0, fft = this.dancer.getSpectrum(); // Sloppy array check if (!frequency.length) { return frequency < fft.length ? fft[~~frequency] : null; } for (var i = frequency[0], l = frequency[1]; i <= l; i++) { if (fft[i] > max) { max = fft[i]; } } return max; } }; window.Dancer.Kick = Kick; })(); (function () { var SAMPLE_SIZE = 2048, SAMPLE_RATE = 44100; var adapter = function (dancer) { var context; if ('AudioContext' in window) { context = new AudioContext(); } else { context = new webkitAudioContext(); } this.dancer = dancer; this.audio = new Audio(); this.context = context; }; adapter.prototype = { load: function (_source, useMic, boost) { var _this = this; this.audio = _source; this.useMic = useMic; this.boost = boost; this.isLoaded = false; this.progress = 0; if (this.proc) { this.proc.onaudioprocess = null; delete this.proc; } this.proc = this.context.createScriptProcessor(SAMPLE_SIZE / 2, 1, 1); this.proc.onaudioprocess = function (e) { _this.update.call(_this, e); }; this.gain = this.context.createGain(); this.fft = new FFT(SAMPLE_SIZE / 2, SAMPLE_RATE, this.boost); this.signal = new Float32Array(SAMPLE_SIZE / 2); if (this.audio.readyState < 3) { this.audio.addEventListener('canplay', function () { connectContext.call(_this); }); } else { connectContext.call(_this); } this.audio.addEventListener('progress', function (e) { if (e.currentTarget.duration && e.currentTarget.duration !== Infinity) { _this.progress = e.currentTarget.seekable.end(0) / e.currentTarget.duration; } }); return this.audio; }, play: function () { this.audio.play(); this.isPlaying = true; }, pause: function () { this.audio.pause(); this.isPlaying = false; }, setVolume: function (volume) { this.gain.gain.value = volume; }, setBoost: function (boost) { if (this.fft) { this.fft.setBoost(boost); } this.boost = boost; }, getVolume: function () { return this.gain.gain.value; }, getProgress: function () { return this.progress; }, getWaveform: function () { return this.signal; }, getSpectrum: function () { return this.fft.spectrum; }, getTime: function () { return this.audio.currentTime; }, update: function (e) { if ((!this.isPlaying || !this.isLoaded) && this.useMic !== true) return; var buffers = [], channels = e.inputBuffer.numberOfChannels, resolution = SAMPLE_SIZE / channels, sum = function (prev, curr) { return prev[i] + curr[i]; }, i; for (i = channels; i--;) { buffers.push(e.inputBuffer.getChannelData(i)); } for (i = 0; i < resolution; i++) { this.signal[i] = channels > 1 ? buffers.reduce(sum) / channels : buffers[0][i]; } this.fft.forward(this.signal); this.dancer.trigger('update'); } }; function connectContext() { try { if (this.useMic) { this.source = this.context.createMediaStreamSource(this.audio); } else { this.source = this.context.createMediaElementSource(this.audio); } } catch (err) { console.info('Dancer: ' + err); return; } this.source.connect(this.proc); this.source.connect(this.gain); this.gain.connect(this.context.destination); this.proc.connect(this.context.destination); this.isLoaded = true; this.progress = 1; this.dancer.trigger('loaded'); } Dancer.adapters.webaudio = adapter; })(); /* * DSP.js - a comprehensive digital signal processing library for javascript * * Created by Corban Brook on 2010-01-01. * Copyright 2010 Corban Brook. All rights reserved. * */ // Fourier Transform Module used by DFT, FFT, RFFT function FourierTransform(bufferSize, sampleRate, boost) { this.bufferSize = bufferSize; this.sampleRate = sampleRate; this.bandwidth = 2 / bufferSize * sampleRate / 2; this.boost = boost ? boost : 1; this.spectrum = new Float32Array(bufferSize / 2); this.real = new Float32Array(bufferSize); this.imag = new Float32Array(bufferSize); this.peakBand = 0; this.peak = 0; /** * Calculates the *middle* frequency of an FFT band. * * @param {Number} index The index of the FFT band. * * @returns The middle frequency in Hz. */ this.getBandFrequency = function (index) { return this.bandwidth * index + this.bandwidth / 2; }; this.setBoost = function (boost) { this.boost = boost; }; this.calculateSpectrum = function () { var spectrum = this.spectrum, real = this.real, imag = this.imag, boost = this.boost, bSi = 2 / this.bufferSize, sqrt = Math.sqrt, rval, ival, mag; for (var i = 0, N = bufferSize / 2; i < N; i++) { rval = real[i]; ival = imag[i]; mag = bSi * sqrt(rval * rval + ival * ival); if (mag > this.peak) { this.peakBand = i; this.peak = mag; } spectrum[i] = mag * boost; } }; } /** * FFT is a class for calculating the Discrete Fourier Transform of a signal * with the Fast Fourier Transform algorithm. * * @param {Number} bufferSize The size of the sample buffer to be computed. Must be power of 2 * @param {Number} sampleRate The sampleRate of the buffer (eg. 44100) * @param {Number} boost The coefficient * * @constructor */ function FFT(bufferSize, sampleRate, boost) { FourierTransform.call(this, bufferSize, sampleRate, boost); this.reverseTable = new Uint32Array(bufferSize); var limit = 1; var bit = bufferSize >> 1; var i; while (limit < bufferSize) { for (i = 0; i < limit; i++) { this.reverseTable[i + limit] = this.reverseTable[i] + bit; } limit = limit << 1; bit = bit >> 1; } this.sinTable = new Float32Array(bufferSize); this.cosTable = new Float32Array(bufferSize); for (i = 0; i < bufferSize; i++) { this.sinTable[i] = Math.sin(-Math.PI / i); this.cosTable[i] = Math.cos(-Math.PI / i); } } /** * Performs a forward transform on the sample buffer. * Converts a time domain signal to frequency domain spectra. * * @param {Array} buffer The sample buffer. Buffer Length must be power of 2 * * @returns The frequency spectrum array */ FFT.prototype.forward = function (buffer) { // Locally scope variables for speed up var bufferSize = this.bufferSize, cosTable = this.cosTable, sinTable = this.sinTable, reverseTable = this.reverseTable, real = this.real, imag = this.imag, spectrum = this.spectrum; var k = Math.floor(Math.log(bufferSize) / Math.LN2); if (Math.pow(2, k) !== bufferSize) { throw "Invalid buffer size, must be a power of 2."; } if (bufferSize !== buffer.length) { throw "Supplied buffer is not the same size as defined FFT. FFT Size: " + bufferSize + " Buffer Size: " + buffer.length; } var halfSize = 1, phaseShiftStepReal, phaseShiftStepImag, currentPhaseShiftReal, currentPhaseShiftImag, off, tr, ti, tmpReal, i; for (i = 0; i < bufferSize; i++) { real[i] = buffer[reverseTable[i]]; imag[i] = 0; } while (halfSize < bufferSize) { //phaseShiftStepReal = Math.cos(-Math.PI/halfSize); //phaseShiftStepImag = Math.sin(-Math.PI/halfSize); phaseShiftStepReal = cosTable[halfSize]; phaseShiftStepImag = sinTable[halfSize]; currentPhaseShiftReal = 1; currentPhaseShiftImag = 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; } return this.calculateSpectrum(); }; chrome.storage.local.get('threshold', ({threshold}) => { threshold = threshold || 0.3; let dancer = new window.Dancer(), paused = false, simulateKick = (mag, ratioKickMag) => { console.log('mag: ' + mag + ', ratioKickMag: ' + ratioKickMag); }, kick = dancer.createKick({ threshold, onKick: (mag, ratioKickMag) => { if (paused === false) { simulateKick(mag, ratioKickMag); } } }); kick.on(); chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { if (request.action === 'start-listening') { chrome.tabCapture.capture({ audio: true, video: false }, function (stream) { let error = null; if (!stream) { error = chrome.runtime.lastError.message; } else { // let audio = new Audio(); // audio.src = URL.createObjectURL(stream); // audio.crossOrigin = "anonymous"; //dancer.load(audio, 1); } sendResponse({ error }); return true; }); } return true; }); }); // simulateKick(/*mag, ratioKickMag*/) { // let activeLights = this.get('activeLights'), // lightsData = this.get('lightsData'), // color = null, // transitiontime = this.get('flashingTransitions'), // stimulateLight = (light, brightness, hue) => { // let options = { 'bri': brightness }; // if (transitiontime) { // options['transitiontime'] = 0; // } else { // options['transitiontime'] = 1; // } // if (!isNone(hue)) { // options.hue = hue; // } // if (lightsData[light].state.on === false) { // options.on = true; // } // $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { // data: JSON.stringify(options), // contentType: 'application/json', // type: 'PUT' // }); // }, // timeToBriOff = 100; // if (activeLights.length > 0) { // let lastLightBopIndex = this.get('lastLightBopIndex'), // lightBopIndex, // brightnessRange = this.get('brightnessRange'), // light; // lightBopIndex = Math.floor(Math.random() * activeLights.length); // // let's try not to select the same light twice in a row // if (activeLights.length > 1) { // while (lightBopIndex === lastLightBopIndex) { // lightBopIndex = Math.floor(Math.random() * activeLights.length); // } // } // light = activeLights[lightBopIndex]; // this.set('lastLightBopIndex', lightBopIndex); // if (!this.get('colorloopMode')) { // let hueRange = this.get('hueRange'); // color = Math.floor(Math.random() * (hueRange[1] - hueRange[0] + 1) + hueRange[0]); // } // if (transitiontime) { // timeToBriOff = 80; // } // stimulateLight(light, brightnessRange[1], color); // later(this, stimulateLight, light, brightnessRange[0], timeToBriOff); // } // this.set('paused', true); // later(this, function () { // this.set('paused', false); // }, 150); // }