├── .gitignore ├── README.md ├── equalizer.min.js └── src ├── index.html └── js ├── equalizer.js └── util └── pubsub.js /.gitignore: -------------------------------------------------------------------------------- 1 | src/style 2 | src/assets 3 | src/js/lib 4 | .DS_Store 5 | src/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

10-way equalizer

2 | 3 | An [article](http://habrahabr.ru/post/240819/) about how it works (_rus_)
4 | [List](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats) of supported formats.
5 | [Can i use](http://caniuse.com/#feat=audio-api) - support of Web Audio API 6 | 7 | [Try it online](http://martinschulz.github.io/equalizer/) 8 | 9 |

Usage:

10 | * Add script to your html: 11 | ```html 12 | 13 | 16 | ``` 17 | or _require_ it 18 | ```javascript 19 | require([ 20 | 'path/to/equalizer' 21 | ], function (equalizer) { 22 | // or here * 23 | }); 24 | ``` 25 | * Use it: 26 | ```javascript 27 | var equalizer = new Equalizer({ 28 | // css selector of your audio element or element itself (existing) 29 | audio: '.my-audio-element', 30 | // css selector of input elements or NodeList itself, if they already exist 31 | inputs: '.my-inputs', 32 | // otherwise, you can specify container element (existing). 33 | // input elements will be created and append to this container. 34 | // css selector of container element or element itself 35 | container: '.my-container' 36 | }); 37 | // You have to specify inputs (if they're already created) OR container, 38 | // if you want to create inputs dynamically 39 | // (jQuery objects are also allowed) 40 | 41 | // then you can turn off it 42 | equalizer.disconnect(); 43 | // and turn on 44 | equalizer.connect(); 45 | ``` 46 | 47 | Also there's couple of events: 48 | ```javascript 49 | new Equalizer({ 50 | /* param */ 51 | }).on('error', function (data) { 52 | // do some error stuff. i.e. hide controls 53 | console.log(data.message); 54 | }).on('change', function (data) { 55 | console.log('input no.' + data.index + ' changed. new value is ' + data.value); 56 | }); 57 | ``` 58 | Full list of events available for now: 59 | error, change, disconnect, connect 60 | 61 | 62 | -------------------------------------------------------------------------------- /equalizer.min.js: -------------------------------------------------------------------------------- 1 | (function(root,factory){"use strict";if(typeof define==="function"&&define.amd){define([],factory)}else if(typeof exports==="object"){module.exports=factory()}else{console.log("ok");root.equalizer=factory()}})(this,function(){"use strict";var context=null,audio=null,filters=[],$$=document.querySelectorAll.bind(document),$=document.querySelector.bind(document),createContext=function(){var previous=window&&window.equalizer;if(previous&&previous.context){context=previous.context}else{context=new AudioContext}},createInputs=function(className,container){var inputs=[],node,i;for(i=0;i<10;i++){node=document.createElement("input");node.className=className.slice(1);container.appendChild(node);inputs.push(node)}return inputs},initInputsData=function(inputs){[].forEach.call(inputs,function(item){item.setAttribute("min",-16);item.setAttribute("max",16);item.setAttribute("step",.1);item.setAttribute("value",0);item.setAttribute("type","range")})},initEvents=function(inputs){[].forEach.call(inputs,function(item,i){item.addEventListener("change",function(e){filters[i].gain.value=e.target.value},false)})},createFilter=function(frequency){var filter=context.createBiquadFilter();filter.type="peaking";filter.frequency.value=frequency;filter.gain.value=0;filter.Q.value=1;return filter},createFilters=function(){var frequencies=[60,170,310,600,1e3,3e3,6e3,12e3,14e3,16e3];filters=frequencies.map(function(frequency){return createFilter(frequency)});filters.reduce(function(prev,curr){prev.connect(curr);return curr})},validateParam=function(param){if(!param){throw new TypeError("equalizer: param required")}var container=$(param.container),inputs=$$(param.inputs);if(param.audio instanceof HTMLMediaElement){audio=param.audio}else if(typeof param.audio==="string"){audio=$(param.audio);if(!audio){throw new TypeError("equalizer: there's no element that match selector"+param.audio)}}else{throw new TypeError('equalizer: parameter "audio" must be string or an audio element')}if(!container&&!inputs.length){throw new TypeError("equalizer: there's no elements match \""+param.container+'" or "'+param.selector)}if(!inputs.length){inputs=createInputs(param.selector||"",container)}return inputs},bindEqualizer=function(){var source=context.createMediaElementSource(audio);source.connect(filters[0]);filters[9].connect(context.destination)},equalizer=function(param){var inputs=validateParam(param);createContext();createFilters();initInputsData(inputs);initEvents(inputs);bindEqualizer()};equalizer.context=context;return equalizer}); -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 37 | 38 | -------------------------------------------------------------------------------- /src/js/equalizer.js: -------------------------------------------------------------------------------- 1 | 2 | (function (root, factory) { 3 | 'use strict'; 4 | 5 | if (typeof define === 'function' && define.amd) { 6 | // AMD. Register as an anonymous module. 7 | define([], factory); 8 | } else if (typeof exports === 'object') { 9 | // Node. Does not work with strict CommonJS, but 10 | // only CommonJS-like environments that support module.exports, 11 | // like Node. 12 | module.exports = factory(); 13 | } else { 14 | // Browser globals (root is window) 15 | root.Equalizer = factory(); 16 | } 17 | 18 | }(this, function () { 19 | 'use strict'; 20 | 21 | var 22 | $$ = document.querySelectorAll.bind(document), 23 | $ = document.querySelector.bind(document), 24 | 25 | AudioContext = window.AudioContext || window.webkitAudioContext; 26 | 27 | var 28 | Equalizer = function (param) { 29 | // if it's not supported - do nothing 30 | if (!AudioContext) { 31 | this.trigger('error', { 32 | message: 'AudioContext not supported' 33 | }, true); 34 | 35 | return; 36 | } 37 | 38 | /** AudioContext object */ 39 | // avoid multiple AudioContext creating 40 | this.context = window.__context || new AudioContext(); 41 | window.__context = this.context; 42 | 43 | this.frequencies = [60, 170, 310, 600, 1000, 3000, 6000, 12000, 14000, 16000]; 44 | 45 | this.length = this.frequencies.length; 46 | 47 | this.connected = false; 48 | 49 | this.initInputs(param); 50 | 51 | this.createFilters(); 52 | this.initInputsAttrs(); 53 | this.initEvents(); 54 | this.connectEqualizer(); 55 | }; 56 | 57 | // <### pubsub ###> 58 | addPubsub(Equalizer); 59 | 60 | /** 61 | * creates input elements 62 | * @param {HTMLElement} container 63 | * @returns [HTMLElement] 64 | */ 65 | Equalizer.prototype.createInputs = function (container) { 66 | var 67 | inputs = [], 68 | node, 69 | i; 70 | 71 | for (i = 0; i < this.length; i++) { 72 | node = document.createElement('input'); 73 | container.appendChild(node); 74 | inputs.push(node); 75 | } 76 | 77 | return inputs; 78 | }; 79 | 80 | /** 81 | * init inputs range and step 82 | */ 83 | Equalizer.prototype.initInputsAttrs = function () { 84 | [].forEach.call(this.inputs, function (item) { 85 | item.setAttribute('min', -16); 86 | item.setAttribute('max', 16); 87 | item.setAttribute('step', 0.1); 88 | item.setAttribute('value', 0); 89 | item.setAttribute('type', 'range'); 90 | }); 91 | }; 92 | 93 | /** 94 | * bind input.change events to the filters 95 | */ 96 | Equalizer.prototype.initEvents = function () { 97 | var 98 | self = this; 99 | 100 | [].forEach.call(this.inputs, function (item, i) { 101 | item.addEventListener('change', function (e) { 102 | self.filters[i].gain.value = e.target.value; 103 | 104 | self.trigger('change', { 105 | value: e.target.value, 106 | inputElement: e.target, 107 | index: i 108 | }); 109 | }, false); 110 | }); 111 | }; 112 | 113 | /** 114 | * creates single BiquadFilter object 115 | * @param frequency {number} 116 | * @returns {BiquadFilter} 117 | */ 118 | Equalizer.prototype.createFilter = function (frequency) { 119 | var 120 | filter = this.context.createBiquadFilter(); 121 | 122 | filter.type = 'peaking'; 123 | filter.frequency.value = frequency; 124 | filter.gain.value = 0; 125 | filter.Q.value = 1; 126 | 127 | return filter; 128 | }; 129 | 130 | /** 131 | * create filter for each frequency and connect 132 | */ 133 | Equalizer.prototype.createFilters = function () { 134 | // create filters 135 | this.filters = this.frequencies.map(this.createFilter.bind(this)); 136 | 137 | // create chain 138 | this.filters.reduce(function (prev, curr) { 139 | prev.connect(curr); 140 | return curr; 141 | }); 142 | }; 143 | 144 | /** 145 | * check param and create input elements if necessary 146 | * 147 | * @holycrap {WTF} Why Must Life Be So Hard http://www.youtube.com/watch?v=rH48caFgZcI 148 | * 149 | * @returns {array|NodeList} input elements 150 | */ 151 | Equalizer.prototype.initInputs = function (param) { 152 | if (!param) { 153 | throw new TypeError('equalizer: param required'); 154 | } 155 | 156 | var 157 | container, 158 | inputs = []; 159 | 160 | if (param.audio instanceof HTMLMediaElement) { 161 | this.audio = param.audio; 162 | } else if (typeof param.audio === 'string') { 163 | this.audio = $(param.audio); 164 | 165 | if (!this.audio) { 166 | throw new TypeError('equalizer: there\'s no element that match selector' + 167 | param.audio); 168 | } 169 | } else { 170 | throw new TypeError('equalizer: parameter "audio" must be string or an audio element'); 171 | } 172 | 173 | // container specified 174 | if (param.container) { 175 | // css selector 176 | if (typeof param.container === 'string') { 177 | container = $(param.container); 178 | if (!container) { 179 | throw new TypeError('equalizer: there\'s no element that match selector' + 180 | param.container); 181 | } 182 | // plain html element 183 | } else if (param.container instanceof HTMLElement) { 184 | container = param.container; 185 | // jquery object 186 | } else if (param.container.jquery && param.container[0]) { 187 | container = param.container[0]; 188 | } else { 189 | throw new TypeError('equalizer: invalid parameter container: ' + param.container); 190 | } 191 | 192 | inputs = this.createInputs(container); 193 | } else { 194 | // no container 195 | if (typeof param.inputs === 'string') { 196 | inputs = $$(param.inputs); 197 | // plainh html collection 198 | } else if (param.inputs instanceof NodeList) { 199 | inputs = param.inputs; 200 | // jquery object 201 | } else if (param.inputs.jquery) { 202 | param.inputs.each(function (i, item) { 203 | inputs.push(item); 204 | }); 205 | } else { 206 | throw new TypeError('equalizer: invalid parameter inputs: ' + param.container); 207 | } 208 | } 209 | 210 | if (inputs.length !== this.length) { 211 | throw new TypeError('equalizer: required exactly ' + this.length + ' elements, but ' + 212 | inputs.length + ' found'); 213 | } 214 | 215 | this.inputs = inputs; 216 | }; 217 | 218 | /** 219 | * create a chain 220 | */ 221 | Equalizer.prototype.connectEqualizer = function () { 222 | this.source = this.context.createMediaElementSource(this.audio); 223 | 224 | this.source.connect(this.filters[0]); 225 | this.filters[this.length - 1].connect(this.context.destination); 226 | }; 227 | 228 | /** 229 | * turn off 230 | */ 231 | Equalizer.prototype.disconnect = function () { 232 | if (!this.connected) { 233 | return this; 234 | } 235 | 236 | this.trigger('disconnect', {}); 237 | this.source.disconnect(); 238 | this.source.connect(this.destination); 239 | 240 | return this; 241 | }; 242 | 243 | /** 244 | * turn on 245 | */ 246 | Equalizer.prototype.connect = function () { 247 | if (this.connected) { 248 | return this; 249 | } 250 | 251 | this.trigger('connect', {}); 252 | this.source.disconnect(); 253 | this.source.connect(this.filters[0]); 254 | 255 | return this; 256 | }; 257 | 258 | return Equalizer; 259 | })); -------------------------------------------------------------------------------- /src/js/util/pubsub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * incredibly simple pub-sub 3 | * @param {object} object - object to extend 4 | */ 5 | var addPubsub = function (object) { 6 | 'use strict'; 7 | 8 | object.prototype.on = function (eventName, callback) { 9 | if (!this.__callbacks) { 10 | this.__callbacks = {}; 11 | } 12 | 13 | if (!this.__callbacks[eventName]) { 14 | this.__callbacks[eventName] = []; 15 | } 16 | 17 | this.__callbacks[eventName].push(callback); 18 | if (this.__callbacks[eventName].deferred) { 19 | callback(this.__callbacks[eventName].data); 20 | } 21 | 22 | return this; 23 | }; 24 | 25 | object.prototype.trigger = function (eventName, data, deferred) { 26 | if (!this.__callbacks || !this.__callbacks[eventName]) { 27 | if (deferred) { 28 | this.__callbacks = this.__callbacks || {}; 29 | 30 | this.__callbacks[eventName] = []; 31 | this.__callbacks[eventName].deferred = true; 32 | this.__callbacks[eventName].data = data; 33 | } 34 | 35 | return; 36 | } 37 | 38 | this.__callbacks[eventName].forEach(function (callback) { 39 | callback(data); 40 | }); 41 | 42 | return this; 43 | }; 44 | }; --------------------------------------------------------------------------------