├── .gitignore ├── package.json ├── README.md ├── test.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-buffer-utils", 3 | "version": "3.1.2", 4 | "description": "Utility functions for AudioBuffers", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js", 8 | "test:browser": "budo test.js" 9 | }, 10 | "files": [ 11 | "index.js" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/jaz303/audio-buffer-utils.git" 16 | }, 17 | "keywords": [ 18 | "web", 19 | "audio", 20 | "api", 21 | "buffer", 22 | "web audio", 23 | "audio-buffer" 24 | ], 25 | "author": "Jason Frame (http://jasonframe.co.uk)", 26 | "contributors": [ 27 | "Dmitry Yv " 28 | ], 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/jaz303/audio-buffer-utils/issues" 32 | }, 33 | "homepage": "https://github.com/jaz303/audio-buffer-utils", 34 | "devDependencies": { 35 | "is-browser": "^2.0.1", 36 | "tst": "^1.3.1" 37 | }, 38 | "dependencies": { 39 | "audio-buffer": "^2.0.0", 40 | "is-audio-buffer": "^1.0.0", 41 | "is-browser": "^2.0.1", 42 | "typedarray-methods": "^1.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Utility functions for Audio Buffers. See [@audiojs/audio-buffer-utils](https://github.com/audiojs/audio-buffer-utils). 2 | 3 | 4 | ## Usage 5 | 6 | [`$ npm install audio-buffer-utils`](https://npmjs.org/package/audio-buffer-utils) 7 | 8 | 9 | ```js 10 | var utils = require('audio-buffer-utils'); 11 | 12 | //Create a new buffer from any argument. 13 | //Data can be a length, an array with channels' data, an other buffer or plain array. 14 | utils.create(channels?, data, sampleRate?); 15 | 16 | //Create a new buffer with the same characteristics as `buffer`, 17 | //contents are undefined. 18 | utils.shallow(buffer); 19 | 20 | //Create a new buffer with the same characteristics as `buffer`, 21 | //fill it with a copy of `buffer`'s data, and return it. 22 | utils.clone(buffer); 23 | 24 | //Copy the data from one buffer to another, with optional offset 25 | utils.copy(fromBuffer, result, offset?); 26 | 27 | //Reverse `buffer`. Place data to `result` buffer, if any, otherwise modify `buffer` in-place. 28 | utils.reverse(buffer, result?); 29 | 30 | //Invert `buffer`. Place data to `result` buffer, if any, otherwise modify `buffer` in-place. 31 | utils.invert(buffer, result?); 32 | 33 | //Zero all of `buffer`'s channel data. `buffer` is modified in-place. 34 | utils.zero(buffer); 35 | 36 | //Fill `buffer` with random data. `buffer` is modified in-place. 37 | utils.noise(buffer); 38 | 39 | //Test whether the content of N buffers is the same. 40 | utils.equal(bufferA, bufferB, ...); 41 | 42 | //Fill `buffer` with provided function or value. 43 | //Place data to `result` buffer, if any, otherwise modify `buffer` in-place. 44 | //Pass optional `start` and `end` indexes. 45 | utils.fill(buffer, result?, value|function (sample, idx, channel) { 46 | return sample / 2; 47 | }, start?, end?); 48 | 49 | //Create a new buffer by mapping the samples of the current one. 50 | utils.map(buffer, function (sample, idx, channel) { 51 | return sample / 2; 52 | }); 53 | 54 | //Create a new buffer by slicing the current one. 55 | utils.slice(buffer, start?, end?); 56 | 57 | //Create a new buffer by concatting passed buffers. 58 | //Channels are extended to the buffer with maximum number. 59 | //Sample rate is changed to the maximum within the buffers. 60 | utils.concat(buffer1, buffer2, buffer3, ...); 61 | 62 | //Return new buffer based on the passed one, with shortened/extended length. 63 | //Initial data is whether sliced or filled with zeros. 64 | //Useful to change duration: `util.resize(buffer, duration * buffer.sampleRate);` 65 | utils.resize(buffer, length); 66 | 67 | //Right/left-pad buffer to the length, filling with value 68 | utils.pad(buffer, length, value?); 69 | utils.pad(length, buffer, value?); 70 | 71 | //Shift signal in the time domain by `offset` samples, filling with zeros. 72 | //Modify `buffer` in-place. 73 | utils.shift(buffer, offset); 74 | 75 | //Shift signal in the time domain by `offset` samples, in circular fashion. 76 | //Modify `buffer` in-place. 77 | utils.rotate(buffer, offset); 78 | 79 | //Fold buffer into a single value. Useful to generate metrics, like loudness, average, etc. 80 | utils.reduce(buffer, function (previousValue, currendValue, idx, channel, channelData) { 81 | return previousValue + currentValue; 82 | }, startValue?); 83 | 84 | //Normalize buffer by the max value, limit to the -1..+1 range. 85 | //Place data to `result` buffer, if any, otherwise modify `buffer` in-place. 86 | utils.normalize(buffer, result?, start?, end?); 87 | 88 | //Create buffer with trimmed zeros from the start and/or end, by the threshold. 89 | utils.trim(buffer, threshold?); 90 | utils.trimStart(buffer, threshold?); 91 | utils.trimEnd(buffer, threshold?) 92 | 93 | //Mix second buffer into the first one. Pass optional weight value or mixing function. 94 | util.mix(bufferA, bufferB, ratio|fn(valA, valB, idx, channel)?, offset?); 95 | 96 | //Return buffer size, in bytes. Use pretty-bytes package to format bytes to a string, if needed. 97 | utils.size(buffer); 98 | 99 | //Get channels' data in array. Pass existing array to transfer the data to it. 100 | //Useful in audio-workers to transfer buffer to output. 101 | utils.data(buffer, data?); 102 | ``` 103 | 104 | 105 | ## Related 106 | 107 | > [audio-buffer](https://github.com/audio-lab/buffer) — audio data container, both for node/browser.
108 | > [pcm-util](https://github.com/audio-lab/pcm-util) — utils for low-level pcm buffers, like audio formats etc.
109 | > [scijs](https://github.com/scijs) — DSP utils, like fft, resample, scale etc. 110 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var util = require('./'); 2 | var assert = require('assert'); 3 | var AudioBuffer = require('audio-buffer'); 4 | var isBrowser = require('is-browser'); 5 | 6 | // var test = require('tst'); 7 | function test (a, b) { 8 | return b && b() 9 | } 10 | 11 | test('create', function () { 12 | var buf1 = util.create(); 13 | assert.equal(buf1.length, 1); 14 | assert.equal(buf1.numberOfChannels, 2); 15 | 16 | var buf2 = util.create([[0,1], [0,1], [1,0]]); 17 | assert.deepEqual(buf2.getChannelData(2), [1, 0]); 18 | 19 | var buf3 = util.create([new Float32Array([0,1]), new Float32Array([0,1]), new Float32Array([1,0])]); 20 | assert.deepEqual(buf3.getChannelData(2), [1, 0]); 21 | 22 | var buf4 = util.create(2, 5, 44100); 23 | assert.deepEqual(buf4.getChannelData(0), [0,0,0,0,0]); 24 | 25 | var buf5 = util.create(buf4); 26 | assert.notEqual(buf4, buf5); 27 | assert.notEqual(buf4.getChannelData(0), buf5.getChannelData(0)); 28 | assert.deepEqual(buf5.getChannelData(0), [0,0,0,0,0]); 29 | 30 | var buf6 = util.create([1,0,0,1]); 31 | assert.deepEqual(buf6.getChannelData(1), [0,1]); 32 | 33 | var buf7 = util.create(1, [1,0,0,1]); 34 | assert.deepEqual(buf7.getChannelData(0), [1,0,0,1]); 35 | }); 36 | 37 | test('equal', function () { 38 | var buf1 = new AudioBuffer([1, 0, -1, 0]); 39 | var buf2 = new AudioBuffer([1, 0, -1, 0]); 40 | var buf3 = new AudioBuffer([1, 0, 1, 0]); 41 | var buf4 = new AudioBuffer([1, 0, 1, 0, 1]); //the last sample is lost 42 | var buf5 = new AudioBuffer([1, 0, 1, 0]); 43 | 44 | assert(util.equal(buf1, buf2)); 45 | assert(!util.equal(buf1, buf3)); 46 | assert(!util.equal(buf1, buf4)); 47 | assert(util.equal(buf3, buf4)); 48 | assert(util.equal(buf3, buf4, buf5)); 49 | assert(!util.equal(buf4, buf5, buf3, buf1)); 50 | 51 | assert.throws(function () { 52 | util.equal(buf1, new Float32Array(1)); 53 | }); 54 | }); 55 | 56 | test('shallow', function () { 57 | var buf1 = new AudioBuffer([0, 1, 2, 3]); 58 | var buf2 = util.shallow(buf1); 59 | 60 | assert.equal(buf1.length, buf2.length); 61 | assert.equal(buf1.numberOfChannels, buf2.numberOfChannels); 62 | assert.equal(buf1.sampleRate, buf2.sampleRate); 63 | 64 | assert.throws(function () { 65 | util.shallow(new Float32Array(1)); 66 | }); 67 | }); 68 | 69 | 70 | test('clone', function () { 71 | var buf1 = new AudioBuffer([1, 0, -1, 0]); 72 | var buf2 = util.clone(buf1); 73 | 74 | assert(util.equal(buf1, buf2)); 75 | assert.notEqual(buf1, buf2); 76 | 77 | buf2.getChannelData(0)[1] = 0.5; 78 | assert.deepEqual(buf1.getChannelData(0), [1, 0]); 79 | assert.deepEqual(buf2.getChannelData(0), [1, 0.5]); 80 | 81 | assert.throws(function () { 82 | util.clone({}); 83 | }); 84 | }); 85 | 86 | 87 | test('copy', function () { 88 | var buf1 = new AudioBuffer([1, 0, -1, 0]); 89 | var buf2 = util.shallow(buf1); 90 | 91 | util.copy(buf1, buf2); 92 | 93 | assert(util.equal(buf1, buf2)); 94 | assert.notEqual(buf1, buf2); 95 | 96 | buf2.getChannelData(0)[1] = 0.5; 97 | assert.deepEqual(buf1.getChannelData(0), [1, 0]); 98 | assert.deepEqual(buf2.getChannelData(0), [1, 0.5]); 99 | 100 | var buf3 = new AudioBuffer(8); 101 | util.copy(buf2, buf3, 4); 102 | assert.deepEqual(buf3.getChannelData(0), [0,0,0,0, 1, 0.5, 0, 0]); 103 | 104 | // util.copy(buf3, buf2); 105 | }); 106 | 107 | 108 | test('clone - backing arrays are not shared between buffers', function () { 109 | var buf1 = new AudioBuffer([0, 1, 2, 3, 4]); 110 | var buf2 = util.clone(buf1); 111 | 112 | buf2.getChannelData(0)[0] = 100; 113 | assert.equal(0, buf1.getChannelData(0)[0]); 114 | }); 115 | 116 | 117 | test('reverse', function () { 118 | var buf1 = new AudioBuffer([1, 0, -1, 0]); 119 | util.reverse(buf1); 120 | 121 | assert.deepEqual(buf1.getChannelData(0), [0, 1]); 122 | assert.deepEqual(buf1.getChannelData(1), [0, -1]); 123 | 124 | assert.throws(function () { 125 | util.reverse([1,2,3]); 126 | }); 127 | 128 | var buf2 = util.shallow(buf1); 129 | util.reverse(buf1, buf2); 130 | 131 | assert.deepEqual(buf2.getChannelData(1), [-1, 0]); 132 | }); 133 | 134 | 135 | test('invert', function () { 136 | var buf1 = new AudioBuffer([1, 0.5, -1, 0]); 137 | util.invert(buf1); 138 | 139 | assert.deepEqual(buf1.getChannelData(0), [-1, -0.5]); 140 | assert.deepEqual(buf1.getChannelData(1), [1, 0]); 141 | 142 | assert.throws(function () { 143 | util.invert(new Float32Array([1,2,3])); 144 | }); 145 | 146 | var buf2 = util.shallow(buf1); 147 | util.invert(buf1, buf2); 148 | 149 | assert.deepEqual(buf2.getChannelData(1), [-1, 0]); 150 | }); 151 | 152 | 153 | test('zero', function () { 154 | var buf1 = new AudioBuffer([1, 0.5, -1, 0]); 155 | util.zero(buf1); 156 | 157 | assert.deepEqual(buf1.getChannelData(0), [0, 0]); 158 | assert.deepEqual(buf1.getChannelData(1), [0, 0]); 159 | 160 | assert.throws(function () { 161 | util.invert(buf1.getChannelData(0)); 162 | }); 163 | }); 164 | 165 | 166 | test('noise', function () { 167 | var buf1 = new AudioBuffer(4); 168 | util.noise(buf1); 169 | 170 | assert.notDeepEqual(buf1.getChannelData(0), [0, 0]); 171 | assert.notDeepEqual(buf1.getChannelData(1), [0, 0]); 172 | 173 | assert.throws(function () { 174 | util.noise(buf1.getChannelData(0)); 175 | }); 176 | }); 177 | 178 | 179 | test('fill with function', function () { 180 | var a = new AudioBuffer([1,2,3,4]); 181 | util.fill(a, function (sample, channel, offset) { return channel + offset }); 182 | 183 | assert.deepEqual(a.getChannelData(0), [0,1]); 184 | assert.deepEqual(a.getChannelData(1), [1,2]); 185 | 186 | assert.throws(function () { 187 | util.fill([1,2,3], function () {}); 188 | }); 189 | }); 190 | 191 | 192 | test('fill with value', function () { 193 | var a = new AudioBuffer([1,2,3,4]); 194 | util.fill(a, 1, 1, 3); 195 | 196 | assert.deepEqual(a.getChannelData(0), [1,1]); 197 | assert.deepEqual(a.getChannelData(1), [3,1]); 198 | 199 | assert.throws(function () { 200 | util.fill(a.getChannelData(1), 1); 201 | }); 202 | }); 203 | 204 | test('fill to another buffer', function () { 205 | var a = new AudioBuffer([1,2,3,4]); 206 | var b = util.shallow(a); 207 | util.fill(a, b, 1, 1, 3); 208 | 209 | assert.deepEqual(a.getChannelData(0), [1,2]); 210 | assert.deepEqual(a.getChannelData(1), [3,4]); 211 | 212 | assert.deepEqual(b.getChannelData(0), [0,1]); 213 | assert.deepEqual(b.getChannelData(1), [0,1]); 214 | }); 215 | 216 | test('fill callback argument', function () { 217 | var a = new AudioBuffer([1,2,3,4]); 218 | 219 | //NOTE: such arguments are possible in case of `Through(util.noise)` etc. 220 | util.fill(a, function () {}, function () { return 1; }); 221 | 222 | assert.deepEqual(a.getChannelData(0), [1,1]); 223 | assert.deepEqual(a.getChannelData(1), [1,1]); 224 | }); 225 | 226 | test('slice', function () { 227 | var a = new AudioBuffer(3, [1,2,3,4,5,6,7,8,9]); 228 | 229 | var b = util.slice(a, 1); 230 | assert.deepEqual(b.getChannelData(0), [2,3]); 231 | assert.deepEqual(b.getChannelData(1), [5,6]); 232 | 233 | var c = util.slice(a, 1, 2); 234 | assert.deepEqual(c.getChannelData(0), [2]); 235 | assert.deepEqual(c.getChannelData(1), [5]); 236 | assert.deepEqual(c.numberOfChannels, 3); 237 | 238 | b.getChannelData(0)[0] = 1; 239 | assert.deepEqual(b.getChannelData(0), [1,3]); 240 | assert.deepEqual(a.getChannelData(0), [1, 2, 3]); 241 | 242 | assert.throws(function () { 243 | util.slice([1,2,3,4], 1, 2); 244 | }); 245 | }); 246 | 247 | /* 248 | test.skip('subbuffer', function () { 249 | //NOTE: in web-audio-API two audiobuffers cannot share the same memory 250 | //as far `.buffer` property of typedarrays cannot be overridden 251 | //and typedarrays are created by WebAudioBuffer innerly 252 | 253 | var a = new AudioBuffer(3, [1,2,3,4,5,6,7,8,9]); 254 | 255 | var b = util.subbuffer(a, 1); 256 | assert.deepEqual(b.getChannelData(0), [2,3]); 257 | assert.deepEqual(b.getChannelData(1), [5,6]); 258 | 259 | var c = util.subbuffer(a, 1, 2); 260 | assert.deepEqual(c.getChannelData(0), [2]); 261 | assert.deepEqual(c.getChannelData(1), [5]); 262 | assert.deepEqual(c.numberOfChannels, 3); 263 | 264 | b.getChannelData(0)[0] = 1; 265 | assert.deepEqual(b.getChannelData(0), [1,3]); 266 | assert.deepEqual(a.getChannelData(0), [1, 1, 3]); 267 | assert.deepEqual(c.getChannelData(0), [1]); 268 | }); 269 | */ 270 | 271 | test('map', function () { 272 | var a = AudioBuffer(3, [1, 1, 1, 1, 1, 1]); 273 | 274 | var b = util.map(a, function (sample, channel, offset) { 275 | return sample + channel + offset 276 | }); 277 | 278 | assert.notEqual(a, b); 279 | assert(!util.equal(a, b)); 280 | assert.deepEqual(b.getChannelData(0), [1,2]); 281 | assert.deepEqual(b.getChannelData(1), [2,3]); 282 | assert.deepEqual(b.numberOfChannels, 3); 283 | 284 | b.getChannelData(0)[0] = 0; 285 | assert.deepEqual(a.getChannelData(0), [1,1]); 286 | assert.deepEqual(b.getChannelData(0), [0,2]); 287 | 288 | assert.throws(function () { 289 | util.map([1,2,3,4], function () {}); 290 | }); 291 | }); 292 | 293 | 294 | test('concat', function () { 295 | var a = AudioBuffer([1,1,1,1]); 296 | var b = AudioBuffer(3, 2); 297 | var c = AudioBuffer(1, [-1, -1], 22050); //handle this! 298 | 299 | var d = util.concat(a, c); 300 | assert.deepEqual(d.getChannelData(0), [1,1,-1,-1]); 301 | assert.deepEqual(d.getChannelData(1), [1,1,0,0]); 302 | 303 | var d = util.concat(c, a); 304 | assert.deepEqual(d.getChannelData(0), [-1,-1,1,1]); 305 | assert.deepEqual(d.getChannelData(1), [0,0,1,1]); 306 | 307 | var d = util.concat(a, b, c); 308 | 309 | assert.deepEqual(d.getChannelData(0), [1,1,0,0,-1,-1]); 310 | assert.deepEqual(d.getChannelData(1), [1,1,0,0,0,0]); 311 | assert.deepEqual(d.getChannelData(2), [0,0,0,0,0,0]); 312 | 313 | assert.throws(function () { 314 | util.concat([1,2,3,4], [5,6]); 315 | }); 316 | }); 317 | 318 | 319 | test('resize', function () { 320 | var a = AudioBuffer(1, [1,1,1,1,1], 44100); 321 | 322 | //set too big 323 | a = util.resize(a, 10); 324 | assert.deepEqual(a.getChannelData(0), [1,1,1,1,1,0,0,0,0,0]); 325 | 326 | //set too small 327 | a = util.resize(a, 2); 328 | assert.deepEqual(a.getChannelData(0), [1,1]); 329 | 330 | assert.throws(function () { 331 | util.resize('123', 2); 332 | }); 333 | }); 334 | 335 | 336 | test('rotate (+ve)', function () { 337 | var a = AudioBuffer(1, [0,0,1,1,0,0,-1,-1]); 338 | util.rotate(a, 2); 339 | assert.deepEqual(a.getChannelData(0), [-1,-1,0,0,1,1,0,0]); 340 | 341 | assert.throws(function () { 342 | util.rotate([1,2,3], 2); 343 | }); 344 | }); 345 | 346 | test('rotate (-ve)', function() { 347 | var a = AudioBuffer(1, [0,0,1,1,0,0,-1,-1]); 348 | util.rotate(a, -3); 349 | assert.deepEqual(a.getChannelData(0), [1,0,0,-1,-1,0,0,1]); 350 | 351 | assert.throws(function () { 352 | util.rotate([1,2,3], -2); 353 | }); 354 | }); 355 | 356 | 357 | test('shift (+ve)', function () { 358 | var a = AudioBuffer(1, [0,0,1,1,0,0,-1,-1]); 359 | util.shift(a, 2); 360 | assert.deepEqual(a.getChannelData(0), [0,0,0,0,1,1,0,0]); 361 | 362 | assert.throws(function () { 363 | util.shift([1,2,3], 2); 364 | }); 365 | }); 366 | 367 | test('shift (-ve)', function () { 368 | var a = AudioBuffer(1, [0,0,1,1,0,0,-1,-1]); 369 | util.shift(a, -3); 370 | assert.deepEqual(a.getChannelData(0), [1,0,0,-1,-1,0,0,0]); 371 | 372 | assert.throws(function () { 373 | util.shift([1,2,3], -2); 374 | }); 375 | }); 376 | 377 | 378 | test('normalize', function () { 379 | var a = AudioBuffer(1, [0, 0.1, 0, -0.2]); 380 | 381 | util.normalize(a); 382 | 383 | assert.deepEqual(a.getChannelData(0), [0, 0.5, 0, -1]); 384 | 385 | //too big value 386 | //FIXME: too large values are interpreted as 1, but maybe we need deamplifying instead 387 | //for example, biquad-filters may return values > 1, then we do not want to clip values 388 | var a = AudioBuffer(2, [0, 0.1, 0, -0.5, 999, 2]); 389 | 390 | util.normalize(a); 391 | 392 | assert.deepEqual(a.getChannelData(1), [-0.5, 1, 1]); 393 | 394 | assert.throws(function () { 395 | util.normalize(new Float32Array([0, 0.1, 0.2])); 396 | }); 397 | }); 398 | 399 | 400 | test('trim', function () { 401 | //trim both 402 | var a = AudioBuffer([0,0,1,0,0,2,3,0]); 403 | var b = util.trim(a); 404 | 405 | assert.deepEqual(b.getChannelData(0), [0,1]); 406 | assert.deepEqual(b.getChannelData(1), [2,3]); 407 | 408 | //no trim 409 | var a = AudioBuffer([1,0,1,0,0,2,3,1]); 410 | var b = util.trim(a); 411 | 412 | assert.deepEqual(b.getChannelData(0), [1,0,1,0]); 413 | assert.deepEqual(b.getChannelData(1), [0,2,3,1]); 414 | 415 | assert.throws(function () { 416 | util.trim(new Float32Array([0, 0.1, 0.2])); 417 | }); 418 | }); 419 | 420 | 421 | test('pad', function () { 422 | //pad right 423 | var a = AudioBuffer([0,1,2,3,4,5]); 424 | var b = util.pad(a, 4); 425 | 426 | assert.deepEqual(b.getChannelData(0), [0,1,2,0]); 427 | assert.deepEqual(b.getChannelData(1), [3,4,5,0]); 428 | 429 | //pad left 430 | var a = AudioBuffer([0,1,2,3,4,5]); 431 | var b = util.pad(4, a); 432 | 433 | assert.deepEqual(b.getChannelData(0), [0,0,1,2]); 434 | assert.deepEqual(b.getChannelData(1), [0,3,4,5]); 435 | 436 | //pad value 437 | var a = AudioBuffer([0,1,2,3,4,5]); 438 | var b = util.pad(4, a, 0.5); 439 | 440 | assert.deepEqual(b.getChannelData(0), [0.5,0,1,2]); 441 | assert.deepEqual(b.getChannelData(1), [0.5,3,4,5]); 442 | 443 | assert.throws(function () { 444 | util.pad(new Float32Array([0, 0.1, 0.2])); 445 | }); 446 | }); 447 | 448 | 449 | test('size', function () { 450 | if (!isBrowser) { 451 | if (typeof Float64Array !== 'undefined') { 452 | AudioBuffer.FloatArray = Float64Array; 453 | var b = AudioBuffer(3, 200, 5000); 454 | assert.equal(util.size(b), 3 * 200 * 8 ); 455 | } 456 | AudioBuffer.FloatArray = Float32Array; 457 | } 458 | else { 459 | var a = AudioBuffer(200); 460 | assert.equal(util.size(a), 200 * 2 * 4); 461 | } 462 | 463 | assert.throws(function () { 464 | util.size(); 465 | }); 466 | }); 467 | 468 | /* 469 | test.skip('resample', function () { 470 | //NOTE: for resampling use https://github.com/scijs/ndarray-resample 471 | //or similar. 472 | 473 | var a = AudioBuffer(1, [0, 0.5, 1, 0.5, 0, -0.5, -1, -0.5, 0], 44100); 474 | var b = util.resample(a, 3000); 475 | 476 | assert.deepEqual(b.getChannelData(0), []); 477 | }); 478 | */ 479 | 480 | test('mix', function () { 481 | var a = AudioBuffer(2, [0,1,0,1]); 482 | var b = AudioBuffer(2, [0.5, 0.5, -0.5, -0.5]); 483 | 484 | //simple mix 485 | util.mix(a, b); 486 | assert.deepEqual(a.getChannelData(0), [0.25, 0.75]); 487 | assert.deepEqual(a.getChannelData(1), [-0.25, 0.25]); 488 | 489 | //fn mix 490 | var a = AudioBuffer(2, [0, 1, 0, 1, 0, 1]); 491 | var b = AudioBuffer(2, [1, 1, 1, 1]); 492 | util.mix(a, b, function (v1, v2) { 493 | return v1 + v2; 494 | }, 1); 495 | 496 | assert.deepEqual(a.getChannelData(0), [0, 2, 1]); 497 | assert.deepEqual(a.getChannelData(1), [1, 1, 2]); 498 | 499 | assert.throws(function () { 500 | util.mix([1,2,3], [4,5,6], 0.1); 501 | }); 502 | }); 503 | 504 | 505 | test('data', function () { 506 | var b = util.create(3, [1,-1, 0.5, -1, 0, -0.5]); 507 | 508 | var data = util.data(b); 509 | 510 | assert.deepEqual(data[0], [1, -1]); 511 | assert.deepEqual(data[1], [0.5, -1]); 512 | assert.deepEqual(data[2], [0, -0.5]); 513 | 514 | var src = [new Float32Array(2), new Float32Array(2), new Float32Array(2)]; 515 | var data = util.data(b, src); 516 | 517 | assert.deepEqual(src[0], [1, -1]); 518 | assert.deepEqual(src[1], [0.5, -1]); 519 | assert.deepEqual(src[2], [0, -0.5]); 520 | }); 521 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module audio-buffer-utils 3 | */ 4 | 5 | require('typedarray-methods'); 6 | var AudioBuffer = require('audio-buffer'); 7 | var isAudioBuffer = require('is-audio-buffer'); 8 | var isBrowser = require('is-browser'); 9 | 10 | 11 | module.exports = { 12 | create: create, 13 | copy: copy, 14 | shallow: shallow, 15 | clone: clone, 16 | reverse: reverse, 17 | invert: invert, 18 | zero: zero, 19 | noise: noise, 20 | equal: equal, 21 | fill: fill, 22 | slice: slice, 23 | map: map, 24 | concat: concat, 25 | resize: resize, 26 | pad: pad, 27 | rotate: rotate, 28 | shift: shift, 29 | reduce: reduce, 30 | normalize: normalize, 31 | trim: trim, 32 | trimStart: trimStart, 33 | trimEnd: trimEnd, 34 | mix: mix, 35 | size: size, 36 | data: data 37 | }; 38 | 39 | 40 | /** 41 | * Create buffer from any argument 42 | */ 43 | function create (a, b, c) { 44 | return new AudioBuffer(a, b, c); 45 | } 46 | 47 | 48 | /** 49 | * Copy data from buffer A to buffer B 50 | */ 51 | function copy (from, to, offset) { 52 | validate(from); 53 | validate(to); 54 | 55 | offset = offset || 0; 56 | 57 | for (var channel = 0, l = Math.min(from.numberOfChannels, to.numberOfChannels); channel < l; channel++) { 58 | to.getChannelData(channel).set(from.getChannelData(channel), offset); 59 | } 60 | 61 | return to; 62 | } 63 | 64 | 65 | /** 66 | * Assert argument is AudioBuffer, throw error otherwise. 67 | */ 68 | function validate (buffer) { 69 | if (!isAudioBuffer(buffer)) throw new Error('Argument should be an AudioBuffer instance.'); 70 | } 71 | 72 | 73 | 74 | /** 75 | * Create a buffer with the same characteristics as inBuffer, without copying 76 | * the data. Contents of resulting buffer are undefined. 77 | */ 78 | function shallow (buffer) { 79 | validate(buffer); 80 | 81 | //workaround for faster browser creation 82 | //avoid extra checks & copying inside of AudioBuffer class 83 | if (isBrowser) { 84 | return AudioBuffer.context.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); 85 | } 86 | 87 | return create(buffer.numberOfChannels, buffer.length, buffer.sampleRate); 88 | } 89 | 90 | 91 | /** 92 | * Create clone of a buffer 93 | */ 94 | function clone (buffer) { 95 | return copy(buffer, shallow(buffer)); 96 | } 97 | 98 | 99 | /** 100 | * Reverse samples in each channel 101 | */ 102 | function reverse (buffer, target) { 103 | validate(buffer); 104 | 105 | if (target) { 106 | validate(target); 107 | copy(buffer, target); 108 | } 109 | else { 110 | target = buffer; 111 | } 112 | 113 | for (var i = 0, c = target.numberOfChannels; i < c; ++i) { 114 | target.getChannelData(i).reverse(); 115 | } 116 | 117 | return target; 118 | } 119 | 120 | 121 | /** 122 | * Invert amplitude of samples in each channel 123 | */ 124 | function invert (buffer, target, start, end) { 125 | return fill(buffer, target, function (sample) { return -sample; }, start, end); 126 | } 127 | 128 | 129 | /** 130 | * Fill with zeros 131 | */ 132 | function zero (buffer, target, start, end) { 133 | return fill(buffer, target, 0, start, end); 134 | } 135 | 136 | 137 | /** 138 | * Fill with white noise 139 | */ 140 | function noise (buffer, target, start, end) { 141 | return fill(buffer, target, function (sample) { return Math.random() * 2 - 1; }, start, end); 142 | } 143 | 144 | 145 | /** 146 | * Test whether two buffers are the same 147 | */ 148 | function equal (bufferA, bufferB) { 149 | //walk by all the arguments 150 | if (arguments.length > 2) { 151 | for (var i = 0, l = arguments.length - 1; i < l; i++) { 152 | if (!equal(arguments[i], arguments[i + 1])) return false; 153 | } 154 | return true; 155 | } 156 | 157 | validate(bufferA); 158 | validate(bufferB); 159 | 160 | if (bufferA.length !== bufferB.length || bufferA.numberOfChannels !== bufferB.numberOfChannels) return false; 161 | 162 | for (var channel = 0; channel < bufferA.numberOfChannels; channel++) { 163 | var dataA = bufferA.getChannelData(channel); 164 | var dataB = bufferB.getChannelData(channel); 165 | 166 | for (var i = 0; i < dataA.length; i++) { 167 | if (dataA[i] !== dataB[i]) return false; 168 | } 169 | } 170 | 171 | return true; 172 | } 173 | 174 | 175 | /** 176 | * A helper to return slicing offset 177 | */ 178 | function getStart (pos, len) { 179 | if (pos == null) return 0; 180 | return pos < 0 ? (len + (pos % len)) : Math.min(len, pos); 181 | } 182 | function getEnd (pos, len) { 183 | if (pos == null) return len; 184 | return pos < 0 ? (len + (pos % len)) : Math.min(len, pos); 185 | } 186 | 187 | 188 | /** 189 | * Generic in-place fill/transform 190 | */ 191 | function fill (buffer, target, value, start, end) { 192 | validate(buffer); 193 | 194 | //if target buffer is passed 195 | if (!isAudioBuffer(target) && target != null) { 196 | //target is bad argument 197 | if (typeof value == 'function') { 198 | target = null; 199 | } 200 | else { 201 | end = start; 202 | start = value; 203 | value = target; 204 | target = null; 205 | } 206 | } 207 | 208 | if (target) { 209 | validate(target); 210 | } 211 | else { 212 | target = buffer; 213 | } 214 | 215 | //resolve optional start/end args 216 | start = getStart(start, buffer.length); 217 | end = getEnd(end, buffer.length); 218 | 219 | //resolve type of value 220 | if (!(value instanceof Function)) { 221 | var fn = function () {return value;}; 222 | } 223 | else { 224 | var fn = value; 225 | } 226 | 227 | for (var channel = 0, c = buffer.numberOfChannels; channel < c; channel++) { 228 | var data = buffer.getChannelData(channel), 229 | targetData = target.getChannelData(channel), 230 | l = buffer.length; 231 | for (var i = start; i < end; i++) { 232 | targetData[i] = fn.call(buffer, data[i], i, channel, data); 233 | } 234 | } 235 | 236 | return target; 237 | } 238 | 239 | 240 | /** 241 | * Return sliced buffer 242 | */ 243 | function slice (buffer, start, end) { 244 | validate(buffer); 245 | 246 | start = getStart(start, buffer.length); 247 | end = getEnd(end, buffer.length); 248 | 249 | var data = []; 250 | for (var channel = 0; channel < buffer.numberOfChannels; channel++) { 251 | data.push(buffer.getChannelData(channel).slice(start, end)); 252 | } 253 | return create(buffer.numberOfChannels, data, buffer.sampleRate); 254 | } 255 | 256 | 257 | /** 258 | * Return new buffer, mapped by a function. 259 | * Similar to transform, but keeps initial buffer untouched 260 | */ 261 | function map (buffer, fn) { 262 | validate(buffer); 263 | 264 | var data = []; 265 | 266 | for (var channel = 0; channel < buffer.numberOfChannels; channel++) { 267 | data.push(buffer.getChannelData(channel).map(function (value, idx) { 268 | return fn.call(buffer, value, idx, channel, data); 269 | })); 270 | } 271 | 272 | return create(buffer.numberOfChannels, data, buffer.sampleRate); 273 | } 274 | 275 | 276 | /** 277 | * Concat buffer with other buffer(s) 278 | */ 279 | function concat (bufferA, bufferB) { 280 | //walk by all the arguments 281 | if (arguments.length > 2) { 282 | var result = bufferA; 283 | for (var i = 1, l = arguments.length; i < l; i++) { 284 | result = concat(result, arguments[i]); 285 | } 286 | return result; 287 | } 288 | 289 | validate(bufferA); 290 | validate(bufferB); 291 | 292 | var data = []; 293 | var channels = Math.max(bufferA.numberOfChannels, bufferB.numberOfChannels); 294 | var length = bufferA.length + bufferB.length; 295 | 296 | //FIXME: there might be required more thoughtful resampling, but now I'm lazy sry :( 297 | var sampleRate = Math.max(bufferA.sampleRate, bufferB.sampleRate); 298 | 299 | for (var channel = 0; channel < channels; channel++) { 300 | var channelData = new Float32Array(length); 301 | 302 | if (channel < bufferA.numberOfChannels) { 303 | channelData.set(bufferA.getChannelData(channel)); 304 | } 305 | 306 | if (channel < bufferB.numberOfChannels) { 307 | channelData.set(bufferB.getChannelData(channel), bufferA.length); 308 | } 309 | 310 | data.push(channelData); 311 | } 312 | 313 | return create(channels, data, sampleRate); 314 | } 315 | 316 | 317 | /** 318 | * Change the length of the buffer, by trimming or filling with zeros 319 | */ 320 | function resize (buffer, length) { 321 | validate(buffer); 322 | 323 | if (length < buffer.length) return slice(buffer, 0, length); 324 | 325 | return concat(buffer, create(length - buffer.length)); 326 | } 327 | 328 | 329 | /** 330 | * Pad buffer to required size 331 | */ 332 | function pad (a, b, value) { 333 | var buffer, length; 334 | 335 | if (typeof a === 'number') { 336 | buffer = b; 337 | length = a; 338 | } else { 339 | buffer = a; 340 | length = b; 341 | } 342 | 343 | value = value || 0; 344 | 345 | validate(buffer); 346 | 347 | //no need to pad 348 | if (length < buffer.length) return buffer; 349 | 350 | //left-pad 351 | if (buffer === b) { 352 | return concat(fill(create(length - buffer.length), value), buffer); 353 | } 354 | 355 | //right-pad 356 | return concat(buffer, fill(create(length - buffer.length), value)); 357 | } 358 | 359 | 360 | 361 | /** 362 | * Shift content of the buffer in circular fashion 363 | */ 364 | function rotate (buffer, offset) { 365 | validate(buffer); 366 | 367 | for (var channel = 0; channel < buffer.numberOfChannels; channel++) { 368 | var cData = buffer.getChannelData(channel); 369 | var srcData = cData.slice(); 370 | for (var i = 0, l = cData.length, idx; i < l; i++) { 371 | idx = (offset + (offset + i < 0 ? l + i : i )) % l; 372 | cData[idx] = srcData[i]; 373 | } 374 | } 375 | 376 | return buffer; 377 | } 378 | 379 | 380 | /** 381 | * Shift content of the buffer 382 | */ 383 | function shift (buffer, offset) { 384 | validate(buffer); 385 | 386 | for (var channel = 0; channel < buffer.numberOfChannels; channel++) { 387 | var cData = buffer.getChannelData(channel); 388 | if (offset > 0) { 389 | for (var i = cData.length - offset; i--;) { 390 | cData[i + offset] = cData[i]; 391 | } 392 | } 393 | else { 394 | for (var i = -offset, l = cData.length - offset; i < l; i++) { 395 | cData[i + offset] = cData[i] || 0; 396 | } 397 | } 398 | } 399 | 400 | return buffer; 401 | } 402 | 403 | 404 | 405 | /** 406 | * Reduce buffer to a single metric, e. g. average, max, min, volume etc 407 | */ 408 | function reduce (buffer, fn, value, start, end) { 409 | validate(buffer); 410 | 411 | start = getStart(start, buffer.length); 412 | end = getEnd(end, buffer.length); 413 | 414 | if (value == null) value = 0; 415 | 416 | for (var channel = 0, c = buffer.numberOfChannels; channel < c; channel++) { 417 | var data = buffer.getChannelData(channel), 418 | l = buffer.length; 419 | for (var i = start; i < end; i++) { 420 | value = fn.call(buffer, value, data[i], i, channel, data); 421 | } 422 | } 423 | 424 | return value; 425 | } 426 | 427 | 428 | /** 429 | * Normalize buffer by the maximum value, 430 | * limit values by the -1..1 range 431 | */ 432 | function normalize (buffer, target, start, end) { 433 | validate(buffer); 434 | 435 | //resolve optional target arg 436 | if (!isAudioBuffer(target)) { 437 | end = start; 438 | start = target; 439 | target = null; 440 | } 441 | 442 | if (target) { 443 | validate(target); 444 | } 445 | else { 446 | target = buffer; 447 | } 448 | 449 | var max = reduce(buffer, function (prev, curr) { 450 | return Math.max(Math.abs(prev), Math.abs(curr)); 451 | }, 0, start, end); 452 | 453 | var amp = 1 / Math.min(max, 1); 454 | 455 | return fill(buffer, target, function (value) { 456 | return Math.min(value * amp, 1); 457 | }, start, end); 458 | } 459 | 460 | 461 | /** 462 | * Trim sound (remove zeros from the beginning and the end) 463 | */ 464 | function trim (buffer, level) { 465 | return trimInternal(buffer, level, true, true); 466 | } 467 | 468 | function trimStart (buffer, level) { 469 | return trimInternal(buffer, level, true, false); 470 | } 471 | 472 | function trimEnd (buffer, level) { 473 | return trimInternal(buffer, level, false, true); 474 | } 475 | 476 | function trimInternal(buffer, level, trimLeft, trimRight) { 477 | validate(buffer); 478 | 479 | level = (level == null) ? 0 : Math.abs(level); 480 | 481 | var start, end; 482 | 483 | if (trimLeft) { 484 | start = buffer.length; 485 | //FIXME: replace with indexOF 486 | for (var channel = 0, c = buffer.numberOfChannels; channel < c; channel++) { 487 | var data = buffer.getChannelData(channel); 488 | for (var i = 0; i < data.length; i++) { 489 | if (i > start) break; 490 | if (Math.abs(data[i]) > level) { 491 | start = i; 492 | break; 493 | } 494 | } 495 | } 496 | } else { 497 | start = 0; 498 | } 499 | 500 | if (trimRight) { 501 | end = 0; 502 | //FIXME: replace with lastIndexOf 503 | for (var channel = 0, c = buffer.numberOfChannels; channel < c; channel++) { 504 | var data = buffer.getChannelData(channel); 505 | for (var i = data.length - 1; i >= 0; i--) { 506 | if (i < end) break; 507 | if (Math.abs(data[i]) > level) { 508 | end = i + 1; 509 | break; 510 | } 511 | } 512 | } 513 | } else { 514 | end = buffer.length; 515 | } 516 | 517 | return slice(buffer, start, end); 518 | } 519 | 520 | 521 | /** 522 | * Mix current buffer with the other one. 523 | * The reason to modify bufferA instead of returning the new buffer 524 | * is reduced amount of calculations and flexibility. 525 | * If required, the cloning can be done before mixing, which will be the same. 526 | */ 527 | function mix (bufferA, bufferB, weight, offset) { 528 | validate(bufferA); 529 | validate(bufferB); 530 | 531 | if (weight == null) weight = 0.5; 532 | var fn = weight instanceof Function ? weight : function (a, b) { 533 | return a * (1 - weight) + b * weight; 534 | }; 535 | 536 | if (offset == null) offset = 0; 537 | else if (offset < 0) offset += bufferA.length; 538 | 539 | for (var channel = 0; channel < bufferA.numberOfChannels; channel++) { 540 | var aData = bufferA.getChannelData(channel); 541 | var bData = bufferB.getChannelData(channel); 542 | 543 | for (var i = offset, j = 0; i < bufferA.length && j < bufferB.length; i++, j++) { 544 | aData[i] = fn.call(bufferA, aData[i], bData[j], j, channel); 545 | } 546 | } 547 | 548 | return bufferA; 549 | } 550 | 551 | 552 | /** 553 | * Size of a buffer, in bytes 554 | */ 555 | function size (buffer) { 556 | validate(buffer); 557 | 558 | return buffer.numberOfChannels * buffer.getChannelData(0).byteLength; 559 | } 560 | 561 | 562 | /** 563 | * Return array with buffer’s per-channel data 564 | */ 565 | function data (buffer, data) { 566 | validate(buffer); 567 | 568 | //ensure output data array, if not defined 569 | data = data || []; 570 | 571 | //transfer data per-channel 572 | for (var channel = 0; channel < buffer.numberOfChannels; channel++) { 573 | if (ArrayBuffer.isView(data[channel])) { 574 | data[channel].set(buffer.getChannelData(channel)); 575 | } 576 | else { 577 | data[channel] = buffer.getChannelData(channel); 578 | } 579 | } 580 | 581 | return data; 582 | } --------------------------------------------------------------------------------