├── .gitignore ├── index.js ├── package.json ├── readme.md ├── test.js └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | //this will affect all the git repos 2 | git config --global core.excludesfile ~/.gitignore 3 | 4 | 5 | //update files since .ignore won't if already tracked 6 | git rm --cached 7 | 8 | # Compiled source # 9 | ################### 10 | *.com 11 | *.class 12 | *.dll 13 | *.exe 14 | *.o 15 | *.so 16 | 17 | # Packages # 18 | ############ 19 | # it's better to unpack these files and commit the raw source 20 | # git has its own built in compression methods 21 | *.7z 22 | *.dmg 23 | *.gz 24 | *.iso 25 | *.jar 26 | *.rar 27 | *.tar 28 | *.zip 29 | 30 | # Logs and databases # 31 | ###################### 32 | *.log 33 | *.sql 34 | *.sqlite 35 | 36 | # OS generated files # 37 | ###################### 38 | .DS_Store 39 | .DS_Store? 40 | ._* 41 | .Spotlight-V100 42 | .Trashes 43 | # Icon? 44 | ehthumbs.db 45 | Thumbs.db 46 | .cache 47 | .project 48 | .settings 49 | .tmproj 50 | *.esproj 51 | nbproject 52 | 53 | # Numerous always-ignore extensions # 54 | ##################################### 55 | *.diff 56 | *.err 57 | *.orig 58 | *.rej 59 | *.swn 60 | *.swo 61 | *.swp 62 | *.vi 63 | *~ 64 | *.sass-cache 65 | *.grunt 66 | *.tmp 67 | 68 | # Dreamweaver added files # 69 | ########################### 70 | _notes 71 | dwsync.xml 72 | 73 | # Komodo # 74 | ########################### 75 | *.komodoproject 76 | .komodotools 77 | 78 | # Node # 79 | ##################### 80 | node_modules 81 | 82 | # Bower # 83 | ##################### 84 | bower_components 85 | 86 | # Folders to ignore # 87 | ##################### 88 | .hg 89 | .svn 90 | .CVS 91 | intermediate 92 | publish 93 | .idea 94 | .graphics 95 | _test 96 | _archive 97 | uploads 98 | tmp 99 | 100 | # Vim files to ignore # 101 | ####################### 102 | .VimballRecord 103 | .netrwhist 104 | 105 | bundle.* 106 | 107 | _demo -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Pass-through/sink node analysing data. 3 | * 4 | * @module audio-analyser 5 | */ 6 | 7 | 8 | var inherits = require('inherits'); 9 | var Transform = require('stream').Transform; 10 | var extend = require('xtend/mutable'); 11 | var pcm = require('pcm-util'); 12 | var fft = require('ndarray-fft'); 13 | var ndarray = require('ndarray'); 14 | var db = require('decibels/from-gain'); 15 | var blackman = require('scijs-window-functions/blackman'); 16 | 17 | 18 | /** 19 | * @constructor 20 | */ 21 | function Analyser (options) { 22 | if (!(this instanceof Analyser)) return new Analyser(options); 23 | 24 | var self = this; 25 | 26 | Transform.call(self, options); 27 | 28 | //overwrite options 29 | extend(self, options); 30 | 31 | //time data buffer 32 | self._data = []; 33 | 34 | //frequency data 35 | self._fdata = new Float32Array(self.fftSize); 36 | 37 | //data counters 38 | self._timeoutCount = 0; 39 | self._fftCount = 0; 40 | } 41 | 42 | 43 | /** Inherit transform */ 44 | inherits(Analyser, Transform); 45 | 46 | 47 | /** Get PCM format */ 48 | extend(Analyser.prototype, pcm.defaultFormat); 49 | 50 | 51 | /** Magnitude diapasone, in dB **/ 52 | Analyser.prototype.minDecibels = -100; 53 | Analyser.prototype.maxDecibels = 0; 54 | 55 | 56 | /** Number of points to grab **/ 57 | Analyser.prototype.fftSize = 1024; 58 | 59 | /** Smoothing */ 60 | Analyser.prototype.smoothingTimeConstant = 0.2; 61 | 62 | /** Number of points to plot */ 63 | Analyser.prototype.frequencyBinCount = 1024/2; 64 | 65 | /** Throttle each N ms */ 66 | Analyser.prototype.throttle = 50; 67 | 68 | /** Size of data to buffer, 1s by default */ 69 | Analyser.prototype.bufferSize = 44100; 70 | 71 | /** Channel to capture */ 72 | Analyser.prototype.channel = 0; 73 | 74 | /** 75 | * Windowing function 76 | * Same as used by chromium, but can be any from: 77 | * https://github.com/scijs/window-functions 78 | */ 79 | Analyser.prototype.applyWindow = blackman; 80 | 81 | 82 | /** 83 | * Basically pass through 84 | * but provide small delays to avoid blocking timeouts for rendering 85 | */ 86 | Analyser.prototype._transform = function (chunk, enc, cb) { 87 | var self = this; 88 | self.push(chunk); 89 | self._capture(chunk, cb); 90 | }; 91 | 92 | 93 | /** 94 | * If pipes count is 0 - don’t stack data 95 | */ 96 | Analyser.prototype._write = function (chunk, enc, cb) { 97 | var self = this; 98 | if (!self._readableState.pipesCount) { 99 | self._capture(chunk, cb); 100 | //just emulate data event 101 | self.emit('data', chunk); 102 | } else { 103 | Transform.prototype._write.call(this, chunk, enc, cb); 104 | } 105 | }; 106 | 107 | 108 | /** 109 | * Capture chunk of data for rendering 110 | */ 111 | Analyser.prototype._capture = function (chunk, cb) { 112 | var self = this; 113 | 114 | //get channel data converting the input 115 | var channelData = pcm.getChannelData(chunk, self.channel, self).map(function (sample) { 116 | return pcm.convertSample(sample, self, {float: true}); 117 | }); 118 | 119 | //shift data & ensure size 120 | self._data = self._data.concat(channelData).slice(-self.bufferSize); 121 | 122 | //increase count 123 | self._timeoutCount += channelData.length; 124 | self._fftCount += channelData.length; 125 | 126 | //perform fft, if enough new data 127 | if (self._fftCount >= self.fftSize) { 128 | self._fftCount = 0; 129 | 130 | var input = self._data.slice(-self.fftSize); 131 | 132 | //do windowing 133 | for (var i = 0; i < self.fftSize; i++) { 134 | input[i] *= self.applyWindow(i, self.fftSize); 135 | } 136 | 137 | //create complex parts 138 | var inputRe = ndarray(input); 139 | var inputIm = ndarray(new Float32Array(self.fftSize)); 140 | 141 | //do fast fourier transform 142 | fft(1, inputRe, inputIm); 143 | 144 | //apply smoothing factor 145 | var k = Math.min(1, Math.max(self.smoothingTimeConstant, 0)); 146 | 147 | //for magnitude imaginary component is blown away. Not necessary though. 148 | for (var i = 0; i < self.fftSize; i++) { 149 | self._fdata[i] = k* self._fdata[i] + (1 - k) * Math.abs(inputRe.get(i)) / self.fftSize; 150 | } 151 | } 152 | 153 | //meditate for a processor tick each 50ms to let something other happen 154 | if (self.throttle && self._timeoutCount / self.sampleRate > self.throttle / 1000) { 155 | self._timeoutCount %= Math.floor(self.sampleRate / self.throttle); 156 | setTimeout(cb); 157 | } else { 158 | cb(); 159 | } 160 | 161 | }; 162 | 163 | 164 | /** 165 | * AudioAnalyser methods 166 | */ 167 | Analyser.prototype.getFloatFrequencyData = function (arr) { 168 | var self = this; 169 | 170 | if (!arr) return arr; 171 | 172 | var minDb = self.minDecibels, maxDb = self.maxDecibels; 173 | 174 | for (var i = 0, l = Math.min(self.frequencyBinCount, arr.length); i < l; i++) { 175 | arr[i] = Math.max(db(self._fdata[i]), minDb); 176 | } 177 | 178 | return arr; 179 | }; 180 | 181 | 182 | Analyser.prototype.getByteFrequencyData = function (arr) { 183 | var self = this; 184 | 185 | if (!arr) return arr; 186 | 187 | var minDb = self.minDecibels, maxDb = self.maxDecibels; 188 | var rangeScaleFactor = maxDb === minDb ? 1 : 1 / (maxDb - minDb); 189 | 190 | for (var i = 0, l = Math.min(self.frequencyBinCount, arr.length); i < l; i++) { 191 | var mg = Math.max(db(self._fdata[i]), minDb); 192 | 193 | //the formula is from the chromium source 194 | var scaledValue = 255 * (mg - minDb) * rangeScaleFactor; 195 | 196 | arr[i] = scaledValue; 197 | } 198 | 199 | return arr; 200 | }; 201 | 202 | 203 | 204 | Analyser.prototype.getFloatTimeDomainData = function (arr) { 205 | var self = this; 206 | 207 | if (!arr) return arr; 208 | var size = Math.min(arr.length, self.fftSize); 209 | 210 | for (var c = 0, i = self._data.length - self.fftSize, l = i + size; i < l; i++, c++) { 211 | arr[c] = self._data[i]; 212 | } 213 | 214 | return arr; 215 | }; 216 | 217 | 218 | Analyser.prototype.getByteTimeDomainData = function (arr) { 219 | var self = this; 220 | 221 | if (!arr) return arr; 222 | var size = Math.min(arr.length, self.fftSize); 223 | 224 | for (var c = 0, i = self._data.length - self.fftSize, l = i + size; i < l; i++, c++) { 225 | arr[c] = pcm.convertSample(self._data[i], {float: true}, {signed: false, bitDepth: 8}); 226 | } 227 | 228 | return arr; 229 | }; 230 | 231 | 232 | Analyser.prototype.getFrequencyData = function (size) { 233 | var self = this; 234 | var result = []; 235 | var minDb = self.minDecibels, maxDb = self.maxDecibels; 236 | 237 | size = size || self.frequencyBinCount; 238 | 239 | size = Math.min(size, self._fdata.length); 240 | 241 | for (var i = 0; i < size; i++) { 242 | result.push(Math.max(db(self._fdata[i]), minDb)); 243 | } 244 | 245 | return result; 246 | }; 247 | 248 | 249 | Analyser.prototype.getTimeData = function (size) { 250 | var result = []; 251 | 252 | size = size || this.fftSize; 253 | 254 | size = Math.min(size, this._data.length); 255 | 256 | return this._data.slice(-size); 257 | }; 258 | 259 | 260 | module.exports = Analyser; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-analyser", 3 | "version": "1.0.2", 4 | "description": "Audio analyser stream", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js" 8 | ], 9 | "scripts": { 10 | "test": "node test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/audio-lab/analyser" 15 | }, 16 | "keywords": [ 17 | "analysernode", 18 | "web-audio", 19 | "web-audio-api", 20 | "audio", 21 | "sound", 22 | "pcm", 23 | "dsp" 24 | ], 25 | "author": "Deema Yvanow ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/audio-lab/analyser/issues" 29 | }, 30 | "homepage": "https://github.com/audio-lab/analyser", 31 | "dependencies": { 32 | "decibels": "^1.0.1", 33 | "inherits": "^2.0.1", 34 | "ndarray": "^1.0.18", 35 | "ndarray-fft": "^1.0.0", 36 | "pcm-util": "^1.1.10", 37 | "scijs-window-functions": "^2.0.2", 38 | "xtend": "^4.0.1" 39 | }, 40 | "devDependencies": { 41 | "almost-equal": "^1.0.0", 42 | "audio-generator": "^1.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Audio analyser stream. Provides API of the [AnalyserNode](https://developer.mozilla.org/en/docs/Web/API/AnalyserNode) for audio-streams. In all respects can be used in the same way. 2 | 3 | ## Usage 4 | 5 | [![npm install audio-analyser](https://nodei.co/npm/audio-analyser.png?mini=true)](https://npmjs.org/package/audio-analyser) 6 | 7 | ```js 8 | var Analyser = require('audio-analyser'); 9 | var Generator = require('audio-generator'); 10 | 11 | 12 | var analyser = new Analyser({ 13 | // Magnitude diapasone, in dB 14 | minDecibels: -100, 15 | maxDecibels: -30, 16 | 17 | // Number of time samples to transform to frequency 18 | fftSize: 1024, 19 | 20 | // Number of frequencies, twice less than fftSize 21 | frequencyBinCount: 1024/2, 22 | 23 | // Smoothing, or the priority of the old data over the new data 24 | smoothingTimeConstant: 0.2, 25 | 26 | // Number of channel to analyse 27 | channel: 0, 28 | 29 | // Size of time data to buffer 30 | bufferSize: 44100, 31 | 32 | // Windowing function for fft, https://github.com/scijs/window-functions 33 | applyWindow: function (sampleNumber, totalSamples) { 34 | } 35 | 36 | //...pcm-stream params, if required 37 | }); 38 | 39 | 40 | //AnalyserNode methods 41 | 42 | // Copies the current frequency data into a Float32Array array passed into it. 43 | analyser.getFloatFrequencyData(arr); 44 | 45 | // Copies the current frequency data into a Uint8Array passed into it. 46 | analyser.getByteFrequencyData(arr); 47 | 48 | // Copies the current waveform, or time-domain data into a Float32Array array passed into it. 49 | analyser.getFloatTimeDomainData(arr); 50 | 51 | // Copies the current waveform, or time-domain data into a Uint8Array passed into it. 52 | analyser.getByteTimeDomainData(arr); 53 | 54 | 55 | //Shortcut methods 56 | 57 | //return array with frequency data in decibels of size <= fftSize 58 | analyser.getFrequencyData(size); 59 | 60 | //return array with time data of size <= self.bufferSize (way more than fftSize) 61 | analyser.getTimeData(size); 62 | 63 | 64 | //Can be used both as a sink or pass-through 65 | Generator().pipe(analyser); 66 | ``` 67 | 68 | ## Related 69 | 70 | > [audio-render](https://npmjs.org/package/audio-render) — render audio streams.
71 | > [audio-spectrum](https://npmjs.org/package/audio-spectrum) — render audio spectrum.
72 | > [audio-spectrogram](https://npmjs.org/package/audio-spectrogram) — render audio spectrogram.
73 | > [audio-waveform](https://npmjs.org/package/audio-waveform) — render audio waveform.
74 | > [audio-stat](https://npmjs.org/package/audio-stat) — render any kind of audio info: waveform, spectrogram etc.
75 | > [pcm-util](https://npmjs.org/package/pcm-util) — utils for work with pcm-streams.
76 | > [ndarray-fft](https://github.com/scijs/ndarray-fft) — implementation of fft for ndarrays.
-------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var Analyser = require('./'); 2 | var Generator = require('audio-generator'); 3 | var assert = require('assert'); 4 | var almost = require('almost-equal'); 5 | var err = 0.1; 6 | var pcm = require('pcm-util'); 7 | 8 | var analyser = Analyser({ 9 | fftSize: 64 10 | }); 11 | 12 | analyser.on('data', function (chunk) { 13 | var floatFreq = this.getFloatFrequencyData(new Float32Array(this.fftSize)); 14 | var floatTime = this.getFloatTimeDomainData(new Float32Array(this.fftSize)); 15 | var byteFreq = this.getByteFrequencyData(new Uint8Array(this.fftSize)); 16 | var byteTime = this.getByteTimeDomainData(new Uint8Array(this.fftSize)); 17 | var freq = this.getFrequencyData(); 18 | var time = this.getTimeData(); 19 | 20 | assert(almost(floatFreq[0], freq[0], err, err)); 21 | // assert(almost(byteFreq[0], freq[0], err, err)); 22 | assert.equal(floatFreq.length, freq.length); 23 | assert.equal(byteFreq.length, freq.length); 24 | 25 | assert(almost(floatTime[0], time[0], err, err)); 26 | assert(almost(pcm.convertSample(byteTime[0], {signed: false, bitDepth: 8}, {float: true}), time[0], err, err)); 27 | assert.equal(floatTime.length, time.length); 28 | assert.equal(byteTime.length, time.length); 29 | }); 30 | 31 | 32 | Generator({ 33 | generate: function () { 34 | return Math.random(); 35 | }, 36 | samplesPerFrame: 64, 37 | duration: 1 38 | }) 39 | .pipe(analyser); -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | * Smoothen windowing, now there are leaps --------------------------------------------------------------------------------