├── .gitignore ├── .jshintrc ├── README.md ├── bower.json ├── bundle.js ├── index.html ├── index.js ├── lib └── resampler.js ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : true, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 12 | "indent" : 4, // {int} Number of spaces to use for indentation 13 | "newcap" : true, // true: Require capitalization of all constructor functions 14 | "noarg" : true, // true: Prohibit use of `arguments.caller`, `arguments.callee` 15 | "noempty" : true, // true: Prohibit use of empty blocks 16 | "nonew" : true, // true: Prohibit use of constructors without assignment 17 | "undef" : true, // true: Require all non-global variables to be declared 18 | "unused" : true, // true: Require all defined variables be used 19 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 20 | "trailing" : true, // true: Prohibit trailing whitespaces 21 | 22 | // Relaxing 23 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 24 | "boss" : false, // true: Tolerate assignments where comparisons are expected 25 | "eqnull" : false, // true: Tolerate use of `== null` 26 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 27 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 28 | "funcscope" : true, // true: Tolerate defining variables inside control statements" 29 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 30 | "iterator" : false, // true: Tolerate using the `__iterator__` property 31 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement 32 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 33 | "laxcomma" : false, // true: Tolerate comma-first style coding 34 | "loopfunc" : true, // true: Tolerate functions being defined in loops 35 | "multistr" : false, // true: Tolerate multi-line strings 36 | "proto" : false, // true: Tolerate using the `__proto__` property 37 | "scripturl" : false, // true: Tolerate script-targeted URLs 38 | "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment 39 | "shadow" : false, // true: Allows re-define variables later in code. 40 | "sub" : false, // true: Tolerate using `[]` notation 41 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 42 | "validthis" : false, // true: Tolerate using this in a non-constructor function 43 | 44 | // Environments 45 | "browser" : true, // Web Browser (window, document, etc) 46 | "devel" : true, // Development/debugging (alert, confirm, etc) 47 | "jquery" : true, 48 | "node" : true, 49 | 50 | // Custom Globals 51 | "globals" : { 52 | "define" : true, 53 | "Dropzone":true, 54 | "OfflineAudioContext": true, 55 | "AudioContext" : true, 56 | "AudioParam" : true, 57 | "AudioNode" : true, 58 | "AudioBuffer" : true, 59 | "expect" : true, 60 | "jasmine": true, 61 | "describe": true, 62 | "it" : true 63 | } // additional predefined global variables 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio-Resampler 2 | 3 | [![npm version](https://badge.fury.io/js/audio-resampler.svg)](http://badge.fury.io/js/audio-resampler) 4 | 5 | Simple WebAudio based resampling library. 6 | 7 | 8 | #### Runs on all [modern browsers which support WebAudio](http://caniuse.com/#search=audio-api). 9 | 10 | ## Installation 11 | 12 | `npm install audio-resampler` 13 | 14 | ## API 15 | 16 | ``` 17 | resampler = require('audio-resampler'); 18 | resampler(input, targetSampleRate, oncomplete); 19 | ``` 20 | 21 | `input` : Input audio file. This can either be a URL, a File object, or an AudioBuffer. 22 | 23 | `targetSampleRate` : The target sample rate after the re-sampling process. This number is limted based on the browser implementation (usually >=3000, <=192000) 24 | 25 | `oncomplete`: Callback when the resampling process is completed. The argument to this callback is an Object which supports the following methods: 26 | 27 | `getAudioBuffer` : Returns the resampler AudioBuffer 28 | 29 | `getFile` : Returns a ObjectURL of a WAV file created from the resampled Audio. 30 | 31 | ## Example Usage 32 | 33 | ``` 34 | resampler = require('audio-resampler'); 35 | var URL = "https://dl.dropboxusercontent.com/u/957/audio/sine.wav" 36 | resampler(URL, 192000, function(event){ 37 | event.getFile(function(fileEvent){ 38 | var a = document.createElement("a"); 39 | document.body.appendChild(a); 40 | a.download = "resampled.wav"; 41 | a.style = "display: none"; 42 | a.href = fileEvent; 43 | a.click(); 44 | window.URL.revokeObjectURL(fileEvent); 45 | document.body.removeChild(a); 46 | }); 47 | }); 48 | ``` 49 | 50 | ## Test 51 | 52 | To test this repository, you can run a local server using the `npm start` command which serves a simple drag-and-drop interface based webpage to resampler audio files. 53 | 54 | 55 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resampler", 3 | "main": "lib/resampler.js", 4 | "version": "1.0.1", 5 | "homepage": "https://github.com/notthetup/resampler", 6 | "authors": [ 7 | "Chinmay Pendharkar " 8 | ], 9 | "description": "Simple WebAudio based resampling library", 10 | "moduleType": [], 11 | "keywords": [ 12 | "resample", 13 | "audio", 14 | "webaudio" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 192000 || targetSampleRate < 3000) { 114 | return returnError('Error: Second argument should be a numeric sample rate between 3000 and 192000'); 115 | } 116 | 117 | if (inputType === '[object String]' || inputType === '[object File]') { 118 | console.log('Loading/decoding input', input); 119 | wal.load(input, { 120 | onload: function(err, audioBuffer) { 121 | if (err) { 122 | return returnError(err); 123 | } 124 | resampleAudioBuffer(audioBuffer); 125 | } 126 | }); 127 | } else if (inputType === '[object AudioBuffer]') { 128 | resampleAudioBuffer(input); 129 | } else if (inputType === '[object Object]' && input.leftBuffer && input.sampleRate) { 130 | var numCh_ = input.rightBuffer ? 2 : 1; 131 | var audioBuffer_ = audioContext.createBuffer(numCh_, input.leftBuffer.length, input.sampleRate); 132 | resampleAudioBuffer(audioBuffer_); 133 | } else { 134 | return returnError('Error: Unknown input type'); 135 | } 136 | 137 | function returnError(errMsg) { 138 | console.error(errMsg); 139 | if (typeof oncomplete === 'function') { 140 | oncomplete(new Error(errMsg)); 141 | } 142 | return; 143 | } 144 | 145 | function resampleAudioBuffer(audioBuffer) { 146 | 147 | 148 | var numCh_ = audioBuffer.numberOfChannels; 149 | var numFrames_ = audioBuffer.length * targetSampleRate / audioBuffer.sampleRate; 150 | 151 | var offlineContext_ = new OfflineAudioContext(numCh_, numFrames_, targetSampleRate); 152 | var bufferSource_ = offlineContext_.createBufferSource(); 153 | bufferSource_.buffer = audioBuffer; 154 | 155 | offlineContext_.oncomplete = function(event) { 156 | var resampeledBuffer = event.renderedBuffer; 157 | console.log('Done Rendering'); 158 | if (typeof oncomplete === 'function') { 159 | oncomplete({ 160 | getAudioBuffer: function() { 161 | return resampeledBuffer; 162 | }, 163 | getFile: function(fileCallback) { 164 | var audioData = { 165 | sampleRate: resampeledBuffer.sampleRate, 166 | channelData: [] 167 | }; 168 | for (var i = 0; i < resampeledBuffer.numberOfChannels; i++) { 169 | audioData.channelData[i] = resampeledBuffer.getChannelData(i); 170 | } 171 | WavEncoder.encode(audioData).then(function(buffer) { 172 | var blob = new Blob([buffer], { 173 | type: "audio/wav" 174 | }); 175 | fileCallback(URL.createObjectURL(blob)); 176 | }); 177 | } 178 | }); 179 | } 180 | }; 181 | 182 | console.log('Starting Offline Rendering'); 183 | bufferSource_.connect(offlineContext_.destination); 184 | bufferSource_.start(0); 185 | offlineContext_.startRendering(); 186 | } 187 | } 188 | 189 | 190 | module.exports = resampler; 191 | 192 | },{"wav-encoder":9,"webaudioloader":12}],3:[function(require,module,exports){ 193 | module.exports = DragDrop 194 | 195 | var throttle = require('lodash.throttle') 196 | 197 | function DragDrop (elem, cb) { 198 | if (typeof elem === 'string') elem = document.querySelector(elem) 199 | elem.addEventListener('dragenter', killEvent, false) 200 | elem.addEventListener('dragover', makeOnDragOver(elem), false) 201 | elem.addEventListener('drop', onDrop.bind(undefined, elem, cb), false) 202 | } 203 | 204 | function killEvent (e) { 205 | e.stopPropagation() 206 | e.preventDefault() 207 | return false 208 | } 209 | 210 | function makeOnDragOver (elem) { 211 | var fn = throttle(function () { 212 | elem.classList.add('drag') 213 | 214 | if (elem.timeout) clearTimeout(elem.timeout) 215 | elem.timeout = setTimeout(function () { 216 | elem.classList.remove('drag') 217 | }, 150) 218 | }, 100, {trailing: false}) 219 | 220 | return function (e) { 221 | e.stopPropagation() 222 | e.preventDefault() 223 | e.dataTransfer.dropEffect = 'copy' 224 | fn() 225 | } 226 | } 227 | 228 | function onDrop (elem, cb, e) { 229 | e.stopPropagation() 230 | e.preventDefault() 231 | elem.classList.remove('drag') 232 | cb(Array.prototype.slice.call(e.dataTransfer.files), { x: e.clientX, y: e.clientY }) 233 | return false 234 | } 235 | 236 | },{"lodash.throttle":4}],4:[function(require,module,exports){ 237 | /** 238 | * lodash 3.0.1 (Custom Build) 239 | * Build: `lodash modern modularize exports="npm" -o ./` 240 | * Copyright 2012-2015 The Dojo Foundation 241 | * Based on Underscore.js 1.7.0 242 | * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 243 | * Available under MIT license 244 | */ 245 | var debounce = require('lodash.debounce'); 246 | 247 | /** Used as the `TypeError` message for "Functions" methods. */ 248 | var FUNC_ERROR_TEXT = 'Expected a function'; 249 | 250 | /** Used as an internal `_.debounce` options object by `_.throttle`. */ 251 | var debounceOptions = { 252 | 'leading': false, 253 | 'maxWait': 0, 254 | 'trailing': false 255 | }; 256 | 257 | /** 258 | * Creates a function that only invokes `func` at most once per every `wait` 259 | * milliseconds. The created function comes with a `cancel` method to cancel 260 | * delayed invocations. Provide an options object to indicate that `func` 261 | * should be invoked on the leading and/or trailing edge of the `wait` timeout. 262 | * Subsequent calls to the throttled function return the result of the last 263 | * `func` call. 264 | * 265 | * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked 266 | * on the trailing edge of the timeout only if the the throttled function is 267 | * invoked more than once during the `wait` timeout. 268 | * 269 | * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) 270 | * for details over the differences between `_.throttle` and `_.debounce`. 271 | * 272 | * @static 273 | * @memberOf _ 274 | * @category Function 275 | * @param {Function} func The function to throttle. 276 | * @param {number} wait The number of milliseconds to throttle invocations to. 277 | * @param {Object} [options] The options object. 278 | * @param {boolean} [options.leading=true] Specify invoking on the leading 279 | * edge of the timeout. 280 | * @param {boolean} [options.trailing=true] Specify invoking on the trailing 281 | * edge of the timeout. 282 | * @returns {Function} Returns the new throttled function. 283 | * @example 284 | * 285 | * // avoid excessively updating the position while scrolling 286 | * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); 287 | * 288 | * // invoke `renewToken` when the click event is fired, but not more than once every 5 minutes 289 | * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }) 290 | * jQuery('.interactive').on('click', throttled); 291 | * 292 | * // cancel a trailing throttled call 293 | * jQuery(window).on('popstate', throttled.cancel); 294 | */ 295 | function throttle(func, wait, options) { 296 | var leading = true, 297 | trailing = true; 298 | 299 | if (typeof func != 'function') { 300 | throw new TypeError(FUNC_ERROR_TEXT); 301 | } 302 | if (options === false) { 303 | leading = false; 304 | } else if (isObject(options)) { 305 | leading = 'leading' in options ? !!options.leading : leading; 306 | trailing = 'trailing' in options ? !!options.trailing : trailing; 307 | } 308 | debounceOptions.leading = leading; 309 | debounceOptions.maxWait = +wait; 310 | debounceOptions.trailing = trailing; 311 | return debounce(func, wait, debounceOptions); 312 | } 313 | 314 | /** 315 | * Checks if `value` is the language type of `Object`. 316 | * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 317 | * 318 | * **Note:** See the [ES5 spec](https://es5.github.io/#x8) for more details. 319 | * 320 | * @static 321 | * @memberOf _ 322 | * @category Lang 323 | * @param {*} value The value to check. 324 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 325 | * @example 326 | * 327 | * _.isObject({}); 328 | * // => true 329 | * 330 | * _.isObject([1, 2, 3]); 331 | * // => true 332 | * 333 | * _.isObject(1); 334 | * // => false 335 | */ 336 | function isObject(value) { 337 | // Avoid a V8 JIT bug in Chrome 19-20. 338 | // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. 339 | var type = typeof value; 340 | return type == 'function' || (value && type == 'object') || false; 341 | } 342 | 343 | module.exports = throttle; 344 | 345 | },{"lodash.debounce":5}],5:[function(require,module,exports){ 346 | /** 347 | * lodash 3.0.2 (Custom Build) 348 | * Build: `lodash modern modularize exports="npm" -o ./` 349 | * Copyright 2012-2015 The Dojo Foundation 350 | * Based on Underscore.js 1.8.2 351 | * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 352 | * Available under MIT license 353 | */ 354 | var isNative = require('lodash.isnative'); 355 | 356 | /** Used as the `TypeError` message for "Functions" methods. */ 357 | var FUNC_ERROR_TEXT = 'Expected a function'; 358 | 359 | /* Native method references for those with the same name as other `lodash` methods. */ 360 | var nativeMax = Math.max, 361 | nativeNow = isNative(nativeNow = Date.now) && nativeNow; 362 | 363 | /** 364 | * Gets the number of milliseconds that have elapsed since the Unix epoch 365 | * (1 January 1970 00:00:00 UTC). 366 | * 367 | * @static 368 | * @memberOf _ 369 | * @category Date 370 | * @example 371 | * 372 | * _.defer(function(stamp) { 373 | * console.log(_.now() - stamp); 374 | * }, _.now()); 375 | * // => logs the number of milliseconds it took for the deferred function to be invoked 376 | */ 377 | var now = nativeNow || function() { 378 | return new Date().getTime(); 379 | }; 380 | 381 | /** 382 | * Creates a function that delays invoking `func` until after `wait` milliseconds 383 | * have elapsed since the last time it was invoked. The created function comes 384 | * with a `cancel` method to cancel delayed invocations. Provide an options 385 | * object to indicate that `func` should be invoked on the leading and/or 386 | * trailing edge of the `wait` timeout. Subsequent calls to the debounced 387 | * function return the result of the last `func` invocation. 388 | * 389 | * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked 390 | * on the trailing edge of the timeout only if the the debounced function is 391 | * invoked more than once during the `wait` timeout. 392 | * 393 | * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) 394 | * for details over the differences between `_.debounce` and `_.throttle`. 395 | * 396 | * @static 397 | * @memberOf _ 398 | * @category Function 399 | * @param {Function} func The function to debounce. 400 | * @param {number} [wait=0] The number of milliseconds to delay. 401 | * @param {Object} [options] The options object. 402 | * @param {boolean} [options.leading=false] Specify invoking on the leading 403 | * edge of the timeout. 404 | * @param {number} [options.maxWait] The maximum time `func` is allowed to be 405 | * delayed before it is invoked. 406 | * @param {boolean} [options.trailing=true] Specify invoking on the trailing 407 | * edge of the timeout. 408 | * @returns {Function} Returns the new debounced function. 409 | * @example 410 | * 411 | * // avoid costly calculations while the window size is in flux 412 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); 413 | * 414 | * // invoke `sendMail` when the click event is fired, debouncing subsequent calls 415 | * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { 416 | * 'leading': true, 417 | * 'trailing': false 418 | * })); 419 | * 420 | * // ensure `batchLog` is invoked once after 1 second of debounced calls 421 | * var source = new EventSource('/stream'); 422 | * jQuery(source).on('message', _.debounce(batchLog, 250, { 423 | * 'maxWait': 1000 424 | * })); 425 | * 426 | * // cancel a debounced call 427 | * var todoChanges = _.debounce(batchLog, 1000); 428 | * Object.observe(models.todo, todoChanges); 429 | * 430 | * Object.observe(models, function(changes) { 431 | * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { 432 | * todoChanges.cancel(); 433 | * } 434 | * }, ['delete']); 435 | * 436 | * // ...at some point `models.todo` is changed 437 | * models.todo.completed = true; 438 | * 439 | * // ...before 1 second has passed `models.todo` is deleted 440 | * // which cancels the debounced `todoChanges` call 441 | * delete models.todo; 442 | */ 443 | function debounce(func, wait, options) { 444 | var args, 445 | maxTimeoutId, 446 | result, 447 | stamp, 448 | thisArg, 449 | timeoutId, 450 | trailingCall, 451 | lastCalled = 0, 452 | maxWait = false, 453 | trailing = true; 454 | 455 | if (typeof func != 'function') { 456 | throw new TypeError(FUNC_ERROR_TEXT); 457 | } 458 | wait = wait < 0 ? 0 : (+wait || 0); 459 | if (options === true) { 460 | var leading = true; 461 | trailing = false; 462 | } else if (isObject(options)) { 463 | leading = options.leading; 464 | maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); 465 | trailing = 'trailing' in options ? options.trailing : trailing; 466 | } 467 | 468 | function cancel() { 469 | if (timeoutId) { 470 | clearTimeout(timeoutId); 471 | } 472 | if (maxTimeoutId) { 473 | clearTimeout(maxTimeoutId); 474 | } 475 | maxTimeoutId = timeoutId = trailingCall = undefined; 476 | } 477 | 478 | function delayed() { 479 | var remaining = wait - (now() - stamp); 480 | if (remaining <= 0 || remaining > wait) { 481 | if (maxTimeoutId) { 482 | clearTimeout(maxTimeoutId); 483 | } 484 | var isCalled = trailingCall; 485 | maxTimeoutId = timeoutId = trailingCall = undefined; 486 | if (isCalled) { 487 | lastCalled = now(); 488 | result = func.apply(thisArg, args); 489 | if (!timeoutId && !maxTimeoutId) { 490 | args = thisArg = null; 491 | } 492 | } 493 | } else { 494 | timeoutId = setTimeout(delayed, remaining); 495 | } 496 | } 497 | 498 | function maxDelayed() { 499 | if (timeoutId) { 500 | clearTimeout(timeoutId); 501 | } 502 | maxTimeoutId = timeoutId = trailingCall = undefined; 503 | if (trailing || (maxWait !== wait)) { 504 | lastCalled = now(); 505 | result = func.apply(thisArg, args); 506 | if (!timeoutId && !maxTimeoutId) { 507 | args = thisArg = null; 508 | } 509 | } 510 | } 511 | 512 | function debounced() { 513 | args = arguments; 514 | stamp = now(); 515 | thisArg = this; 516 | trailingCall = trailing && (timeoutId || !leading); 517 | 518 | if (maxWait === false) { 519 | var leadingCall = leading && !timeoutId; 520 | } else { 521 | if (!maxTimeoutId && !leading) { 522 | lastCalled = stamp; 523 | } 524 | var remaining = maxWait - (stamp - lastCalled), 525 | isCalled = remaining <= 0 || remaining > maxWait; 526 | 527 | if (isCalled) { 528 | if (maxTimeoutId) { 529 | maxTimeoutId = clearTimeout(maxTimeoutId); 530 | } 531 | lastCalled = stamp; 532 | result = func.apply(thisArg, args); 533 | } 534 | else if (!maxTimeoutId) { 535 | maxTimeoutId = setTimeout(maxDelayed, remaining); 536 | } 537 | } 538 | if (isCalled && timeoutId) { 539 | timeoutId = clearTimeout(timeoutId); 540 | } 541 | else if (!timeoutId && wait !== maxWait) { 542 | timeoutId = setTimeout(delayed, wait); 543 | } 544 | if (leadingCall) { 545 | isCalled = true; 546 | result = func.apply(thisArg, args); 547 | } 548 | if (isCalled && !timeoutId && !maxTimeoutId) { 549 | args = thisArg = null; 550 | } 551 | return result; 552 | } 553 | debounced.cancel = cancel; 554 | return debounced; 555 | } 556 | 557 | /** 558 | * Checks if `value` is the language type of `Object`. 559 | * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 560 | * 561 | * **Note:** See the [ES5 spec](https://es5.github.io/#x8) for more details. 562 | * 563 | * @static 564 | * @memberOf _ 565 | * @category Lang 566 | * @param {*} value The value to check. 567 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 568 | * @example 569 | * 570 | * _.isObject({}); 571 | * // => true 572 | * 573 | * _.isObject([1, 2, 3]); 574 | * // => true 575 | * 576 | * _.isObject(1); 577 | * // => false 578 | */ 579 | function isObject(value) { 580 | // Avoid a V8 JIT bug in Chrome 19-20. 581 | // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. 582 | var type = typeof value; 583 | return type == 'function' || (value && type == 'object') || false; 584 | } 585 | 586 | module.exports = debounce; 587 | 588 | },{"lodash.isnative":6}],6:[function(require,module,exports){ 589 | /** 590 | * lodash 3.0.0 (Custom Build) 591 | * Build: `lodash modern modularize exports="npm" -o ./` 592 | * Copyright 2012-2015 The Dojo Foundation 593 | * Based on Underscore.js 1.7.0 594 | * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 595 | * Available under MIT license 596 | */ 597 | 598 | /** `Object#toString` result references. */ 599 | var funcTag = '[object Function]'; 600 | 601 | /** Used to detect host constructors (Safari > 5). */ 602 | var reHostCtor = /^\[object .+?Constructor\]$/; 603 | 604 | /** 605 | * Used to match `RegExp` special characters. 606 | * See this [article on `RegExp` characters](http://www.regular-expressions.info/characters.html#special) 607 | * for more details. 608 | */ 609 | var reRegExpChars = /[.*+?^${}()|[\]\/\\]/g, 610 | reHasRegExpChars = RegExp(reRegExpChars.source); 611 | 612 | /** 613 | * Converts `value` to a string if it is not one. An empty string is returned 614 | * for `null` or `undefined` values. 615 | * 616 | * @private 617 | * @param {*} value The value to process. 618 | * @returns {string} Returns the string. 619 | */ 620 | function baseToString(value) { 621 | if (typeof value == 'string') { 622 | return value; 623 | } 624 | return value == null ? '' : (value + ''); 625 | } 626 | 627 | /** 628 | * Checks if `value` is object-like. 629 | * 630 | * @private 631 | * @param {*} value The value to check. 632 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 633 | */ 634 | function isObjectLike(value) { 635 | return (value && typeof value == 'object') || false; 636 | } 637 | 638 | /** Used for native method references. */ 639 | var objectProto = Object.prototype; 640 | 641 | /** Used to resolve the decompiled source of functions. */ 642 | var fnToString = Function.prototype.toString; 643 | 644 | /** 645 | * Used to resolve the `toStringTag` of values. 646 | * See the [ES spec](https://people.mozilla.org/~jorendorff/es6-draft.html#sec-object.prototype.tostring) 647 | * for more details. 648 | */ 649 | var objToString = objectProto.toString; 650 | 651 | /** Used to detect if a method is native. */ 652 | var reNative = RegExp('^' + 653 | escapeRegExp(objToString) 654 | .replace(/toString|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' 655 | ); 656 | 657 | /** 658 | * Checks if `value` is a native function. 659 | * 660 | * @static 661 | * @memberOf _ 662 | * @category Lang 663 | * @param {*} value The value to check. 664 | * @returns {boolean} Returns `true` if `value` is a native function, else `false`. 665 | * @example 666 | * 667 | * _.isNative(Array.prototype.push); 668 | * // => true 669 | * 670 | * _.isNative(_); 671 | * // => false 672 | */ 673 | function isNative(value) { 674 | if (value == null) { 675 | return false; 676 | } 677 | if (objToString.call(value) == funcTag) { 678 | return reNative.test(fnToString.call(value)); 679 | } 680 | return (isObjectLike(value) && reHostCtor.test(value)) || false; 681 | } 682 | 683 | /** 684 | * Escapes the `RegExp` special characters "\", "^", "$", ".", "|", "?", "*", 685 | * "+", "(", ")", "[", "]", "{" and "}" in `string`. 686 | * 687 | * @static 688 | * @memberOf _ 689 | * @category String 690 | * @param {string} [string=''] The string to escape. 691 | * @returns {string} Returns the escaped string. 692 | * @example 693 | * 694 | * _.escapeRegExp('[lodash](https://lodash.com/)'); 695 | * // => '\[lodash\]\(https://lodash\.com/\)' 696 | */ 697 | function escapeRegExp(string) { 698 | string = baseToString(string); 699 | return (string && reHasRegExpChars.test(string)) 700 | ? string.replace(reRegExpChars, '\\$&') 701 | : string; 702 | } 703 | 704 | module.exports = isNative; 705 | 706 | },{}],7:[function(require,module,exports){ 707 | "use strict"; 708 | /* jshint esnext: false */ 709 | 710 | /** 711 | CAUTION!!!! 712 | This file is used in WebWorker. 713 | So, must write with ES5, not use ES6. 714 | You need attention not to be traspiled by babel. 715 | */ 716 | 717 | var self = {}; 718 | 719 | function encoder() { 720 | self.onmessage = function (e) { 721 | switch (e.data.type) { 722 | case "encode": 723 | self.encode(e.data.audioData, e.data.format).then(function (buffer) { 724 | var data = { 725 | type: "encoded", 726 | callbackId: e.data.callbackId, 727 | buffer: buffer 728 | }; 729 | self.postMessage(data, [buffer]); 730 | }, function (err) { 731 | var data = { 732 | type: "error", 733 | callbackId: e.data.callbackId, 734 | message: err.message 735 | }; 736 | self.postMessage(data); 737 | }); 738 | break; 739 | } 740 | }; 741 | 742 | self.encode = function (audioData, format) { 743 | format.floatingPoint = !!format.floatingPoint; 744 | format.bitDepth = format.bitDepth | 0 || 16; 745 | 746 | return new Promise(function (resolve) { 747 | var numberOfChannels = audioData.numberOfChannels; 748 | var sampleRate = audioData.sampleRate; 749 | var bytes = format.bitDepth >> 3; 750 | var length = audioData.length * numberOfChannels * bytes; 751 | var writer = new BufferWriter(44 + length); 752 | 753 | writer.writeString("RIFF"); // RIFF header 754 | writer.writeUint32(writer.length - 8); // file length 755 | writer.writeString("WAVE"); // RIFF Type 756 | 757 | writer.writeString("fmt "); // format chunk identifier 758 | writer.writeUint32(16); // format chunk length 759 | writer.writeUint16(format.floatingPoint ? 3 : 1); // format (PCM) 760 | writer.writeUint16(numberOfChannels); // number of channels 761 | writer.writeUint32(sampleRate); // sample rate 762 | writer.writeUint32(sampleRate * numberOfChannels * bytes); // byte rate 763 | writer.writeUint16(numberOfChannels * bytes); // block size 764 | writer.writeUint16(format.bitDepth); // bits per sample 765 | 766 | writer.writeString("data"); // data chunk identifier 767 | writer.writeUint32(length); // data chunk length 768 | 769 | var channelData = audioData.buffers.map(function (buffer) { 770 | return new Float32Array(buffer); 771 | }); 772 | 773 | writer.writePCM(channelData, format); 774 | 775 | resolve(writer.toArrayBuffer()); 776 | }); 777 | }; 778 | 779 | function BufferWriter(length) { 780 | this.buffer = new ArrayBuffer(length); 781 | this.view = new DataView(this.buffer); 782 | this.length = length; 783 | this.pos = 0; 784 | } 785 | 786 | BufferWriter.prototype.writeUint8 = function (data) { 787 | this.view.setUint8(this.pos, data); 788 | this.pos += 1; 789 | }; 790 | 791 | BufferWriter.prototype.writeUint16 = function (data) { 792 | this.view.setUint16(this.pos, data, true); 793 | this.pos += 2; 794 | }; 795 | 796 | BufferWriter.prototype.writeUint32 = function (data) { 797 | this.view.setUint32(this.pos, data, true); 798 | this.pos += 4; 799 | }; 800 | 801 | BufferWriter.prototype.writeString = function (data) { 802 | for (var i = 0; i < data.length; i++) { 803 | this.writeUint8(data.charCodeAt(i)); 804 | } 805 | }; 806 | 807 | BufferWriter.prototype.writePCM8 = function (x) { 808 | x = Math.max(-128, Math.min(x * 128, 127)) | 0; 809 | this.view.setInt8(this.pos, x); 810 | this.pos += 1; 811 | }; 812 | 813 | BufferWriter.prototype.writePCM16 = function (x) { 814 | x = Math.max(-32768, Math.min(x * 32768, 32767)) | 0; 815 | this.view.setInt16(this.pos, x, true); 816 | this.pos += 2; 817 | }; 818 | 819 | BufferWriter.prototype.writePCM24 = function (x) { 820 | x = Math.max(-8388608, Math.min(x * 8388608, 8388607)) | 0; 821 | this.view.setUint8(this.pos + 0, x >> 0 & 255); 822 | this.view.setUint8(this.pos + 1, x >> 8 & 255); 823 | this.view.setUint8(this.pos + 2, x >> 16 & 255); 824 | this.pos += 3; 825 | }; 826 | 827 | BufferWriter.prototype.writePCM32 = function (x) { 828 | x = Math.max(-2147483648, Math.min(x * 2147483648, 2147483647)) | 0; 829 | this.view.setInt32(this.pos, x, true); 830 | this.pos += 4; 831 | }; 832 | 833 | BufferWriter.prototype.writePCM32F = function (x) { 834 | this.view.setFloat32(this.pos, x, true); 835 | this.pos += 4; 836 | }; 837 | 838 | BufferWriter.prototype.writePCM64F = function (x) { 839 | this.view.setFloat64(this.pos, x, true); 840 | this.pos += 8; 841 | }; 842 | 843 | BufferWriter.prototype.writePCM = function (channelData, format) { 844 | var length = channelData[0].length; 845 | var numberOfChannels = channelData.length; 846 | var method = "writePCM" + format.bitDepth; 847 | 848 | if (format.floatingPoint) { 849 | method += "F"; 850 | } 851 | 852 | if (!this[method]) { 853 | throw new Error("not suppoerted bit depth " + format.bitDepth); 854 | } 855 | 856 | for (var i = 0; i < length; i++) { 857 | for (var ch = 0; ch < numberOfChannels; ch++) { 858 | this[method](channelData[ch][i]); 859 | } 860 | } 861 | }; 862 | 863 | BufferWriter.prototype.toArrayBuffer = function () { 864 | return this.buffer; 865 | }; 866 | 867 | self.BufferWriter = BufferWriter; 868 | } 869 | 870 | encoder.self = encoder.util = self; 871 | 872 | module.exports = encoder; 873 | },{}],8:[function(require,module,exports){ 874 | "use strict"; 875 | 876 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; 877 | 878 | var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 879 | 880 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 881 | 882 | "use stirct"; 883 | 884 | var InlineWorker = _interopRequire(require("inline-worker")); 885 | 886 | var encoder = _interopRequire(require("./encoder-worker")); 887 | 888 | var Encoder = (function () { 889 | function Encoder() { 890 | var _this = this; 891 | 892 | var format = arguments[0] === undefined ? {} : arguments[0]; 893 | 894 | _classCallCheck(this, Encoder); 895 | 896 | this.format = { 897 | floatingPoint: !!format.floatingPoint, 898 | bitDepth: format.bitDepth | 0 || 16 }; 899 | this._worker = new InlineWorker(encoder, encoder.self); 900 | this._worker.onmessage = function (e) { 901 | var callback = _this._callbacks[e.data.callbackId]; 902 | 903 | if (callback) { 904 | if (e.data.type === "encoded") { 905 | callback.resolve(e.data.buffer); 906 | } else { 907 | callback.reject(new Error(e.data.message)); 908 | } 909 | } 910 | 911 | _this._callbacks[e.data.callbackId] = null; 912 | }; 913 | this._callbacks = []; 914 | } 915 | 916 | _createClass(Encoder, { 917 | canProcess: { 918 | value: function canProcess(format) { 919 | return Encoder.canProcess(format); 920 | } 921 | }, 922 | encode: { 923 | value: function encode(audioData, format) { 924 | var _this = this; 925 | 926 | if (format == null || typeof format !== "object") { 927 | format = this.format; 928 | } 929 | return new Promise(function (resolve, reject) { 930 | var callbackId = _this._callbacks.length; 931 | 932 | _this._callbacks.push({ resolve: resolve, reject: reject }); 933 | 934 | var numberOfChannels = audioData.channelData.length; 935 | var length = audioData.channelData[0].length; 936 | var sampleRate = audioData.sampleRate; 937 | var buffers = audioData.channelData.map(function (data) { 938 | return data.buffer; 939 | }); 940 | 941 | audioData = { numberOfChannels: numberOfChannels, length: length, sampleRate: sampleRate, buffers: buffers }; 942 | 943 | _this._worker.postMessage({ 944 | type: "encode", audioData: audioData, format: format, callbackId: callbackId 945 | }, audioData.buffers); 946 | }); 947 | } 948 | } 949 | }, { 950 | canProcess: { 951 | value: function canProcess(format) { 952 | if (format && (format === "wav" || format.type === "wav")) { 953 | return "maybe"; 954 | } 955 | return ""; 956 | } 957 | }, 958 | encode: { 959 | value: function encode(audioData, format) { 960 | return new Encoder(format).encode(audioData); 961 | } 962 | } 963 | }); 964 | 965 | return Encoder; 966 | })(); 967 | 968 | module.exports = Encoder; 969 | },{"./encoder-worker":7,"inline-worker":10}],9:[function(require,module,exports){ 970 | "use strict"; 971 | 972 | var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; }; 973 | 974 | var Encoder = _interopRequire(require("./encoder")); 975 | 976 | module.exports = Encoder; 977 | },{"./encoder":8}],10:[function(require,module,exports){ 978 | "use strict"; 979 | 980 | module.exports = require("./inline-worker"); 981 | },{"./inline-worker":11}],11:[function(require,module,exports){ 982 | (function (global){ 983 | "use strict"; 984 | 985 | var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 986 | 987 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 988 | 989 | var WORKER_ENABLED = !!(global === global.window && global.URL && global.Blob && global.Worker); 990 | 991 | var InlineWorker = (function () { 992 | function InlineWorker(func, self) { 993 | var _this = this; 994 | 995 | _classCallCheck(this, InlineWorker); 996 | 997 | if (WORKER_ENABLED) { 998 | var functionBody = func.toString().trim().match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1]; 999 | var url = global.URL.createObjectURL(new global.Blob([functionBody], { type: "text/javascript" })); 1000 | 1001 | return new global.Worker(url); 1002 | } 1003 | 1004 | this.self = self; 1005 | this.self.postMessage = function (data) { 1006 | setTimeout(function () { 1007 | _this.onmessage({ data: data }); 1008 | }, 0); 1009 | }; 1010 | 1011 | setTimeout(function () { 1012 | func.call(self); 1013 | }, 0); 1014 | } 1015 | 1016 | _createClass(InlineWorker, { 1017 | postMessage: { 1018 | value: function postMessage(data) { 1019 | var _this = this; 1020 | 1021 | setTimeout(function () { 1022 | _this.self.onmessage({ data: data }); 1023 | }, 0); 1024 | } 1025 | } 1026 | }); 1027 | 1028 | return InlineWorker; 1029 | })(); 1030 | 1031 | module.exports = InlineWorker; 1032 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 1033 | },{}],12:[function(require,module,exports){ 1034 | "use strict"; 1035 | 1036 | /* 1037 | constructor 1038 | var wal = new WebAudioLoader({cache : false, maxCacheSize : 1000, onload: function(){}, onprogress: function(){}, context : audioContext }) 1039 | */ 1040 | function WebAudioLoader (options){ 1041 | 1042 | if ( !( this instanceof WebAudioLoader ) ) { 1043 | throw new TypeError( "WebAudioLoader constructor cannot be called as a function." ); 1044 | } 1045 | 1046 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 1047 | 1048 | // Singleton using a global reference. 1049 | if (window.webAudioLoader){ 1050 | return window.webAudioLoader; 1051 | } 1052 | 1053 | // setup cache object 1054 | this._cachedAudio = null; 1055 | 1056 | // Define default local properties 1057 | this.cache = true; 1058 | this.onload = null; 1059 | this.onprogress = null; 1060 | Object.defineProperty(this,'maxCacheSize', { 1061 | enumerable: true, 1062 | configurable: false, 1063 | set : function (maxSize){ 1064 | if (this._cachedAudio){ 1065 | this._cachedAudio.max = maxSize; 1066 | } 1067 | }, 1068 | get : function (){ 1069 | return this._cachedAudio.max; 1070 | } 1071 | }); 1072 | 1073 | // Options parsing. 1074 | options = options || {}; 1075 | for (var opt in options){ 1076 | if (this.hasOwnProperty(opt) && options[opt] !== undefined){ 1077 | this[opt] = options[opt]; 1078 | } 1079 | } 1080 | this.context = options.audioContext || new AudioContext(); 1081 | 1082 | // Setup Cache 1083 | var cacheOptions = { 1084 | max: options.maxCacheSize || 1000, 1085 | length: function(audioBuffer){ 1086 | return (audioBuffer.length*audioBuffer.numberOfChannels*4)/1000; 1087 | } 1088 | }; 1089 | this._cachedAudio = require('lru-cache')(cacheOptions); 1090 | 1091 | 1092 | // Resgiter as global 1093 | window.webAudioLoader = this; 1094 | 1095 | // Helper functions 1096 | this._loadURLOrFile = function (URL, onprogress, onload){ 1097 | var urlType = Object.prototype.toString.call( URL ); 1098 | var request = null; 1099 | if (urlType === '[object String]'){ 1100 | request = new XMLHttpRequest(); 1101 | request.open('GET', URL, true); 1102 | request.responseType = 'arraybuffer'; 1103 | } 1104 | else if (urlType === '[object File]' || urlType === '[object Blob]' ){ 1105 | request = new FileReader(); 1106 | }else{ 1107 | return; 1108 | } 1109 | 1110 | request.onload = function () { 1111 | if (urlType === '[object String]' && request.status === 200){ 1112 | if (typeof onload === 'function'){ 1113 | onload(null, request.response); 1114 | } 1115 | }else if (urlType === '[object File]' || urlType === '[object Blob]'){ 1116 | if (typeof onload === 'function'){ 1117 | onload(null, request.result); 1118 | } 1119 | }else{ 1120 | if (typeof onload === 'function'){ 1121 | onload(new Error("Loading Error"), null); 1122 | } 1123 | } 1124 | }; 1125 | request.onerror = function(){ 1126 | if (typeof onload === 'function'){ 1127 | onload(new Error("Loading Error"), null); 1128 | } 1129 | }; 1130 | request.onprogress = function(event){ 1131 | if (typeof onprogress === 'function'){ 1132 | onprogress(event); 1133 | } 1134 | 1135 | if (typeof this.onprogress === 'function'){ 1136 | this.onprogress(event); 1137 | } 1138 | }.bind(this); 1139 | 1140 | if (urlType === '[object String]'){ 1141 | request.send(); 1142 | }else if (urlType === '[object File]' || urlType === '[object Blob]' ){ 1143 | request.readAsArrayBuffer( URL ); 1144 | } 1145 | 1146 | }; 1147 | } 1148 | /* 1149 | load method. 1150 | wal.load('http://www.example.com/audio.mp3'); 1151 | wal.load([object File]); 1152 | wal.load('http://www.example.com/audio.mp3', {decode: false,cache : false , onload: function(){}, onprogress: function(){}}); 1153 | */ 1154 | WebAudioLoader.prototype.load = function (source, options){ 1155 | 1156 | var decode = true; 1157 | var thisLoadCache = true; 1158 | var thisLoadOnload = options.onload || null; 1159 | var thisLoadOnprogress = options.onprogress || null; 1160 | // var startPoint = options.startPoint || 0; 1161 | // var endPoint = options.endPoint || 0; 1162 | 1163 | 1164 | if (options.cache !== null && options.cache !== undefined){ 1165 | thisLoadCache = options.cache; 1166 | }else{ 1167 | thisLoadCache = this.cache; 1168 | } 1169 | 1170 | if (options.decode !== null && options.decode !== undefined){ 1171 | decode = options.decode; 1172 | } 1173 | 1174 | var onLoadProxy = function (err,audioBuffer){ 1175 | if(typeof thisLoadOnload === 'function'){ 1176 | thisLoadOnload(err,audioBuffer); 1177 | } 1178 | if (typeof this.onload === 'function'){ 1179 | this.onload(err,audioBuffer); 1180 | } 1181 | }.bind(this); 1182 | 1183 | if (this.cache && thisLoadCache){ 1184 | var testCache = this._cachedAudio.get(source); 1185 | if (testCache){ 1186 | onLoadProxy(null, testCache); 1187 | return; 1188 | } 1189 | } 1190 | 1191 | this._loadURLOrFile(source, thisLoadOnprogress, function (err, arrayBuffer){ 1192 | if(err || !decode){ 1193 | onLoadProxy(err,arrayBuffer); 1194 | }else{ 1195 | this.context.decodeAudioData(arrayBuffer, function(audioBuffer){ 1196 | if (thisLoadCache){ 1197 | this._cachedAudio.set(source,audioBuffer); 1198 | } 1199 | onLoadProxy(err,audioBuffer); 1200 | }.bind(this), function(){ 1201 | onLoadProxy(new Error("Decoding Error"),null); 1202 | }.bind(this)); 1203 | } 1204 | }.bind(this)); 1205 | }; 1206 | 1207 | /* 1208 | flushCache method 1209 | Resets and empties the cache. 1210 | */ 1211 | WebAudioLoader.prototype.flushCache = function (){ 1212 | this._cachedAudio.reset(); 1213 | }; 1214 | 1215 | module.exports = WebAudioLoader; 1216 | 1217 | },{"lru-cache":13}],13:[function(require,module,exports){ 1218 | ;(function () { // closure for web browsers 1219 | 1220 | if (typeof module === 'object' && module.exports) { 1221 | module.exports = LRUCache 1222 | } else { 1223 | // just set the global for non-node platforms. 1224 | this.LRUCache = LRUCache 1225 | } 1226 | 1227 | function hOP (obj, key) { 1228 | return Object.prototype.hasOwnProperty.call(obj, key) 1229 | } 1230 | 1231 | function naiveLength () { return 1 } 1232 | 1233 | function LRUCache (options) { 1234 | if (!(this instanceof LRUCache)) 1235 | return new LRUCache(options) 1236 | 1237 | if (typeof options === 'number') 1238 | options = { max: options } 1239 | 1240 | if (!options) 1241 | options = {} 1242 | 1243 | this._max = options.max 1244 | // Kind of weird to have a default max of Infinity, but oh well. 1245 | if (!this._max || !(typeof this._max === "number") || this._max <= 0 ) 1246 | this._max = Infinity 1247 | 1248 | this._lengthCalculator = options.length || naiveLength 1249 | if (typeof this._lengthCalculator !== "function") 1250 | this._lengthCalculator = naiveLength 1251 | 1252 | this._allowStale = options.stale || false 1253 | this._maxAge = options.maxAge || null 1254 | this._dispose = options.dispose 1255 | this.reset() 1256 | } 1257 | 1258 | // resize the cache when the max changes. 1259 | Object.defineProperty(LRUCache.prototype, "max", 1260 | { set : function (mL) { 1261 | if (!mL || !(typeof mL === "number") || mL <= 0 ) mL = Infinity 1262 | this._max = mL 1263 | if (this._length > this._max) trim(this) 1264 | } 1265 | , get : function () { return this._max } 1266 | , enumerable : true 1267 | }) 1268 | 1269 | // resize the cache when the lengthCalculator changes. 1270 | Object.defineProperty(LRUCache.prototype, "lengthCalculator", 1271 | { set : function (lC) { 1272 | if (typeof lC !== "function") { 1273 | this._lengthCalculator = naiveLength 1274 | this._length = this._itemCount 1275 | for (var key in this._cache) { 1276 | this._cache[key].length = 1 1277 | } 1278 | } else { 1279 | this._lengthCalculator = lC 1280 | this._length = 0 1281 | for (var key in this._cache) { 1282 | this._cache[key].length = this._lengthCalculator(this._cache[key].value) 1283 | this._length += this._cache[key].length 1284 | } 1285 | } 1286 | 1287 | if (this._length > this._max) trim(this) 1288 | } 1289 | , get : function () { return this._lengthCalculator } 1290 | , enumerable : true 1291 | }) 1292 | 1293 | Object.defineProperty(LRUCache.prototype, "length", 1294 | { get : function () { return this._length } 1295 | , enumerable : true 1296 | }) 1297 | 1298 | 1299 | Object.defineProperty(LRUCache.prototype, "itemCount", 1300 | { get : function () { return this._itemCount } 1301 | , enumerable : true 1302 | }) 1303 | 1304 | LRUCache.prototype.forEach = function (fn, thisp) { 1305 | thisp = thisp || this 1306 | var i = 0; 1307 | for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { 1308 | i++ 1309 | var hit = this._lruList[k] 1310 | if (this._maxAge && (Date.now() - hit.now > this._maxAge)) { 1311 | del(this, hit) 1312 | if (!this._allowStale) hit = undefined 1313 | } 1314 | if (hit) { 1315 | fn.call(thisp, hit.value, hit.key, this) 1316 | } 1317 | } 1318 | } 1319 | 1320 | LRUCache.prototype.keys = function () { 1321 | var keys = new Array(this._itemCount) 1322 | var i = 0 1323 | for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { 1324 | var hit = this._lruList[k] 1325 | keys[i++] = hit.key 1326 | } 1327 | return keys 1328 | } 1329 | 1330 | LRUCache.prototype.values = function () { 1331 | var values = new Array(this._itemCount) 1332 | var i = 0 1333 | for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { 1334 | var hit = this._lruList[k] 1335 | values[i++] = hit.value 1336 | } 1337 | return values 1338 | } 1339 | 1340 | LRUCache.prototype.reset = function () { 1341 | if (this._dispose && this._cache) { 1342 | for (var k in this._cache) { 1343 | this._dispose(k, this._cache[k].value) 1344 | } 1345 | } 1346 | 1347 | this._cache = Object.create(null) // hash of items by key 1348 | this._lruList = Object.create(null) // list of items in order of use recency 1349 | this._mru = 0 // most recently used 1350 | this._lru = 0 // least recently used 1351 | this._length = 0 // number of items in the list 1352 | this._itemCount = 0 1353 | } 1354 | 1355 | // Provided for debugging/dev purposes only. No promises whatsoever that 1356 | // this API stays stable. 1357 | LRUCache.prototype.dump = function () { 1358 | return this._cache 1359 | } 1360 | 1361 | LRUCache.prototype.dumpLru = function () { 1362 | return this._lruList 1363 | } 1364 | 1365 | LRUCache.prototype.set = function (key, value) { 1366 | if (hOP(this._cache, key)) { 1367 | // dispose of the old one before overwriting 1368 | if (this._dispose) this._dispose(key, this._cache[key].value) 1369 | if (this._maxAge) this._cache[key].now = Date.now() 1370 | this._cache[key].value = value 1371 | this.get(key) 1372 | return true 1373 | } 1374 | 1375 | var len = this._lengthCalculator(value) 1376 | var age = this._maxAge ? Date.now() : 0 1377 | var hit = new Entry(key, value, this._mru++, len, age) 1378 | 1379 | // oversized objects fall out of cache automatically. 1380 | if (hit.length > this._max) { 1381 | if (this._dispose) this._dispose(key, value) 1382 | return false 1383 | } 1384 | 1385 | this._length += hit.length 1386 | this._lruList[hit.lu] = this._cache[key] = hit 1387 | this._itemCount ++ 1388 | 1389 | if (this._length > this._max) trim(this) 1390 | return true 1391 | } 1392 | 1393 | LRUCache.prototype.has = function (key) { 1394 | if (!hOP(this._cache, key)) return false 1395 | var hit = this._cache[key] 1396 | if (this._maxAge && (Date.now() - hit.now > this._maxAge)) { 1397 | return false 1398 | } 1399 | return true 1400 | } 1401 | 1402 | LRUCache.prototype.get = function (key) { 1403 | return get(this, key, true) 1404 | } 1405 | 1406 | LRUCache.prototype.peek = function (key) { 1407 | return get(this, key, false) 1408 | } 1409 | 1410 | LRUCache.prototype.pop = function () { 1411 | var hit = this._lruList[this._lru] 1412 | del(this, hit) 1413 | return hit || null 1414 | } 1415 | 1416 | LRUCache.prototype.del = function (key) { 1417 | del(this, this._cache[key]) 1418 | } 1419 | 1420 | function get (self, key, doUse) { 1421 | var hit = self._cache[key] 1422 | if (hit) { 1423 | if (self._maxAge && (Date.now() - hit.now > self._maxAge)) { 1424 | del(self, hit) 1425 | if (!self._allowStale) hit = undefined 1426 | } else { 1427 | if (doUse) use(self, hit) 1428 | } 1429 | if (hit) hit = hit.value 1430 | } 1431 | return hit 1432 | } 1433 | 1434 | function use (self, hit) { 1435 | shiftLU(self, hit) 1436 | hit.lu = self._mru ++ 1437 | self._lruList[hit.lu] = hit 1438 | } 1439 | 1440 | function trim (self) { 1441 | while (self._lru < self._mru && self._length > self._max) 1442 | del(self, self._lruList[self._lru]) 1443 | } 1444 | 1445 | function shiftLU (self, hit) { 1446 | delete self._lruList[ hit.lu ] 1447 | while (self._lru < self._mru && !self._lruList[self._lru]) self._lru ++ 1448 | } 1449 | 1450 | function del (self, hit) { 1451 | if (hit) { 1452 | if (self._dispose) self._dispose(hit.key, hit.value) 1453 | self._length -= hit.length 1454 | self._itemCount -- 1455 | delete self._cache[ hit.key ] 1456 | shiftLU(self, hit) 1457 | } 1458 | } 1459 | 1460 | // classy, since V8 prefers predictable objects. 1461 | function Entry (key, value, lu, length, now) { 1462 | this.key = key 1463 | this.value = value 1464 | this.lu = lu 1465 | this.length = length 1466 | this.now = now 1467 | } 1468 | 1469 | })() 1470 | 1471 | },{}]},{},[1]); 1472 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Resampler 5 | 6 | 7 | 8 | 9 | 10 |

Resampler

11 |

Audio resampling for nerds!

12 |

Resample to 13 | 23 |

24 |
25 |
26 |

Drop files here or click to upload an audio file to resample

27 | 34 |
35 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var resampler = require('./lib/resampler.js'); 4 | var dragDrop = require('drag-drop'); 5 | 6 | window.addEventListener('load', function(){ 7 | // dom elements 8 | var fSelectOption = document.getElementById('freqSelect'); 9 | var messageBox = document.getElementById('message'); 10 | var input = document.getElementById('input'); 11 | var note = document.getElementById('note'); 12 | var spinner = document.getElementById('spinner'); 13 | 14 | //monkeypatch 15 | var ddEventListeners = {}; 16 | var dropzone = document.querySelector('#dropzone'); 17 | var dEL = dropzone.addEventListener.bind(dropzone); 18 | 19 | dropzone.addEventListener = function(event, callback, flag){ 20 | ddEventListeners[event] = callback; 21 | dEL(event,callback,flag); 22 | }; 23 | 24 | var disableDragDrop = function(elem) { 25 | elem.removeEventListener ('dragenter', ddEventListeners.dragenter); 26 | elem.removeEventListener('dragover', ddEventListeners.dragover); 27 | elem.removeEventListener('drop', ddEventListeners.drop); 28 | }; 29 | 30 | dragDrop('#dropzone', resampleDraggedFiles); 31 | 32 | messageBox.addEventListener('click', function (){ 33 | input.click(); 34 | }); 35 | 36 | input.addEventListener('change', function(evt){ 37 | var chosenFile = evt.target.files[0]; 38 | if (chosenFile){ 39 | var chosenSampleRate = parseInt(fSelectOption.selectedOptions[0].value); 40 | console.log(chosenFile,chosenSampleRate); 41 | resampleFile(chosenFile,chosenSampleRate); 42 | } 43 | }); 44 | 45 | function resampleDraggedFiles(files){ 46 | var chosenFile = files[0] || files; 47 | var chosenSampleRate = parseInt(fSelectOption.selectedOptions[0].value); 48 | console.log(chosenFile,chosenSampleRate); 49 | resampleFile(chosenFile,chosenSampleRate); 50 | } 51 | 52 | function resampleFile (file, targetSampleRate){ 53 | // note.messageBox. 54 | note.style.display = "none"; 55 | spinner.style.display = "inherit"; 56 | input.disabled = true; 57 | disableDragDrop(dropzone); 58 | resampler(file, targetSampleRate, function(event){ 59 | event.getFile(function(fileEvent){ 60 | console.log(fileEvent); 61 | spinner.style.display = "none"; 62 | note.style.display = "inherit"; 63 | input.disabled = false; 64 | dragDrop('#dropzone'); 65 | var a = document.createElement("a"); 66 | document.body.appendChild(a); 67 | a.style.display = "none"; 68 | a.href = fileEvent; 69 | var fileExt = file.name.split('.').pop(); 70 | var fileName = file.name.substr(0, file.name.length-fileExt.length-1); 71 | a.download = fileName + "_resampled."+ fileExt; 72 | a.click(); 73 | window.URL.revokeObjectURL(fileEvent); 74 | document.body.removeChild(a); 75 | }); 76 | }); 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /lib/resampler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var WebAudioLoader = require('webaudioloader'); 4 | var WavEncoder = require("wav-encoder"); 5 | 6 | // WebAudio Shim. 7 | window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext; 8 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 9 | 10 | var audioContext = new AudioContext(); 11 | 12 | var wal = new WebAudioLoader({ 13 | context: audioContext, 14 | cache: false 15 | }); 16 | 17 | function resampler(input, targetSampleRate, oncomplete) { 18 | 19 | if (!input && !targetSampleRate) { 20 | return returnError('Error: First argument should be either a File, URL or AudioBuffer'); 21 | } 22 | 23 | var inputType = Object.prototype.toString.call(input); 24 | if (inputType !== '[object String]' && 25 | inputType !== '[object File]' && 26 | inputType !== '[object AudioBuffer]' && 27 | inputType !== '[object Object]') { 28 | return returnError('Error: First argument should be either a File, URL or AudioBuffer'); 29 | } 30 | 31 | if (typeof targetSampleRate !== 'number' || 32 | targetSampleRate > 192000 || targetSampleRate < 3000) { 33 | return returnError('Error: Second argument should be a numeric sample rate between 3000 and 192000'); 34 | } 35 | 36 | if (inputType === '[object String]' || inputType === '[object File]') { 37 | console.log('Loading/decoding input', input); 38 | wal.load(input, { 39 | onload: function(err, audioBuffer) { 40 | if (err) { 41 | return returnError(err); 42 | } 43 | resampleAudioBuffer(audioBuffer); 44 | } 45 | }); 46 | } else if (inputType === '[object AudioBuffer]') { 47 | resampleAudioBuffer(input); 48 | } else if (inputType === '[object Object]' && input.leftBuffer && input.sampleRate) { 49 | var numCh_ = input.rightBuffer ? 2 : 1; 50 | var audioBuffer_ = audioContext.createBuffer(numCh_, input.leftBuffer.length, input.sampleRate); 51 | resampleAudioBuffer(audioBuffer_); 52 | } else { 53 | return returnError('Error: Unknown input type'); 54 | } 55 | 56 | function returnError(errMsg) { 57 | console.error(errMsg); 58 | if (typeof oncomplete === 'function') { 59 | oncomplete(new Error(errMsg)); 60 | } 61 | return; 62 | } 63 | 64 | function resampleAudioBuffer(audioBuffer) { 65 | 66 | 67 | var numCh_ = audioBuffer.numberOfChannels; 68 | var numFrames_ = audioBuffer.length * targetSampleRate / audioBuffer.sampleRate; 69 | 70 | var offlineContext_ = new OfflineAudioContext(numCh_, numFrames_, targetSampleRate); 71 | var bufferSource_ = offlineContext_.createBufferSource(); 72 | bufferSource_.buffer = audioBuffer; 73 | 74 | offlineContext_.oncomplete = function(event) { 75 | var resampeledBuffer = event.renderedBuffer; 76 | console.log('Done Rendering'); 77 | if (typeof oncomplete === 'function') { 78 | oncomplete({ 79 | getAudioBuffer: function() { 80 | return resampeledBuffer; 81 | }, 82 | getFile: function(fileCallback) { 83 | var audioData = { 84 | sampleRate: resampeledBuffer.sampleRate, 85 | channelData: [] 86 | }; 87 | for (var i = 0; i < resampeledBuffer.numberOfChannels; i++) { 88 | audioData.channelData[i] = resampeledBuffer.getChannelData(i); 89 | } 90 | WavEncoder.encode(audioData).then(function(buffer) { 91 | var blob = new Blob([buffer], { 92 | type: "audio/wav" 93 | }); 94 | fileCallback(URL.createObjectURL(blob)); 95 | }); 96 | } 97 | }); 98 | } 99 | }; 100 | 101 | console.log('Starting Offline Rendering'); 102 | bufferSource_.connect(offlineContext_.destination); 103 | bufferSource_.start(0); 104 | offlineContext_.startRendering(); 105 | } 106 | } 107 | 108 | 109 | module.exports = resampler; 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audio-resampler", 3 | "version": "1.0.1", 4 | "description": "Simple WebAudio based resampling library", 5 | "main": "lib/resampler.js", 6 | "files": ["lib/resampler.js"], 7 | "scripts": { 8 | "build": "jshint index.js lib/resampler.js && browserify index.js > bundle.js", 9 | "watch:index": "watch 'npm run build' . --wait=5", 10 | "build:watch": "parallelshell 'npm run watch:index' 'http-server'", 11 | "start": "npm run build && http-server" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/notthetup/resampler.git" 16 | }, 17 | "keywords": [ 18 | "webaudio", 19 | "resample", 20 | "audio" 21 | ], 22 | "author": "Chinmay Pendharkar ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/notthetup/resampler/issues" 26 | }, 27 | "homepage": "https://github.com/notthetup/resampler", 28 | "dependencies": { 29 | "wav-encoder": "^0.2.2", 30 | "webaudioloader": "^1.0.1" 31 | }, 32 | "devDependencies": { 33 | "browserify": "^9.0.3", 34 | "drag-drop": "^2.0.0", 35 | "jshint": "^2.6.3", 36 | "parallelshell": "^1.1.1", 37 | "watch": "^0.14.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); 2 | * { 3 | -webkit-box-sizing: border-box; 4 | -moz-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | body { 8 | font-family: "Lato","Helvetica Neue",Helvetica,Arial,sans-serif; 9 | font-weight: 400; 10 | line-height: 1.1; 11 | color: inherit; 12 | } 13 | input, 14 | button, 15 | select, 16 | textarea { 17 | font-family: inherit; 18 | font-size: inherit; 19 | line-height: inherit; 20 | } 21 | a { 22 | color: #18bc9c; 23 | text-decoration: none; 24 | } 25 | a:hover, 26 | a:focus { 27 | color: #18bc9c; 28 | text-decoration: underline; 29 | } 30 | a:focus { 31 | outline: thin dotted; 32 | outline: 5px auto -webkit-focus-ring-color; 33 | outline-offset: -2px; 34 | } 35 | figure { 36 | margin: 0; 37 | } 38 | img { 39 | vertical-align: middle; 40 | } 41 | 42 | h1, 43 | h2, 44 | h3, 45 | h4, 46 | h5, 47 | h6, 48 | .h1, 49 | .h2, 50 | .h3, 51 | .h4, 52 | .h5, 53 | .h6 { 54 | font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 55 | font-weight: 400; 56 | line-height: 1.1; 57 | color: inherit; 58 | } 59 | 60 | h1, 61 | .h1 { 62 | font-size: 60px; 63 | } 64 | h2, 65 | .h2 { 66 | font-size: 32px; 67 | } 68 | h3, 69 | .h3 { 70 | font-size: 26px; 71 | } 72 | h4, 73 | .h4 { 74 | font-size: 19px; 75 | } 76 | h5, 77 | .h5 { 78 | font-size: 15px; 79 | } 80 | h6, 81 | .h6 { 82 | font-size: 13px; 83 | } 84 | 85 | .center { 86 | margin-left: auto; 87 | margin-right: auto; 88 | width: 6em 89 | } 90 | .dropzone { 91 | border: 2px solid rgba(0, 0, 0, 0.3); 92 | border-radius: 5px; 93 | max-width: 60%; 94 | margin-left: auto; 95 | margin-right: auto; 96 | min-height: 200px; 97 | padding: 20px 20px; 98 | } 99 | .dropzone .message { 100 | cursor: pointer; 101 | text-align: center; 102 | margin: 2em 0; 103 | } 104 | 105 | .spinner { 106 | margin: 100px auto; 107 | width: 50px; 108 | height: 30px; 109 | text-align: center; 110 | font-size: 10px; 111 | } 112 | 113 | .spinner > div { 114 | background-color: #333; 115 | height: 100%; 116 | width: 6px; 117 | display: inline-block; 118 | 119 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 120 | animation: stretchdelay 1.2s infinite ease-in-out; 121 | } 122 | 123 | .spinner .rect2 { 124 | -webkit-animation-delay: -1.1s; 125 | animation-delay: -1.1s; 126 | } 127 | 128 | .spinner .rect3 { 129 | -webkit-animation-delay: -1.0s; 130 | animation-delay: -1.0s; 131 | } 132 | 133 | .spinner .rect4 { 134 | -webkit-animation-delay: -0.9s; 135 | animation-delay: -0.9s; 136 | } 137 | 138 | .spinner .rect5 { 139 | -webkit-animation-delay: -0.8s; 140 | animation-delay: -0.8s; 141 | } 142 | 143 | @-webkit-keyframes stretchdelay { 144 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 145 | 20% { -webkit-transform: scaleY(1.0) } 146 | } 147 | 148 | @keyframes stretchdelay { 149 | 0%, 40%, 100% { 150 | transform: scaleY(0.4); 151 | -webkit-transform: scaleY(0.4); 152 | } 20% { 153 | transform: scaleY(1.0); 154 | -webkit-transform: scaleY(1.0); 155 | } 156 | } 157 | 158 | h1,h2,h3 { 159 | text-align: center; 160 | margin-top: 21px; 161 | margin-bottom: 10.5px; 162 | } 163 | 164 | select { 165 | background: transparent; 166 | border: 1px dashed #ddd; 167 | padding-left: 2px; 168 | padding-bottom: 2px; 169 | /* -webkit-appearance: none; 170 | -moz-appearance: none; 171 | appearance: none;*/ 172 | } 173 | 174 | /* 175 | 1. bigger title 176 | 2. make the title and tagline same width 177 | 3. there should be a down arrow in the dropdown 178 | 4. starting state of the dropdown should have "Select sampling rate" 179 | 5. put a dashed border for the drop files section and grey out the text and border 180 | 6. make the drop files section bigger height 181 | 7. put a grey border for the dropdown instead of red 182 | */ 183 | --------------------------------------------------------------------------------