├── _config.yml ├── index.js ├── musquito.png ├── sounds ├── bg.mp3 ├── beep.mp3 ├── click.mp3 ├── button.mp3 ├── sprite.ac3 ├── sprite.m4a ├── sprite.mp3 ├── sprite.ogg └── sprite.json ├── .gitignore ├── .npmignore ├── .babelrc ├── src ├── DownloadStatus.js ├── LoadState.js ├── DownloadResult.js ├── Queue.js ├── Emitter.js ├── BufferLoader.js ├── Utility.js ├── WorkerTimer.js ├── MediaLoader.js ├── BufferLoader.spec.js ├── Html5AudioPool.js ├── Sound.js ├── Buzz.js └── Engine.js ├── tests ├── autoplay.html └── html5audiotest.html ├── CONTRIBUTING.md ├── .editorconfig ├── webpack.dev.config.babel.js ├── LICENSE ├── karma.conf.js ├── webpack.config.babel.js ├── dev.js ├── package.json ├── index.html ├── .eslintrc └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/musquito-3.0.1'); 2 | -------------------------------------------------------------------------------- /musquito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/musquito.png -------------------------------------------------------------------------------- /sounds/bg.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/bg.mp3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log* 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log* 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/beep.mp3 -------------------------------------------------------------------------------- /sounds/click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/click.mp3 -------------------------------------------------------------------------------- /sounds/button.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/button.mp3 -------------------------------------------------------------------------------- /sounds/sprite.ac3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/sprite.ac3 -------------------------------------------------------------------------------- /sounds/sprite.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/sprite.m4a -------------------------------------------------------------------------------- /sounds/sprite.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/sprite.mp3 -------------------------------------------------------------------------------- /sounds/sprite.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJAI/musquito/master/sounds/sprite.ogg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "babel-plugin-transform-class-properties", 5 | "transform-export-extensions", 6 | "babel-plugin-transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /sounds/sprite.json: -------------------------------------------------------------------------------- 1 | { 2 | "beep": [ 3 | 0, 4 | 0.48108843537414964 5 | ], 6 | "button": [ 7 | 2, 8 | 2.4290249433106577 9 | ], 10 | "click": [ 11 | 4, 12 | 4.672018140589569 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/DownloadStatus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum to represent the download status of audio resource. 3 | * @enum {string} 4 | */ 5 | const DownloadStatus = { 6 | Success: 'success', 7 | Failure: 'error' 8 | }; 9 | 10 | export default DownloadStatus; 11 | -------------------------------------------------------------------------------- /src/LoadState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum that represents the different states occurs while loading a sound. 3 | * @enum {string} 4 | */ 5 | const LoadState = { 6 | NotLoaded: 'notloaded', 7 | Loading: 'loading', 8 | Loaded: 'loaded' 9 | }; 10 | 11 | export default LoadState; 12 | -------------------------------------------------------------------------------- /tests/autoplay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Autoplay Test 5 | 6 | 13 | 14 | 15 |

Autoplay Test

16 | 17 | 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | I would be more than happy to help someone contribute to this awesome project. If anyone interested to contribute to this project in any ways please reach me out through my website [http://prideparrot.com/contact](http://prideparrot.com/contact). 4 | 5 | ## Todo Items 6 | 7 | 1. 3D Spatial Plugin (not-started) 8 | 2. Low Pass Filter (not-started) 9 | 3. Room Effects Support (not-started) 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /webpack.dev.config.babel.js: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import { HotModuleReplacementPlugin } from 'webpack'; 3 | 4 | export default { 5 | mode: 'development', 6 | entry: { app: './dev.js' }, 7 | module: { 8 | rules: [ 9 | { test: /\.js$/, exclude: /node_modules/, loader: 'eslint-loader', enforce: 'pre' }, 10 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 11 | ] 12 | }, 13 | devServer: { 14 | inline: true, 15 | hot: true 16 | }, 17 | plugins: [ 18 | new HotModuleReplacementPlugin(), 19 | new HtmlWebpackPlugin({ 20 | template: './index.html', 21 | inject: true 22 | }) 23 | ], 24 | devtool: 'source-map' 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Vijaya Anand (http://www.prideparrot.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /src/DownloadResult.js: -------------------------------------------------------------------------------- 1 | import DownloadStatus from './DownloadStatus'; 2 | 3 | /** 4 | * Represents the download result of an audio. 5 | * @class 6 | */ 7 | class DownloadResult { 8 | 9 | /** 10 | * The url of the audio resource 11 | * @type {string|null} 12 | */ 13 | url = null; 14 | 15 | /** 16 | * AudioBuffer or Html5Audio element 17 | * @type {AudioBuffer|Audio} 18 | */ 19 | value = null; 20 | 21 | /** 22 | * Download error 23 | * @type {any} 24 | */ 25 | error = null; 26 | 27 | /** 28 | * Success or failure status of download. 29 | * @type {DownloadStatus} 30 | */ 31 | status = null; 32 | 33 | /** 34 | * @param {string|null} url The url of the audio resource 35 | * @param {AudioBuffer|Audio} [value] AudioBuffer or Html5Audio element 36 | * @param {*} [error] Download error 37 | */ 38 | constructor(url, value, error) { 39 | this.url = url; 40 | this.value = value; 41 | this.error = error || null; 42 | this.status = error ? DownloadStatus.Failure : DownloadStatus.Success; 43 | } 44 | } 45 | 46 | export default DownloadResult; 47 | -------------------------------------------------------------------------------- /tests/html5audiotest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML5 Audio Preload Test 5 | 6 | 7 |

HTML5 Audio Preload Test

8 | 9 | 10 | 11 | 12 | 13 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '.', 4 | frameworks: ['jasmine-ajax', 'jasmine'], 5 | files: [ 6 | 'src/**/*.spec.js', 7 | { pattern: 'sounds/*.*', included: false } 8 | ], 9 | preprocessors: { 10 | 'src/**/*.spec.js': ['webpack', 'sourcemap'] 11 | }, 12 | webpack: { 13 | module: { 14 | rules: [ 15 | { test: /\.js$/, exclude: /node_modules/, loader: 'eslint-loader', enforce: 'pre' }, 16 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 17 | ] 18 | }, 19 | devtool: 'inline-source-map' 20 | }, 21 | reporters: ['mocha'], 22 | port: 9876, 23 | colors: true, 24 | logLevel: config.LOG_INFO, 25 | autoWatch: true, 26 | plugins: [ 27 | 'karma-chrome-launcher', 28 | 'karma-jasmine', 29 | 'karma-jasmine-ajax', 30 | 'karma-webpack', 31 | 'karma-mocha-reporter', 32 | 'karma-sourcemap-loader' 33 | ], 34 | browsers: ['Chrome'], 35 | singleRun: false, 36 | concurrency: Infinity 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import yargs from 'yargs'; 3 | import pkg from './package.json'; 4 | 5 | const { optimizeMinimize } = yargs.alias('p', 'optimize-minimize').argv; 6 | const nodeEnv = optimizeMinimize ? 'production' : 'development'; 7 | const version = pkg.version; 8 | 9 | export default { 10 | mode: 'production', 11 | entry: { app: './src/Buzz.js' }, 12 | output: { 13 | path: __dirname + '/dist', 14 | filename: optimizeMinimize ? `musquito-${version}.min.js` : `musquito-${version}.js`, 15 | library: '$buzz', 16 | libraryTarget: 'umd', 17 | umdNamedDefine: true 18 | }, 19 | module: { 20 | rules: [ 21 | { test: /\.js$/, exclude: /node_modules/, loader: 'eslint-loader', enforce: 'pre' }, 22 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 23 | ] 24 | }, 25 | plugins: [ 26 | new webpack.BannerPlugin({ 27 | banner: 28 | `/*! 29 | * musquito v3.0.1 30 | * http://musquitojs.com 31 | * 32 | * (c) 2020 Vijaya Anand 33 | * http://prideparrot.com 34 | * 35 | * MIT License 36 | */`, 37 | raw: true 38 | }) 39 | ], 40 | devtool: optimizeMinimize ? 'source-map' : false 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import $buzz from './src/Buzz'; 4 | import engine from './src/Engine'; 5 | 6 | class EngineTester { 7 | 8 | _codeTextArea = null; 9 | 10 | _runBtn = null; 11 | 12 | _clearBtn = null; 13 | 14 | init() { 15 | window.$buzz = $buzz; 16 | window.engine = engine; 17 | 18 | this.run = this.run.bind(this); 19 | this.clear = this.clear.bind(this); 20 | 21 | this._codeTextArea = document.getElementById('code'); 22 | this._runBtn = document.getElementById('run'); 23 | this._clearBtn = document.getElementById('clear'); 24 | 25 | this._runBtn.addEventListener('click', this.run); 26 | this._clearBtn.addEventListener('click', this.clear); 27 | 28 | this._codeTextArea.value = window.localStorage.getItem('code'); 29 | this._codeTextArea.focus(); 30 | } 31 | 32 | run() { 33 | const code = this._codeTextArea.value; 34 | window.localStorage.setItem('code', code); 35 | eval(code); 36 | } 37 | 38 | clear() { 39 | window.localStorage.setItem('code', ''); 40 | this._codeTextArea.value = ''; 41 | } 42 | } 43 | 44 | window.engineTester = new EngineTester(); 45 | window.addEventListener('load', () => engineTester.init()); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "musquito", 3 | "version": "3.0.1", 4 | "description": "An audio engine for HTML5 games and interactive websites", 5 | "keywords": [ 6 | "audio", 7 | "sound", 8 | "audio engine", 9 | "player", 10 | "web audio api", 11 | "javascript" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/VJAI/musquito.git" 16 | }, 17 | "author": "Vijaya Anand", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/VJAI/musquito/issues" 21 | }, 22 | "homepage": "https://github.com/VJAI/musquito#readme", 23 | "scripts": { 24 | "start": "opener http://localhost:8080/index.html && webpack-dev-server --config webpack.dev.config.babel.js", 25 | "build": "webpack && webpack -p", 26 | "test": "karma start" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.26.0", 30 | "babel-core": "^6.26.3", 31 | "babel-eslint": "^10.0.1", 32 | "babel-loader": "^7.1.5", 33 | "babel-plugin-transform-class-properties": "^6.24.1", 34 | "babel-plugin-transform-export-extensions": "^6.22.0", 35 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-register": "^6.26.0", 38 | "eslint": "^5.13.0", 39 | "eslint-loader": "^2.1.2", 40 | "html-webpack-banner-plugin": "^2.0.0", 41 | "html-webpack-plugin": "^3.2.0", 42 | "karma": "^4.4.1", 43 | "karma-chrome-launcher": "^3.1.0", 44 | "karma-coverage": "^2.0.1", 45 | "karma-jasmine": "^3.1.1", 46 | "karma-jasmine-ajax": "^0.1.13", 47 | "karma-mocha-reporter": "^2.2.5", 48 | "karma-sourcemap-loader": "^0.3.7", 49 | "karma-webpack": "^4.0.2", 50 | "opener": "^1.5.1", 51 | "raw-loader": "^1.0.0", 52 | "webpack": "^4.29.3", 53 | "webpack-cli": "^3.2.3", 54 | "webpack-dev-server": "^3.1.14", 55 | "yargs": "^12.0.5" 56 | }, 57 | "dependencies": { 58 | "babel-polyfill": "^6.26.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | musquito test drive 4 | 5 | 88 | 89 | 90 | 91 |
92 |

musquito

93 |
94 | 95 |
96 | 97 |
98 | 99 | 100 |
101 |
102 | 103 | 106 | 107 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/Queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores queue of actions that has to be run before or after specific events. 3 | */ 4 | class Queue { 5 | 6 | _eventActions = {}; 7 | 8 | /** 9 | * Queues the passed action to the event. 10 | * @param {string} eventName The event name. 11 | * @param {string} actionIdentifier The action identifier. 12 | * @param {function} action The action function. 13 | * @param {boolean} [removeAfterRun = true] Remove the action once it's run. 14 | */ 15 | add(eventName, actionIdentifier, action, removeAfterRun = true) { 16 | if (!this.hasEvent(eventName)) { 17 | this._eventActions[eventName] = {}; 18 | } 19 | 20 | this._eventActions[eventName][actionIdentifier] = { fn: action, removeAfterRun: removeAfterRun }; 21 | } 22 | 23 | /** 24 | * Returns true if there is a event exists for the passed name. 25 | * @param {string} eventName The event name. 26 | * @return {boolean} 27 | */ 28 | hasEvent(eventName) { 29 | return this._eventActions.hasOwnProperty(eventName); 30 | } 31 | 32 | /** 33 | * Returns true if the passed action is already queued-up. 34 | * @param {string} eventName The event name. 35 | * @param {string} actionIdentifier The action identifier. 36 | * @return {boolean} 37 | */ 38 | hasAction(eventName, actionIdentifier) { 39 | if (!this.hasEvent(eventName)) { 40 | return false; 41 | } 42 | 43 | return this._eventActions[eventName].hasOwnProperty(actionIdentifier); 44 | } 45 | 46 | /** 47 | * Runs all the actions queued up for the passed event. 48 | * @param {string} eventName The event name. 49 | * @param {string} [actionIdentifier] The action identifier. 50 | */ 51 | run(eventName, actionIdentifier) { 52 | if (!this.hasEvent(eventName)) { 53 | return; 54 | } 55 | 56 | if (typeof actionIdentifier !== 'undefined') { 57 | if (!this.hasAction(eventName, actionIdentifier)) { 58 | return; 59 | } 60 | 61 | this._run(eventName, actionIdentifier); 62 | 63 | return; 64 | } 65 | 66 | Object.keys(this._eventActions[eventName]).forEach(action => this._run(eventName, action)); 67 | } 68 | 69 | /** 70 | * Removes the event or a queued action for the event. 71 | * @param {string} eventName The event name. 72 | * @param {string} [actionIdentifier] The action identifier. 73 | */ 74 | remove(eventName, actionIdentifier) { 75 | if (!this._eventActions.hasOwnProperty(eventName)) { 76 | return; 77 | } 78 | 79 | if (!actionIdentifier) { 80 | delete this._eventActions[eventName]; 81 | return; 82 | } 83 | 84 | delete this._eventActions[eventName][actionIdentifier]; 85 | } 86 | 87 | /** 88 | * Clears all the stored events and the queued-up actions. 89 | */ 90 | clear() { 91 | this._eventActions = {}; 92 | } 93 | 94 | /** 95 | * Runs a single action. 96 | * @param {string} eventName The event name. 97 | * @param {string} actionIdentifier The action identifier. 98 | * @private 99 | */ 100 | _run(eventName, actionIdentifier) { 101 | const queued = this._eventActions[eventName][actionIdentifier]; 102 | queued.fn(); 103 | queued.removeAfterRun && this.remove(eventName, actionIdentifier); 104 | } 105 | } 106 | 107 | export default Queue; 108 | -------------------------------------------------------------------------------- /src/Emitter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Singleton global event emitter. 3 | * @class 4 | */ 5 | class Emitter { 6 | 7 | /** 8 | * Dictionary that maps the objects with their events and handlers. 9 | * @type {object} 10 | * @private 11 | */ 12 | _objectsEventsHandlersMap = {}; 13 | 14 | /** 15 | * Subscribes to an event of the passed object. 16 | * @param {number} id The unique id of the object. 17 | * @param {string} eventName Name of the event 18 | * @param {function} handler The event-handler function 19 | * @param {boolean} [once = false] Is it one-time subscription or not? 20 | * @return {Emitter} 21 | */ 22 | on(id, eventName, handler, once = false) { 23 | if (!this._hasObject(id)) { 24 | this._objectsEventsHandlersMap[id] = {}; 25 | } 26 | 27 | const objEvents = this._objectsEventsHandlersMap[id]; 28 | 29 | if (!objEvents.hasOwnProperty(eventName)) { 30 | objEvents[eventName] = []; 31 | } 32 | 33 | objEvents[eventName].push({ 34 | handler: handler, 35 | once: once 36 | }); 37 | 38 | return this; 39 | } 40 | 41 | /** 42 | * Un-subscribes from an event of the passed object. 43 | * @param {number} id The unique id of the object. 44 | * @param {string} eventName The event name. 45 | * @param {function} [handler] The handler function. 46 | * @return {Emitter} 47 | */ 48 | off(id, eventName, handler) { 49 | if (!this._hasEvent(id, eventName)) { 50 | return this; 51 | } 52 | 53 | const objEvents = this._objectsEventsHandlersMap[id]; 54 | 55 | if (!handler) { 56 | objEvents[eventName] = []; 57 | } else { 58 | objEvents[eventName] = objEvents[eventName].filter(eventSubscriber => { 59 | return eventSubscriber.handler !== handler; 60 | }); 61 | } 62 | 63 | return this; 64 | } 65 | 66 | /** 67 | * Fires an event of the object passing the source and other optional arguments. 68 | * @param {number} id The unique id of the object. 69 | * @param {string} eventName The event name 70 | * @param {...*} args The arguments that to be passed to handler 71 | * @return {Emitter} 72 | */ 73 | fire(id, eventName, ...args) { 74 | if (!this._hasEvent(id, eventName)) { 75 | return this; 76 | } 77 | 78 | let eventSubscribers = this._objectsEventsHandlersMap[id][eventName]; 79 | 80 | for (let i = 0; i < eventSubscribers.length; i++) { 81 | let eventSubscriber = eventSubscribers[i]; 82 | 83 | setTimeout(function (subscriber) { 84 | const { handler, once } = subscriber; 85 | 86 | handler(...args); 87 | 88 | if (once) { 89 | this.off(id, eventName, handler); 90 | } 91 | }.bind(this, eventSubscriber), 0); 92 | } 93 | 94 | return this; 95 | } 96 | 97 | /** 98 | * Clears the event handlers of the passed object. 99 | * @param {number} [id] The unique id of the object. 100 | * @return {Emitter} 101 | */ 102 | clear(id) { 103 | if (!id) { 104 | this._objectsEventsHandlersMap = {}; 105 | return this; 106 | } 107 | 108 | if (this._hasObject(id)) { 109 | delete this._objectsEventsHandlersMap[id]; 110 | } 111 | 112 | return this; 113 | } 114 | 115 | /** 116 | * Returns true if the object is already registered. 117 | * @param {number} id The object id. 118 | * @return {boolean} 119 | * @private 120 | */ 121 | _hasObject(id) { 122 | return this._objectsEventsHandlersMap.hasOwnProperty(id); 123 | } 124 | 125 | /** 126 | * Returns true if the passed object has an entry of the passed event. 127 | * @param {number} id The object id. 128 | * @param {string} eventName The event name. 129 | * @return {boolean} 130 | * @private 131 | */ 132 | _hasEvent(id, eventName) { 133 | return this._hasObject(id) && this._objectsEventsHandlersMap[id].hasOwnProperty(eventName); 134 | } 135 | } 136 | 137 | export default new Emitter(); 138 | -------------------------------------------------------------------------------- /src/BufferLoader.js: -------------------------------------------------------------------------------- 1 | import utility from './Utility'; 2 | import DownloadResult from './DownloadResult'; 3 | 4 | /** 5 | * Loads the audio sources into audio buffers and returns them. 6 | * The loaded buffers are cached. 7 | * @class 8 | */ 9 | class BufferLoader { 10 | 11 | /** 12 | * AudioContext. 13 | * @type {AudioContext} 14 | * @private 15 | */ 16 | _context = null; 17 | 18 | /** 19 | * In-memory audio buffer cache store. 20 | * @type {object} 21 | * @private 22 | */ 23 | _bufferCache = {}; 24 | 25 | /** 26 | * Dictionary to store the current progress calls and their callbacks. 27 | * @type {object} 28 | * @private 29 | */ 30 | _progressCallsAndCallbacks = {}; 31 | 32 | /** 33 | * True if the loader is disposed. 34 | * @type {boolean} 35 | * @private 36 | */ 37 | _disposed = false; 38 | 39 | /** 40 | * Create the cache. 41 | * @param {AudioContext} context The Audio Context 42 | */ 43 | constructor(context) { 44 | this._context = context; 45 | } 46 | 47 | /** 48 | * Loads single or multiple audio resources into audio buffers. 49 | * @param {string|string[]} urls Single or array of audio urls. 50 | * @param {function} [progressCallback] The callback that is called to intimate the percentage downloaded. 51 | * @return {Promise>} 52 | */ 53 | load(urls, progressCallback) { 54 | if (typeof urls === 'string') { 55 | return this._load(urls, progressCallback); 56 | } 57 | 58 | return Promise.all(urls.map(url => this._load(url, progressCallback))); 59 | } 60 | 61 | /** 62 | * Removes the cached audio buffers. 63 | * @param {string|string[]} [urls] Single or array of audio urls 64 | */ 65 | unload(urls) { 66 | if (typeof urls === 'string') { 67 | this._unload(urls); 68 | return; 69 | } 70 | 71 | if (Array.isArray(urls)) { 72 | urls.forEach(url => this._unload(url), this); 73 | return; 74 | } 75 | 76 | this._bufferCache = {}; 77 | } 78 | 79 | /** 80 | * Dispose the loader. 81 | */ 82 | dispose() { 83 | if (this._disposed) { 84 | return; 85 | } 86 | 87 | this.unload(); 88 | this._bufferCache = null; 89 | this._progressCallsAndCallbacks = null; 90 | this._context = null; 91 | this._disposed = true; 92 | } 93 | 94 | /** 95 | * Loads a single audio resource into audio buffer and cache result if the download is succeeded. 96 | * @param {string} url The Audio url. 97 | * @param {function} [progressCallback] The callback that is called to intimate the percentage downloaded. 98 | * @return {Promise} 99 | * @private 100 | */ 101 | _load(url, progressCallback) { 102 | return new Promise(resolve => { 103 | if (this._bufferCache.hasOwnProperty(url)) { 104 | resolve(new DownloadResult(url, this._bufferCache[url])); 105 | return; 106 | } 107 | 108 | if (this._progressCallsAndCallbacks.hasOwnProperty(url)) { 109 | this._progressCallsAndCallbacks[url].push(resolve); 110 | return; 111 | } 112 | 113 | this._progressCallsAndCallbacks[url] = []; 114 | this._progressCallsAndCallbacks[url].push(resolve); 115 | 116 | const reject = err => { 117 | if (this._disposed) { 118 | return; 119 | } 120 | 121 | this._progressCallsAndCallbacks[url].forEach(r => r(new DownloadResult(url, null, err))); 122 | delete this._progressCallsAndCallbacks[url]; 123 | }; 124 | 125 | const decodeAudioData = arrayBuffer => { 126 | if (this._disposed) { 127 | return; 128 | } 129 | 130 | this._context.decodeAudioData(arrayBuffer, buffer => { 131 | this._bufferCache[url] = buffer; 132 | this._progressCallsAndCallbacks[url].forEach(r => r(new DownloadResult(url, buffer))); 133 | delete this._progressCallsAndCallbacks[url]; 134 | }, reject); 135 | }; 136 | 137 | if (utility.isBase64(url)) { 138 | const data = atob(url.split(',')[1]); 139 | const dataView = new Uint8Array(data.length); // eslint-disable-line no-undef 140 | 141 | for (let i = 0; i < data.length; i++) { 142 | dataView[i] = data.charCodeAt(i); 143 | } 144 | 145 | decodeAudioData(dataView.buffer); 146 | return; 147 | } 148 | 149 | const req = new XMLHttpRequest(); 150 | req.open('GET', url, true); 151 | req.responseType = 'arraybuffer'; 152 | 153 | req.addEventListener('load', () => decodeAudioData(req.response), false); 154 | 155 | if (progressCallback) { 156 | req.addEventListener('progress', (evt) => { 157 | if (!evt.lengthComputable) { 158 | progressCallback({ url: url, percentageDownloaded: 0 }); 159 | } 160 | 161 | const percentageDownloaded = Math.round((evt.loaded / evt.total) * 100); 162 | progressCallback({ url: url, percentageDownloaded: percentageDownloaded }); 163 | }); 164 | } 165 | 166 | req.addEventListener('error', reject, false); 167 | req.send(); 168 | }); 169 | } 170 | 171 | /** 172 | * Removes the single cached audio buffer. 173 | * @param {string} url Audio url 174 | * @private 175 | */ 176 | _unload(url) { 177 | delete this._bufferCache[url]; 178 | } 179 | } 180 | 181 | export default BufferLoader; 182 | -------------------------------------------------------------------------------- /src/Utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helper methods. 3 | */ 4 | class Utility { 5 | 6 | /** 7 | * The navigator object. 8 | * @type {Navigator} 9 | * @private 10 | */ 11 | _navigator = null; 12 | 13 | /** 14 | * The AudioContext type. 15 | * @type {Function} 16 | * @private 17 | */ 18 | _contextType = null; 19 | 20 | /** 21 | * Dictionary of audio formats and their support status. 22 | * @type {object} 23 | * @private 24 | */ 25 | _formats = {}; 26 | 27 | /** 28 | * User agent. 29 | * @private 30 | */ 31 | _userAgent = null; 32 | 33 | /** 34 | * @constructor 35 | */ 36 | constructor() { 37 | if (typeof navigator !== 'undefined') { 38 | this._navigator = navigator; 39 | this._userAgent = navigator.userAgent; 40 | } 41 | 42 | // Set the available Web Audio Context type available in browser. 43 | if (typeof AudioContext !== 'undefined') { 44 | this._contextType = AudioContext; 45 | } else if (typeof webkitAudioContext !== 'undefined') { 46 | this._contextType = webkitAudioContext; 47 | } 48 | 49 | // Determine the supported audio formats. 50 | let audio = new Audio(); 51 | 52 | this._formats = { 53 | mp3: Boolean(audio.canPlayType('audio/mp3;').replace(/^no$/, '')), 54 | mpeg: Boolean(audio.canPlayType('audio/mpeg;').replace(/^no$/, '')), 55 | opus: Boolean(audio.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/, '')), 56 | ogg: Boolean(audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '')), 57 | oga: Boolean(audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '')), 58 | wav: Boolean(audio.canPlayType('audio/wav; codecs="1"').replace(/^no$/, '')), 59 | aac: Boolean(audio.canPlayType('audio/aac;').replace(/^no$/, '')), 60 | caf: Boolean(audio.canPlayType('audio/x-caf;').replace(/^no$/, '')), 61 | m4a: Boolean((audio.canPlayType('audio/x-m4a;') || 62 | audio.canPlayType('audio/m4a;') || 63 | audio.canPlayType('audio/aac;')).replace(/^no$/, '')), 64 | mp4: Boolean((audio.canPlayType('audio/x-mp4;') || 65 | audio.canPlayType('audio/mp4;') || 66 | audio.canPlayType('audio/aac;')).replace(/^no$/, '')), 67 | weba: Boolean(audio.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), 68 | webm: Boolean(audio.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/, '')), 69 | dolby: Boolean(audio.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/, '')), 70 | flac: Boolean((audio.canPlayType('audio/x-flac;') || audio.canPlayType('audio/flac;')).replace(/^no$/, '')) 71 | }; 72 | 73 | audio = null; 74 | } 75 | 76 | /** 77 | * Returns an unique id (credit: https://howlerjs.com). 78 | * @return {number} 79 | */ 80 | id() { 81 | return Math.round(Date.now() * Math.random()); 82 | } 83 | 84 | /** 85 | * Returns the available context type. 86 | * @return {Function} 87 | */ 88 | getContextType() { 89 | return this._contextType; 90 | } 91 | 92 | /** 93 | * Instantiates and returns the audio context. 94 | * @return {AudioContext|webkitAudioContext} 95 | */ 96 | getContext() { 97 | return new this._contextType(); 98 | } 99 | 100 | /** 101 | * Returns the supported audio formats. 102 | * @return {Object} 103 | */ 104 | supportedFormats() { 105 | return this._formats; 106 | } 107 | 108 | /** 109 | * Returns true if the passed format is supported. 110 | * @param {string} format The audio format ex. "mp3" 111 | * @return {boolean} 112 | */ 113 | isFormatSupported(format) { 114 | return Boolean(this._formats[format]); 115 | } 116 | 117 | /** 118 | * Returns the first supported format from the passed array. 119 | * @param {string[]} formats Array of audio formats 120 | * @return {string} 121 | */ 122 | getSupportedFormat(formats) { 123 | return formats.find(format => this.isFormatSupported(format)); 124 | } 125 | 126 | /** 127 | * Returns true if the audio source is supported. 128 | * @param {string} source The audio source url or base64 string 129 | * @return {boolean} 130 | */ 131 | isSourceSupported(source) { 132 | let ext = this.isBase64(source) ? 133 | (/^data:audio\/([^;,]+);/i).exec(source) : 134 | (/^.+\.([^.]+)$/).exec(source); 135 | 136 | ext = (/^.+\.([^.]+)$/).exec(source); 137 | return ext ? this.isFormatSupported(ext[1].toLowerCase()) : false; 138 | } 139 | 140 | /** 141 | * Returns the first supported audio source from the passed array. 142 | * @param {string[]} sources Array of audio sources. The audio source could be either url or base64 string. 143 | * @return {string} 144 | */ 145 | getSupportedSource(sources) { 146 | return sources.find(source => this.isSourceSupported(source)); 147 | } 148 | 149 | /** 150 | * Returns whether the passed string is a base64 string or not. 151 | * @param {string} str Base64 audio string 152 | * @return {boolean} 153 | */ 154 | isBase64(str) { 155 | return (/^data:[^;]+;base64,/).test(str); 156 | } 157 | 158 | /** 159 | * Returns true if the platform is mobile. 160 | * @return {boolean} 161 | * @private 162 | */ 163 | _isMobile() { 164 | if (!this._navigator) { 165 | return false; 166 | } 167 | 168 | return (/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i).test(this._userAgent); 169 | } 170 | 171 | /** 172 | * Returns true if the platform is touch supported. 173 | * @return {boolean} 174 | * @private 175 | */ 176 | _isTouch() { 177 | return typeof window !== 'undefined' && (Boolean(('ontouchend' in window) || 178 | (this._navigator && this._navigator.maxTouchPoints > 0) || 179 | (this._navigator && this._navigator.msMaxTouchPoints > 0))); 180 | } 181 | 182 | /** 183 | * Returns true if the user agent is IE. 184 | * @return {boolean} 185 | */ 186 | isIE() { 187 | return Boolean(this._userAgent && (/MSIE |Trident\//).test(this._userAgent)); 188 | } 189 | 190 | /** 191 | * Destroys the passed audio node. 192 | * @param {Audio} audio The HTML5 audio element. 193 | */ 194 | destroyHtml5Audio(audio) { 195 | audio.pause(); 196 | this.isIE() && (audio.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'); 197 | audio.onerror = null; 198 | audio.onend = null; 199 | audio.canplaythrough = null; 200 | } 201 | } 202 | 203 | export default new Utility(); 204 | 205 | -------------------------------------------------------------------------------- /src/WorkerTimer.js: -------------------------------------------------------------------------------- 1 | // Credit: https://github.com/goldfire/howler.js/issues/626 2 | 3 | const WORKER_SCRIPT = ` 4 | var timerIds = {}, timeoutWorker = {}; 5 | 6 | timeoutWorker.setTimeout = function(timerId, duration) { 7 | timerIds[timerId] = setTimeout(function() { 8 | postMessage({ timerId: timerId }); 9 | }, duration); 10 | }; 11 | 12 | timeoutWorker.clearTimeout = function(timerId) { 13 | clearTimeout(timerIds[timerId]); 14 | }; 15 | 16 | timeoutWorker.setInterval = function(timerId, duration) { 17 | timerIds[timerId] = setInterval(function() { 18 | postMessage({ timerId: timerId }); 19 | }, duration); 20 | }; 21 | 22 | timeoutWorker.clearInterval = function(timerId) { 23 | clearInterval(timerIds[timerId]); 24 | }; 25 | 26 | onmessage = function(e) { 27 | var command = e.data.command; 28 | timeoutWorker[command](e.data.timerId, e.data.duration); 29 | }; 30 | `; 31 | 32 | /** 33 | * Provides more accurate timeouts and intervals when the browser tab is not active using Web Workers. 34 | * @class 35 | */ 36 | class WorkerTimer { 37 | 38 | /** 39 | * Web worker. 40 | * @type {Worker} 41 | * @private 42 | */ 43 | _worker = null; 44 | 45 | /** 46 | * Whether Web Worker is available or not. If not available then normal setTimeout and setInterval will be used. 47 | * @type {boolean} 48 | * @private 49 | */ 50 | _isWorkerThreadAvailable = false; 51 | 52 | /** 53 | * Dictionary to store the callbacks that should be invoked after timeouts and intervals. 54 | * @type {{}} 55 | * @private 56 | */ 57 | _timerCallbacks = {}; 58 | 59 | /** 60 | * The incrementing id that is used to link the timer running in worker with the callback. 61 | * @type {number} 62 | * @private 63 | */ 64 | _timerId = 0; 65 | 66 | /** 67 | * @constructor 68 | */ 69 | constructor() { 70 | this._handleMessage = this._handleMessage.bind(this); 71 | } 72 | 73 | /** 74 | * Initialize the worker 75 | */ 76 | init() { 77 | if (!Worker || this._worker) { 78 | return; 79 | } 80 | 81 | let blob = this._getBlob(WORKER_SCRIPT); 82 | if (blob === null) { 83 | return; 84 | } 85 | 86 | let workerUrl = this._createObjectURL(blob); 87 | if (workerUrl === null) { 88 | return; 89 | } 90 | 91 | this._worker = new Worker(workerUrl); 92 | this._worker.addEventListener('message', this._handleMessage); 93 | this._isWorkerThreadAvailable = true; 94 | } 95 | 96 | /** 97 | * Returns a blob. 98 | * @param {string} script The javascript code string. 99 | * @return {*} 100 | * @private 101 | */ 102 | _getBlob(script) { 103 | let blob = null; 104 | 105 | try { 106 | blob = new Blob([script], { type: 'application/javascript' }); 107 | } catch (e) { 108 | let blobBuilderType = null; 109 | 110 | if (typeof BlobBuilder !== 'undefined') { 111 | blobBuilderType = BlobBuilder; 112 | } else if (typeof WebKitBlobBuilder !== 'undefined') { 113 | blobBuilderType = WebKitBlobBuilder; 114 | } 115 | 116 | blob = new blobBuilderType(); // eslint-disable-line new-cap 117 | blob.append(script); 118 | blob = blob.getBlob(); 119 | } 120 | 121 | return blob; 122 | } 123 | 124 | /** 125 | * Returns object url. 126 | * @param {*} file The blob. 127 | * @return {*} 128 | * @private 129 | */ 130 | _createObjectURL(file) { 131 | if (typeof URL !== 'undefined' && URL.createObjectURL) { 132 | return URL.createObjectURL(file); 133 | } else if (typeof webkitURL !== 'undefined') { 134 | return webkitURL.createObjectURL(file); 135 | } 136 | 137 | return null; 138 | } 139 | 140 | /** 141 | * Callback that handles the messages send by worker. 142 | * @param {object} e Event argument that contains the message data and other information 143 | * @private 144 | */ 145 | _handleMessage(e) { 146 | const callback = this._timerCallbacks[e.data.timerId]; 147 | 148 | if (callback && callback.cb) { 149 | callback.cb(); 150 | } 151 | 152 | if (!callback.repeat) { 153 | delete this._timerCallbacks[e.data.timerId]; 154 | } 155 | } 156 | 157 | /** 158 | * Invokes a callback after the passed time. 159 | * @param {function} callback The callback that should be called after the elapsed period. 160 | * @param {number} duration The time period in ms. 161 | * @return {number} 162 | */ 163 | setTimeout(callback, duration) { 164 | if (!this._isWorkerThreadAvailable) { 165 | return setTimeout(callback, duration); 166 | } 167 | 168 | this._timerId = this._timerId + 1; 169 | this._timerCallbacks[this._timerId] = { cb: callback, repeat: false }; 170 | this._worker.postMessage({ command: 'setTimeout', timerId: this._timerId, duration: duration }); 171 | return this._timerId; 172 | } 173 | 174 | /** 175 | * Clears the scheduled timeout. 176 | * @param {number} timeoutId The timeout id. 177 | */ 178 | clearTimeout(timeoutId) { 179 | if (!this._isWorkerThreadAvailable) { 180 | return clearTimeout(timeoutId); 181 | } 182 | 183 | this._worker.postMessage({ command: 'clearTimeout', timerId: timeoutId }); 184 | delete this._timerCallbacks[timeoutId]; 185 | } 186 | 187 | /** 188 | * Invokes the callback function at the passed interval. 189 | * @param {function} callback The callback function. 190 | * @param {number} duration The time interval. 191 | * @return {number} 192 | */ 193 | setInterval(callback, duration) { 194 | if (!this._isWorkerThreadAvailable) { 195 | return setInterval(callback, duration); 196 | } 197 | 198 | this._timerId = this._timerId + 1; 199 | this._timerCallbacks[this._timerId] = { cb: callback, repeat: true }; 200 | this._worker.postMessage({ command: 'setInterval', timerId: this._timerId, duration: duration }); 201 | return this._timerId; 202 | } 203 | 204 | /** 205 | * Clears the scheduled interval. 206 | * @param {number} intervalId The interval id. 207 | */ 208 | clearInterval(intervalId) { 209 | if (!this._isWorkerThreadAvailable) { 210 | return clearTimeout(intervalId); 211 | } 212 | 213 | this._worker.postMessage({ command: 'clearTimeout', timerId: intervalId }); 214 | delete this._timerCallbacks[intervalId]; 215 | } 216 | 217 | /** 218 | * Kills the worker thread. 219 | */ 220 | terminate() { 221 | if (this._worker) { 222 | this._worker.removeEventListener('message', this._handleMessage); 223 | this._worker.terminate(); 224 | this._worker = null; 225 | } 226 | 227 | this._isWorkerThreadAvailable = false; 228 | this._timerCallbacks = {}; 229 | this._timerId = 0; 230 | } 231 | } 232 | 233 | export default new WorkerTimer(); 234 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "classes": true 8 | } 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "jasmine": true 14 | }, 15 | "globals": { 16 | "Promise": true, 17 | "webkitAudioContext": true, 18 | "BlobBuilder": true, 19 | "WebKitBlobBuilder": true, 20 | "webkitURL": true 21 | }, 22 | "rules": { 23 | "no-extra-parens": "off", 24 | "no-unexpected-multiline": "off", 25 | "valid-jsdoc": [ "warn", { 26 | "requireReturn": false, 27 | "requireReturnDescription": false, 28 | "requireParamDescription": true 29 | }], 30 | "accessor-pairs": [ "error", { 31 | "getWithoutSet": false, 32 | "setWithoutGet": true 33 | }], 34 | "block-scoped-var": "warn", 35 | "consistent-return": "warn", 36 | "curly": "warn", 37 | "default-case": "warn", 38 | "dot-location": [ "warn", "property" ], 39 | "dot-notation": "warn", 40 | "eqeqeq": [ "error", "smart" ], 41 | "guard-for-in": "warn", 42 | "no-alert": "error", 43 | "no-caller": "error", 44 | "no-case-declarations": "warn", 45 | "no-div-regex": "warn", 46 | "no-else-return": "warn", 47 | "no-empty-pattern": "warn", 48 | "no-eq-null": "warn", 49 | "no-eval": "error", 50 | "no-extend-native": "error", 51 | "no-extra-bind": "warn", 52 | "no-floating-decimal": "warn", 53 | "no-implicit-coercion": [ "warn", { 54 | "boolean": true, 55 | "number": true, 56 | "string": true 57 | }], 58 | "no-implied-eval": "error", 59 | "no-invalid-this": "error", 60 | "no-iterator": "error", 61 | "no-labels": "warn", 62 | "no-lone-blocks": "warn", 63 | "no-loop-func": "error", 64 | "no-magic-numbers": "off", 65 | "no-multi-spaces": "warn", 66 | "no-multi-str": "warn", 67 | "no-native-reassign": "error", 68 | "no-new-func": "error", 69 | "no-new-wrappers": "error", 70 | "no-new": "error", 71 | "no-octal-escape": "error", 72 | "no-param-reassign": "error", 73 | "no-process-env": "warn", 74 | "no-proto": "error", 75 | "no-redeclare": "error", 76 | "no-return-assign": "error", 77 | "no-script-url": "error", 78 | "no-self-compare": "error", 79 | "no-throw-literal": "error", 80 | "no-unused-expressions": "off", 81 | "no-useless-call": "error", 82 | "no-useless-concat": "error", 83 | "no-void": "warn", 84 | "no-warning-comments": [ "warn", { 85 | "terms": [ "TODO" ], 86 | "location": "start" 87 | }], 88 | "no-with": "warn", 89 | "radix": "warn", 90 | "vars-on-top": "error", 91 | "wrap-iife": [ "error", "outside" ], 92 | "yoda": "error", 93 | "strict": [ "error", "never" ], 94 | "init-declarations": [ "error", "always" ], 95 | "no-catch-shadow": "warn", 96 | "no-delete-var": "error", 97 | "no-label-var": "error", 98 | "no-shadow-restricted-names": "error", 99 | "no-shadow": "warn", 100 | "no-undef-init": "off", 101 | "no-undef": "error", 102 | "no-undefined": "off", 103 | "no-unused-vars": "warn", 104 | "no-use-before-define": "error", 105 | "callback-return": [ "warn", [ "callback", "next" ]], 106 | "global-require": "error", 107 | "handle-callback-err": "warn", 108 | "no-mixed-requires": "warn", 109 | "no-new-require": "error", 110 | "no-path-concat": "error", 111 | "no-process-exit": "error", 112 | "no-restricted-modules": "off", 113 | "arrow-body-style": "off", 114 | "arrow-parens": "off", 115 | "arrow-spacing": [ "error", { "before": true, "after": true }], 116 | "constructor-super": "error", 117 | "generator-star-spacing": [ "error", "before" ], 118 | "no-confusing-arrow": "error", 119 | "no-constant-condition": "error", 120 | "no-class-assign": "error", 121 | "no-const-assign": "error", 122 | "no-dupe-class-members": "error", 123 | "no-this-before-super": "error", 124 | "no-var": "warn", 125 | "object-shorthand": [ "warn", "never" ], 126 | "prefer-arrow-callback": "warn", 127 | "prefer-spread": "warn", 128 | "prefer-template": "warn", 129 | "require-yield": "error", 130 | "array-bracket-spacing": "off", 131 | "block-spacing": [ "warn", "always" ], 132 | "brace-style": [ "warn", "1tbs", { "allowSingleLine": false } ], 133 | "camelcase": "warn", 134 | "comma-spacing": [ "warn", { "before": false, "after": true } ], 135 | "comma-style": [ "warn", "last" ], 136 | "computed-property-spacing": [ "warn", "never" ], 137 | "consistent-this": [ "warn", "self" ], 138 | "eol-last": "warn", 139 | "func-names": "off", 140 | "func-style": ["warn", "declaration", { "allowArrowFunctions": true }], 141 | "id-length": [ "warn", { "min": 1, "max": 32 } ], 142 | "indent": [ "warn", 2 ], 143 | "jsx-quotes": [ "warn", "prefer-double" ], 144 | "linebreak-style": [ "warn", "unix" ], 145 | "lines-around-comment": [ "warn", { "beforeBlockComment": true } ], 146 | "max-depth": [ "warn", 8 ], 147 | "max-len": [ "warn", 132 ], 148 | "max-nested-callbacks": [ "warn", 8 ], 149 | "max-params": [ "warn", 8 ], 150 | "new-cap": "warn", 151 | "new-parens": "warn", 152 | "no-array-constructor": "warn", 153 | "no-bitwise": "off", 154 | "no-continue": "off", 155 | "no-inline-comments": "off", 156 | "no-lonely-if": "warn", 157 | "no-mixed-spaces-and-tabs": "warn", 158 | "no-multiple-empty-lines": "warn", 159 | "no-negated-condition": "off", 160 | "no-nested-ternary": "off", 161 | "no-new-object": "warn", 162 | "no-plusplus": "off", 163 | "no-spaced-func": "warn", 164 | "no-ternary": "off", 165 | "no-trailing-spaces": "off", 166 | "no-underscore-dangle": "off", 167 | "no-unneeded-ternary": "warn", 168 | "object-curly-spacing": [ "warn", "always" ], 169 | "one-var": "off", 170 | "operator-assignment": [ "warn", "never" ], 171 | "operator-linebreak": [ "warn", "after" ], 172 | "padded-blocks": "off", 173 | "quote-props": [ "warn", "consistent-as-needed" ], 174 | "quotes": [ "warn", "single" ], 175 | "require-jsdoc": [ "warn", { 176 | "require": { 177 | "FunctionDeclaration": true, 178 | "MethodDefinition": true, 179 | "ClassDeclaration": false 180 | } 181 | }], 182 | "semi-spacing": [ "warn", { "before": false, "after": true }], 183 | "semi": [ "error", "always" ], 184 | "sort-vars": "off", 185 | "space-before-blocks": [ "warn", "always" ], 186 | "space-before-function-paren": "off", 187 | "keyword-spacing": "warn", 188 | "space-in-parens": [ "warn", "never" ], 189 | "space-infix-ops": [ "warn", { "int32Hint": true } ], 190 | "space-unary-ops": "warn", 191 | "spaced-comment": [ "warn", "always" ], 192 | "wrap-regex": "warn" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/MediaLoader.js: -------------------------------------------------------------------------------- 1 | import Html5AudioPool from './Html5AudioPool'; 2 | import DownloadResult from './DownloadResult'; 3 | 4 | /** 5 | * Loads the HTML5 audio nodes and returns them. 6 | * @class 7 | */ 8 | class MediaLoader { 9 | 10 | /** 11 | * HTML5 audio pool. 12 | * @type {Html5AudioPool} 13 | * @private 14 | */ 15 | _audioPool = null; 16 | 17 | /** 18 | * Store the array of audio elements that are currently in buffering state. 19 | * @type {Array} 20 | * @private 21 | */ 22 | _bufferingAudios = []; 23 | 24 | /** 25 | * True if the loader is disposed. 26 | * @type {boolean} 27 | * @private 28 | */ 29 | _disposed = false; 30 | 31 | /** 32 | * Creates the audio pool. 33 | * @param {number} maxNodesPerSource Maximum number of audio nodes allowed for a url. 34 | * @param {number} inactiveTime The period after which HTML5 audio node is marked as inactive. 35 | * @param {function} soundCleanUpCallback The inactive sounds cleanup callback. 36 | */ 37 | constructor(maxNodesPerSource, inactiveTime, soundCleanUpCallback) { 38 | this._audioPool = new Html5AudioPool(maxNodesPerSource, inactiveTime, soundCleanUpCallback); 39 | } 40 | 41 | /** 42 | * Preloads the HTML5 audio nodes with audio and return them. 43 | * @param {string|string[]} urls Single or array of audio file urls. 44 | * @return {Promise>} 45 | */ 46 | load(urls) { 47 | if (typeof urls === 'string') { 48 | return this._load(urls); 49 | } 50 | 51 | return Promise.all(urls.map(url => this._load(url))); 52 | } 53 | 54 | /** 55 | * Allocates audio node for a group. 56 | * @param {string} url The audio file url. 57 | * @param {number} groupId The group id. 58 | * @return {Promise} 59 | */ 60 | allocateForGroup(url, groupId) { 61 | return this._load(url, groupId); 62 | } 63 | 64 | /** 65 | * Allocates an audio node for sound and returns it. 66 | * @param {string} src The audio file url. 67 | * @param {number} groupId The buzz id. 68 | * @param {number} soundId The sound id. 69 | * @return {Audio} 70 | */ 71 | allocateForSound(src, groupId, soundId) { 72 | return this._audioPool.allocateForSound(src, groupId, soundId); 73 | } 74 | 75 | /** 76 | * Releases the allocated audio node(s) for the passed urls. 77 | * @param {string|string[]} [urls] Single or array of audio file urls. 78 | */ 79 | unload(urls) { 80 | const removeAudioObjOfUrl = url => { 81 | const audioObj = this._bufferingAudios.find(a => a.url === url); 82 | audioObj && this._cleanUp(audioObj); 83 | }; 84 | 85 | if (!urls) { 86 | this._bufferingAudios.forEach(audioObj => this._cleanUp(audioObj)); 87 | this._audioPool.release(); 88 | } else if (typeof urls === 'string') { 89 | removeAudioObjOfUrl(urls); 90 | this._audioPool.releaseForSource(urls); 91 | } else if (Array.isArray(urls) && urls.length) { 92 | urls.forEach(url => { 93 | removeAudioObjOfUrl(url); 94 | this._audioPool.releaseForSource(url); 95 | }); 96 | } 97 | } 98 | 99 | /** 100 | * Releases the allocated audio node for the passed group. 101 | * @param {string} url The audio file url. 102 | * @param {number} groupId The group id. 103 | */ 104 | releaseForGroup(url, groupId) { 105 | this._bufferingAudios 106 | .filter(a => a.groupId === groupId) 107 | .forEach(a => this._cleanUp(a)); 108 | 109 | this._audioPool.releaseForGroup(url, groupId); 110 | } 111 | 112 | /** 113 | * Destroys the audio node reserved for sound. 114 | * @param {string} src The audio file url. 115 | * @param {number} groupId The buzz id. 116 | * @param {number} soundId The sound id. 117 | */ 118 | releaseForSound(src, groupId, soundId) { 119 | this._audioPool.releaseAudio(src, groupId, soundId); 120 | } 121 | 122 | /** 123 | * Returns if there are free audio nodes available for a group. 124 | * @param {string} src The audio file url. 125 | * @param {number} groupId The group id. 126 | * @return {boolean} 127 | */ 128 | hasFreeNodes(src, groupId) { 129 | return this._audioPool.hasFreeNodes(src, groupId); 130 | } 131 | 132 | /** 133 | * Acquires the unallocated audio nodes and removes the excess ones. 134 | */ 135 | cleanUp() { 136 | this._audioPool.cleanUp(); 137 | } 138 | 139 | /** 140 | * Clear the event handlers of buffering audio elements and dispose the pool. 141 | */ 142 | dispose() { 143 | if (this._disposed) { 144 | return; 145 | } 146 | 147 | [...this._bufferingAudios].forEach(audioObj => this._cleanUp(audioObj)); 148 | this._bufferingAudios = null; 149 | this._audioPool.dispose(); 150 | this._audioPool = null; 151 | this._disposed = true; 152 | } 153 | 154 | /** 155 | * Preload the HTML5 audio element with the passed audio file and allocate it to the passed sound (if any). 156 | * @param {string} url The audio file url. 157 | * @param {number} [groupId] The buzz id. 158 | * @return {Promise} 159 | * @private 160 | */ 161 | _load(url, groupId) { 162 | return new Promise(resolve => { 163 | const audio = groupId ? this._audioPool.allocateForGroup(url, groupId) : this._audioPool.allocateForSource(url); 164 | 165 | const onCanPlayThrough = () => { 166 | if (this._disposed) { 167 | return; 168 | } 169 | 170 | const audioObj = this._bufferingAudios.find(obj => obj.audio === audio); 171 | audioObj && this._cleanUp(audioObj); 172 | resolve(new DownloadResult(url, audio)); 173 | }; 174 | 175 | const onError = (err) => { 176 | if (this._disposed) { 177 | return; 178 | } 179 | 180 | const audioObj = this._bufferingAudios.find(obj => obj.audio === audio); 181 | audioObj && this._cleanUp(audioObj); 182 | this._audioPool.releaseAudio(url, groupId, audio); 183 | resolve(new DownloadResult(url, null, err)); 184 | }; 185 | 186 | audio.addEventListener('canplaythrough', onCanPlayThrough); 187 | audio.addEventListener('error', onError); 188 | 189 | this._bufferingAudios.push({ 190 | url: url, 191 | groupId: groupId, 192 | audio: audio, 193 | canplaythrough: onCanPlayThrough, 194 | error: onError 195 | }); 196 | 197 | if (!audio.src) { // new audio element? 198 | audio.src = url; 199 | audio.load(); 200 | return; 201 | } 202 | 203 | audio.currentTime = 0; 204 | 205 | if (audio.readyState >= 3) { 206 | onCanPlayThrough(); 207 | } 208 | }); 209 | } 210 | 211 | /** 212 | * Removes the event-handlers from the audio element. 213 | * @param {object} audioObj The buffering audio object. 214 | * @private 215 | */ 216 | _cleanUp(audioObj) { 217 | ['canplaythrough', 'error'].forEach(evt => audioObj.audio.removeEventListener(evt, audioObj[audioObj])); 218 | this._bufferingAudios.splice(this._bufferingAudios.indexOf(audioObj), 1); 219 | } 220 | } 221 | 222 | export default MediaLoader; 223 | -------------------------------------------------------------------------------- /src/BufferLoader.spec.js: -------------------------------------------------------------------------------- 1 | import utility from './Utility'; 2 | import BufferLoader from './BufferLoader'; 3 | import DownloadStatus from './DownloadStatus'; 4 | 5 | describe('BufferLoader', () => { 6 | 7 | let context = null, bufferLoader = null; 8 | 9 | beforeAll(() => { 10 | context = utility.getContext(); 11 | }); 12 | 13 | afterAll(() => { 14 | if (context) { 15 | context.close(); 16 | context = null; 17 | } 18 | }); 19 | 20 | beforeEach(() => { 21 | bufferLoader = new BufferLoader(context); 22 | }); 23 | 24 | afterEach(() => { 25 | if (bufferLoader) { 26 | bufferLoader.dispose(); 27 | } 28 | }); 29 | 30 | describe('on constructed', () => { 31 | 32 | it('should have cache created', () => { 33 | expect(bufferLoader._bufferCache).toBeDefined(); 34 | }); 35 | }); 36 | 37 | describe('when loading a single sound', () => { 38 | 39 | describe('from a valid source', () => { 40 | 41 | const url = 'base/sounds/beep.mp3'; 42 | let promise = null; 43 | 44 | beforeEach(() => { 45 | promise = bufferLoader.load(url); 46 | }); 47 | 48 | it('record the progressing call info', () => { 49 | expect(bufferLoader._progressCallsAndCallbacks[url]).toBeDefined(); 50 | }); 51 | 52 | it('should return an object with url, buffer, status and empty error', done => { 53 | promise.then(downloadResult => { 54 | expect(downloadResult.status).toBe(DownloadStatus.Success); 55 | expect(downloadResult.url).toBe(url); 56 | expect(downloadResult.value).toBeDefined(); 57 | expect(downloadResult.error).toBeNull(); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should have the buffer cached', done => { 63 | promise.then(downloadResult => { 64 | expect(bufferLoader._bufferCache.hasOwnProperty(url)).toBe(true); 65 | expect(bufferLoader._bufferCache[url]).toBe(downloadResult.value); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('should removed the recorded info after the call is over', done => { 71 | promise.then(() => { 72 | expect(bufferLoader._progressCallsAndCallbacks[url]).not.toBeDefined(); 73 | done(); 74 | }); 75 | }); 76 | }); 77 | 78 | describe('from an invalid source', () => { 79 | 80 | const url = 'base/sounds/notexist.mp3'; 81 | let promise = null; 82 | 83 | beforeEach(() => { 84 | promise = bufferLoader.load(url); 85 | }); 86 | 87 | it('should return error', () => { 88 | promise.then(downloadResult => { 89 | expect(downloadResult.status).toBe(DownloadStatus.Failure); 90 | expect(downloadResult.error).toBeDefined(); 91 | }); 92 | }); 93 | 94 | it('should not be cached', () => { 95 | promise.then(() => { 96 | expect(Object.keys(bufferLoader._bufferCache)).toBe(0); 97 | }); 98 | }); 99 | 100 | it('should removed the recorded info after the call is over', done => { 101 | promise.then(() => { 102 | expect(bufferLoader._progressCallsAndCallbacks[url]).not.toBeDefined(); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('when loading a valid single sound multiple times', () => { 110 | 111 | let promise1 = null; 112 | let promise2 = null; 113 | 114 | beforeEach(() => { 115 | spyOn(XMLHttpRequest.prototype, 'send').and.callThrough(); 116 | 117 | promise1 = bufferLoader.load('base/sounds/beep.mp3'); 118 | promise2 = bufferLoader.load('base/sounds/beep.mp3'); 119 | }); 120 | 121 | it('should trigger the ajax call only once', () => { 122 | expect(XMLHttpRequest.prototype.send.calls.count()).toBe(1); 123 | }); 124 | 125 | it('should resolve all the promises', done => { 126 | Promise.all([promise1, promise2]).then(done); 127 | }); 128 | }); 129 | 130 | describe('when loading an in-valid single sound multiple times', () => { 131 | 132 | let promise1 = null; 133 | let promise2 = null; 134 | 135 | beforeEach(() => { 136 | spyOn(XMLHttpRequest.prototype, 'send').and.callThrough(); 137 | 138 | promise1 = bufferLoader.load('base/sounds/notexist.mp3'); 139 | promise2 = bufferLoader.load('base/sounds/notexist.mp3'); 140 | }); 141 | 142 | it('should trigger the ajax call only once', () => { 143 | expect(XMLHttpRequest.prototype.send.calls.count()).toBe(1); 144 | }); 145 | 146 | it('should reject all the promises', done => { 147 | Promise.all([promise1, promise2]).then(done); 148 | }); 149 | }); 150 | 151 | describe('when downloading multiple sounds', () => { 152 | 153 | describe('with valid and invalid sources', () => { 154 | 155 | const urls = [ 'base/sounds/beep.mp3', 'base/sounds/notexist.mp3' ]; 156 | let downloadResults = []; 157 | 158 | beforeEach((done) => { 159 | bufferLoader.load(urls, context) 160 | .then(result => { 161 | downloadResults = result; 162 | done(); 163 | }); 164 | }); 165 | 166 | it('should return all the results with correct values', () => { 167 | expect(downloadResults).toBeDefined(); 168 | expect(downloadResults.length).toBe(2); 169 | 170 | const successResults = downloadResults.filter(downloadResult => downloadResult.status === DownloadStatus.Success); 171 | expect(successResults.length).toBe(1); 172 | expect(successResults[0].url).toBe(urls[0]); 173 | 174 | const failedResults = downloadResults.filter(downloadResult => downloadResult.status === DownloadStatus.Failure); 175 | expect(failedResults.length).toBe(1); 176 | expect(failedResults[0].url).toBe(urls[1]); 177 | }); 178 | }); 179 | 180 | describe('with duplicate valid source', () => { 181 | 182 | const urls = [ 'base/sounds/beep.mp3', 'base/sounds/beep.mp3' ]; 183 | let downloadResults = []; 184 | 185 | beforeEach((done) => { 186 | spyOn(XMLHttpRequest.prototype, 'send').and.callThrough(); 187 | 188 | bufferLoader.load(urls, context) 189 | .then(result => { 190 | downloadResults = result; 191 | done(); 192 | }); 193 | }); 194 | 195 | it('should trigger the ajax call only once', () => { 196 | expect(XMLHttpRequest.prototype.send.calls.count()).toBe(1); 197 | }); 198 | 199 | it('should return the result duplicated in array', () => { 200 | expect(downloadResults.length).toBe(2); 201 | expect(downloadResults[0]).toEqual(downloadResults[1]); 202 | }); 203 | }); 204 | }); 205 | 206 | /*describe('when calling unload passing single url', () => { 207 | 208 | beforeEach(() => { 209 | spyOn(bufferLoader._bufferCache, 'removeBuffer').and.callThrough(); 210 | bufferLoader.unload('base/sounds/beep.mp3'); 211 | }); 212 | 213 | it('should remove the cached url from buffer-cache', () => { 214 | expect(bufferLoader._bufferCache.removeBuffer).toHaveBeenCalledWith('base/sounds/beep.mp3'); 215 | }); 216 | }); 217 | 218 | describe('when calling unload passing array of urls', () => { 219 | 220 | beforeEach(() => { 221 | spyOn(bufferLoader._bufferCache, 'removeBuffer').and.callThrough(); 222 | bufferLoader.unload(['base/sounds/beep.mp3', 'base/sounds/button.mp3']); 223 | }); 224 | 225 | it('should call removeBuffer twice', () => { 226 | expect(bufferLoader._bufferCache.removeBuffer.calls.count()).toBe(2); 227 | }); 228 | }); 229 | 230 | describe('when calling unload without passing any argument', () => { 231 | 232 | beforeEach(() => { 233 | spyOn(bufferLoader._bufferCache, 'clearBuffers').and.callThrough(); 234 | bufferLoader.unload(); 235 | }); 236 | 237 | it('should call clearBuffers', () => { 238 | expect(bufferLoader._bufferCache.clearBuffers).toHaveBeenCalled(); 239 | }); 240 | });*/ 241 | 242 | describe('when calling dispose', () => { 243 | 244 | beforeEach(() => { 245 | spyOn(bufferLoader, 'unload').and.callThrough(); 246 | bufferLoader.dispose(); 247 | }); 248 | 249 | it('should call unload method', () => { 250 | expect(bufferLoader.unload).toHaveBeenCalled(); 251 | }); 252 | 253 | it('should set the cache, progress-calls and context variables to null', () => { 254 | expect(bufferLoader._bufferCache).toBeNull(); 255 | expect(bufferLoader._progressCallsAndCallbacks).toBeNull(); 256 | expect(bufferLoader._context).toBeNull(); 257 | }); 258 | 259 | it('should set the disposed variable to true', () => { 260 | expect(bufferLoader._disposed).toBe(true); 261 | }); 262 | }); 263 | }); 264 | -------------------------------------------------------------------------------- /src/Html5AudioPool.js: -------------------------------------------------------------------------------- 1 | import utility from './Utility'; 2 | 3 | /** 4 | * Manages the pool of HTML5 audio nodes. 5 | * @class 6 | */ 7 | class Html5AudioPool { 8 | 9 | /** 10 | * Maximum number of HTML5 audio nodes that can be allocated for a resource. 11 | * @type {number} 12 | * @private 13 | */ 14 | _maxNodesPerSource = 100; 15 | 16 | /** 17 | * Inactive time of sound/HTML5 audio. 18 | * @type {number} 19 | * @private 20 | */ 21 | _inactiveTime = 2; 22 | 23 | /** 24 | * The sounds store. 25 | * @type {function} 26 | * @private 27 | */ 28 | _soundCleanUpCallback = null; 29 | 30 | /** 31 | * Created audio nodes for each resource. 32 | * @type {object} 33 | * @private 34 | */ 35 | _resourceNodesMap = {}; 36 | 37 | /** 38 | * True if the `soundCleanUpCallback` called. 39 | * @type {boolean} 40 | * @private 41 | */ 42 | _cleanUpCalled = false; 43 | 44 | /** 45 | * Constructor 46 | * @param {number} maxNodesPerSource Maximum number of audio nodes allowed for a resource. 47 | * @param {number} inactiveTime The period after which HTML5 audio node is marked as inactive. 48 | * @param {function} soundCleanUpCallback The inactive sounds cleanup callback. 49 | */ 50 | constructor(maxNodesPerSource, inactiveTime, soundCleanUpCallback) { 51 | this._maxNodesPerSource = maxNodesPerSource; 52 | this._inactiveTime = inactiveTime; 53 | this._soundCleanUpCallback = soundCleanUpCallback; 54 | } 55 | 56 | /** 57 | * Allocates an audio node for the passed source. 58 | * @param {string} src The audio url. 59 | * @return {Audio} 60 | */ 61 | allocateForSource(src) { 62 | this._createSrc(src); 63 | this._checkMaxNodesForSrc(src); 64 | 65 | const nodes = this._resourceNodesMap[src], 66 | { unallocated } = nodes; 67 | 68 | const audio = new Audio(); 69 | audio.crossOrigin = 'anonymous'; 70 | unallocated.push({ audio: audio, time: new Date() }); 71 | 72 | return audio; 73 | } 74 | 75 | /** 76 | * Allocates a HTML5 audio node to a particular group. 77 | * @param {string} src The audio url. 78 | * @param {number} [groupId] The buzz group id. 79 | * @return {Audio} 80 | */ 81 | allocateForGroup(src, groupId) { 82 | this._createGroup(src, groupId); 83 | this._checkMaxNodesForSrc(src); 84 | 85 | const nodes = this._resourceNodesMap[src], 86 | { unallocated, allocated } = nodes, 87 | audio = unallocated.length ? unallocated.shift().audio : new Audio(); 88 | 89 | audio.crossOrigin = 'anonymous'; 90 | 91 | allocated[groupId].push({ soundId: null, audio: audio, time: new Date() }); 92 | 93 | return audio; 94 | } 95 | 96 | /** 97 | * Allocates the pre-loaded HTML5 audio node to a sound. 98 | * @param {string} src The audio file url. 99 | * @param {number} groupId The group id. 100 | * @param {number} soundId The sound id. 101 | * @return {Audio} 102 | */ 103 | allocateForSound(src, groupId, soundId) { 104 | this._createGroup(src, groupId); 105 | 106 | const nodes = this._resourceNodesMap[src], 107 | { allocated } = nodes, 108 | notAllocatedAudioObj = allocated[groupId].find(x => x.soundId === null); 109 | 110 | if (!notAllocatedAudioObj) { 111 | throw new Error(`No free audio nodes available in the group ${groupId}`); 112 | } 113 | 114 | notAllocatedAudioObj.soundId = soundId; 115 | 116 | return notAllocatedAudioObj.audio; 117 | } 118 | 119 | /** 120 | * Releases the audio nodes allocated for all resources. 121 | */ 122 | release() { 123 | Object.keys(this._resourceNodesMap).forEach(src => this.releaseForSource(src)); 124 | } 125 | 126 | /** 127 | * Releases the audio nodes allocated for a resource. 128 | * @param {string} src The audio url. 129 | */ 130 | releaseForSource(src) { 131 | const nodes = this._resourceNodesMap[src], 132 | { unallocated, allocated } = nodes; 133 | 134 | unallocated.forEach(x => this._destroyNode(x.audio)); 135 | 136 | Object.keys(allocated).forEach(groupId => this.releaseForGroup(src, groupId)); 137 | 138 | delete this._resourceNodesMap[src]; 139 | } 140 | 141 | /** 142 | * Releases the audio nodes allocated for a group. 143 | * @param {string} src The audio file url. 144 | * @param {number} groupId The group id. 145 | */ 146 | releaseForGroup(src, groupId) { 147 | const nodes = this._resourceNodesMap[src], 148 | { allocated } = nodes; 149 | 150 | allocated[groupId].map(x => x.audio).forEach(node => this._destroyNode(node)); 151 | delete allocated[groupId]; 152 | } 153 | 154 | /** 155 | * Destroys the audio node reserved for sound. 156 | * @param {string} src The audio file url. 157 | * @param {number} groupId The buzz id. 158 | * @param {number|Audio} soundIdOrAudio The sound id or audio. 159 | */ 160 | releaseAudio(src, groupId, soundIdOrAudio) { 161 | const nodes = this._resourceNodesMap[src], 162 | { allocated, unallocated } = nodes; 163 | 164 | if (soundIdOrAudio instanceof Audio) { 165 | this._destroyNode(soundIdOrAudio); 166 | 167 | if (groupId) { 168 | allocated[groupId] = allocated[groupId].filter(x => x.audio !== soundIdOrAudio); 169 | } else { 170 | nodes.unallocated = unallocated.filter(x => x.audio !== soundIdOrAudio); 171 | } 172 | } else { 173 | const allocatedAudioObj = allocated[groupId].find(x => x.soundId === soundIdOrAudio); 174 | this._destroyNode(allocatedAudioObj.audio); 175 | allocated[groupId] = allocated[groupId].filter(x => x.soundId !== soundIdOrAudio); 176 | } 177 | 178 | groupId && !allocated[groupId].length && delete allocated[groupId]; 179 | !unallocated.length && !Object.keys(allocated).length && delete this._resourceNodesMap[src]; 180 | } 181 | 182 | /** 183 | * Removes inactive HTML5 audio nodes. 184 | */ 185 | cleanUp() { 186 | const now = new Date(); 187 | 188 | Object.keys(this._resourceNodesMap).forEach(src => { 189 | const nodes = this._resourceNodesMap[src], 190 | { unallocated, allocated } = nodes; 191 | 192 | Object.keys(allocated).forEach(groupId => { 193 | const inactiveNodes = allocated[groupId] 194 | .filter(x => x.soundId === null && ((now - x.time) / 1000 > this._inactiveTime * 60)); 195 | 196 | allocated[groupId] = allocated[groupId].filter(x => inactiveNodes.indexOf(x) === -1); 197 | 198 | inactiveNodes.forEach(x => this._destroyNode(x.audio)); 199 | }); 200 | 201 | const inactiveNodes = unallocated.filter(x => ((now - x.time) / 1000 > this._inactiveTime * 60)); 202 | nodes.unallocated = unallocated.filter(x => inactiveNodes.indexOf(x) === -1); 203 | 204 | inactiveNodes.forEach(x => this._destroyNode(x.audio)); 205 | }); 206 | } 207 | 208 | /** 209 | * Releases all the audio nodes. 210 | */ 211 | dispose() { 212 | Object.keys(this._resourceNodesMap).forEach(src => this.releaseForSource(src)); 213 | } 214 | 215 | /** 216 | * Returns true if there are free audio nodes available for a source or group. 217 | * @param {string} src The audio file url. 218 | * @param {number} [groupId] The group id. 219 | * @return {boolean} 220 | */ 221 | hasFreeNodes(src, groupId) { 222 | if (!this._resourceNodesMap.hasOwnProperty(src)) { 223 | return false; 224 | } 225 | 226 | const nodes = this._resourceNodesMap[src], 227 | { unallocated, allocated } = nodes; 228 | 229 | return !groupId ? unallocated.length > 0 : allocated[groupId].filter(x => x.soundId === null).length > 0; 230 | } 231 | 232 | /** 233 | * Creates an entry for the passed source in object if not exists. 234 | * @param {string} src The audio file. 235 | * @private 236 | */ 237 | _createSrc(src) { 238 | if (this._resourceNodesMap.hasOwnProperty(src)) { 239 | return; 240 | } 241 | 242 | this._resourceNodesMap[src] = { 243 | unallocated: [], 244 | allocated: {} 245 | }; 246 | } 247 | 248 | /** 249 | * Creates an entry for the passed source and group if not exists. 250 | * @param {string} src The audio file. 251 | * @param {number} groupId The group id. 252 | * @private 253 | */ 254 | _createGroup(src, groupId) { 255 | this._createSrc(src); 256 | 257 | const nodes = this._resourceNodesMap[src], 258 | { allocated } = nodes; 259 | 260 | if (allocated.hasOwnProperty(groupId)) { 261 | return; 262 | } 263 | 264 | allocated[groupId] = []; 265 | } 266 | 267 | /** 268 | * Checks and throws error if max audio nodes reached for the passed resource. 269 | * @param {string} src The source url. 270 | * @private 271 | */ 272 | _checkMaxNodesForSrc(src) { 273 | if (!this._resourceNodesMap.hasOwnProperty(src)) { 274 | return; 275 | } 276 | 277 | const nodes = this._resourceNodesMap[src], 278 | { unallocated, allocated } = nodes; 279 | 280 | let totalAllocatedLength = 0; 281 | 282 | Object.keys(allocated).forEach(groupId => { 283 | totalAllocatedLength = totalAllocatedLength + allocated[groupId].length; 284 | }); 285 | 286 | if (unallocated.length + totalAllocatedLength < this._maxNodesPerSource) { 287 | return; 288 | } 289 | 290 | if (!this._cleanUpCalled) { 291 | this.cleanUp(); 292 | this._soundCleanUpCallback(src); 293 | this._cleanUpCalled = true; 294 | this._checkMaxNodesForSrc(src); 295 | } 296 | 297 | this._cleanUpCalled = false; 298 | 299 | throw new Error(`Maximum nodes reached for resource ${src}`); 300 | } 301 | 302 | /** 303 | * Destroys the passed audio node. 304 | * @param {Audio} audio The HTML5 audio element. 305 | * @private 306 | */ 307 | _destroyNode(audio) { 308 | audio.pause(); 309 | utility.isIE() && (audio.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'); 310 | audio.onerror = null; 311 | audio.onend = null; 312 | audio.canplaythrough = null; 313 | } 314 | } 315 | 316 | export default Html5AudioPool; 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # musquito 6 | 7 | [musquito](http://musquitojs.com) is an audio engine created using Web Audio API for HTML5 games and interactive websites. It provides 8 | a simple abstraction to create and play sounds easier. 9 | 10 | Below are some of the core features supported by the library. 11 | 12 | - Built on the powerful Web Audio API 13 | - Simple API to create and play sounds 14 | - Supports sound groups 15 | - Supports variety of codecs 16 | - Supports audio sprites 17 | - Supports streaming using HTML5 audio nodes 18 | - Fading 19 | - Caching 20 | - Auto Garbage Management 21 | 22 | ## Browser Support 23 | 24 | - Google Chrome 25 | - Firefox 26 | - Safari 27 | - Opera 28 | - Microsoft Edge 29 | 30 | 31 | ## Installation 32 | 33 | At this time *musquito* is available only in npm and you can install it using the below command. 34 | 35 | ``` 36 | npm install musquito --save 37 | ``` 38 | 39 | You can also directly reference files available from the distribution folder. 40 | 41 | ``` 42 | 43 | ``` 44 | 45 | 46 | ## "Hello World" example 47 | 48 | A simple example of how to create and play a gun fire sound. 49 | 50 | ```js 51 | import $buzz from 'musquito'; 52 | 53 | $buzz.play('gun.mp3'); 54 | ``` 55 | 56 | 57 | ## Passing Additional Parameters 58 | 59 | The below example shows how you can pass additional parameters like volume, rate and callback. 60 | 61 | ```js 62 | $buzz.play({ 63 | src: ['greenade.mp3', 'greenade.wav'], 64 | volume: 0.5, 65 | rate: 2, 66 | playEndCallback: () => alert('Playback started') 67 | }); 68 | ``` 69 | 70 | 71 | ## Using Sprites 72 | 73 | Audio Sprites are like image sprites concatenates multiple small sounds in a single file. You can create audio sprite using this tool. 74 | 75 | Below is an example of how to use sprites. 76 | 77 | ```js 78 | $buzz.play({ 79 | src: 'sprite.mp3', 80 | sprite: { 81 | "beep": [ 82 | 0, 83 | 0.48108843537414964 84 | ], 85 | "button": [ 86 | 2, 87 | 2.4290249433106577 88 | ], 89 | "click": [ 90 | 4, 91 | 4.672018140589569 92 | ] 93 | }, 94 | sound: 'beep' 95 | }); 96 | ``` 97 | 98 | 99 | ## Pausing and Stopping Sounds 100 | 101 | Calling the `play` method returns the sound id and you can use it to call other methods like pause, stop, change the volume and more properties of the sound. 102 | 103 | ```js 104 | const soundid = $buzz.play('bg.mp3'); 105 | 106 | // Pausing sound 107 | $buzz.pause(soundid); 108 | 109 | // Stopping sound 110 | $buzz.stop(soundid); 111 | ``` 112 | 113 | 114 | ## Fading Sounds 115 | 116 | You can fade the volume of a playing sound linearly or exponentially as shown below. 117 | 118 | ```js 119 | const soundid = $buzz.play('bg.mp3'); 120 | 121 | setTimeout(() => { 122 | $buzz.fade(soundid, 0, 3); 123 | }, 2000); 124 | ``` 125 | 126 | 127 | ## Playing Long Audio Files 128 | 129 | To stream long audio files and play using HTML5 audio node you can pass the `stream` parameter as true. 130 | 131 | ```js 132 | $buzz.play({ 133 | src: 'bg.mp3', 134 | stream: true 135 | }); 136 | ``` 137 | 138 | 139 | ## Advanced Example 140 | 141 | The below example shows how we can setup audio engine by passing audio resources with shorthand identifiers initially before playing sounds. The `setup` method also takes lot of other arguments to configure the engine, please refer the API docs. 142 | 143 | ```js 144 | $buzz.setup({ 145 | src: { 146 | bg: 'bg.mp3', 147 | sprite: { 148 | url: 'sprite.mp3', 149 | sprite: { 150 | "beep": [ 151 | 0, 152 | 0.48108843537414964 153 | ], 154 | "button": [ 155 | 2, 156 | 2.4290249433106577 157 | ], 158 | "click": [ 159 | 4, 160 | 4.672018140589569 161 | ] 162 | } 163 | } 164 | }, 165 | oninit: () => { 166 | // Playing sounds with identifiers 167 | $buzz.play('#bg'); 168 | $buzz.play('#sprite.button'); 169 | } 170 | }); 171 | ``` 172 | 173 | 174 | ## Creating Audio Groups 175 | 176 | Sometimes it's convenient to create a sound group which is called as "Buzz" that helps to create and manage multiple sounds for a single audio resource. Buzzes also supports events. The below example shows how we can create a sound group for a sprite and play multiple sounds easier. 177 | 178 | ```js 179 | const buzz = $buzz({ 180 | src: 'sprite.mp3', 181 | sprite:{ 182 | "beep": [ 183 | 0, 184 | 0.48108843537414964 185 | ], 186 | "button": [ 187 | 2, 188 | 2.4290249433106577 189 | ], 190 | "click": [ 191 | 4, 192 | 4.672018140589569 193 | ] 194 | } 195 | }); 196 | 197 | buzz.play('beep'); 198 | buzz.play('button'); 199 | buzz.play('click'); 200 | ``` 201 | 202 | 203 | For demos and detailed documentation please visit [here](http://musquitojs.com). 204 | 205 | 206 | ## API 207 | 208 | ### `$buzz` static methods 209 | 210 | Sets-up the audio engine. When you call the `$buzz` function the setup method will be automatically called. Often you call this method manually during the application startup to pass the parameters to the audio engine and also when you like to pre-load all the audio resources upfront. Before calling any method in engine you should call this method first. 211 | 212 | The parameters you can pass to the `setup` method are below. 213 | 214 | | Method | Returns | Description | 215 | |--------|:-------:|-------------| 216 | | setup(args?: object) | $buzz | Sets-up the audio engine. The different parameters you can pass in arguments object are `volume`, `muted`, `maxNodesPerSource`, `cleanUpInterval`, `autoEnable`, `src`, `preload`, `progress` and event handler functions like `oninit`, `onstop`, `onmute`, `onvolume`, `onsuspend`, `onresume`, `onerror` and `ondone`. | 217 | | play(idOrSoundArgs: number,string,Array,object) | $buzz,number | Creates and plays a new sound or the existing sound for the passed id. Returns sound id if new one is created. | 218 | | pause(id: number) | $buzz | Pauses the sound for the passed id. | 219 | | stop(id?: number) | $buzz | Stops the sound for the passed id or all the playing sounds. Stopping engine fires the "stop" event. | 220 | | mute(id?: number) | $buzz | Mutes the sound if id is passed or the engine. Fires the "mute" event if engine is muted. | 221 | | unmute(id?: number) | $buzz | Un-mutes the sound if id is passed or the engine. Fires the "mute" event if engine is un-muted. | 222 | | volume(vol?: number, id?: number) | $buzz,number | Gets/sets the volume for the audio engine that controls global volume for all sounds or the sound of the passed id. Fires the "volume" event in case of engine. The value of the passed volume should be from 0 to 1. | 223 | | fade(id: number, to: number, duration: number, type = 'linear','exponential') | $buzz | Fades the sound belongs to the passed id. | 224 | | fadeStop(id: number) | $buzz | Stops the running fade. | 225 | | rate(id: number, rate?: number) | $buzz,number | Gets/sets the rate of the passed sound. The value of the passed rate should be from 1 to 5. | 226 | | seek(id: number, seek?: number) | $buzz,number | Gets/sets the current position of the passed sound. | 227 | | loop(id: number, loop?: boolean) | $buzz,boolean | Gets/sets the loop parameter of the sound. | 228 | | destroy(id: number) | $buzz | Destroys the passed sound. | 229 | | load(urls: string, Array, progressCallback: function) | Promise | Loads single or multiple audio resources into audio buffers and returns them. | 230 | | loadMedia(urls: string, Array) | Promise | Pre-loads single or multiple HTML5 audio nodes with the passed resources and returns them. | 231 | | unload(urls: string, Array) | $buzz | Unloads single or multiple loaded audio buffers from cache. | 232 | | unloadMedia(urls: string, Array) | $buzz | Releases audio nodes allocated for the passed urls. | 233 | | register(src: string|Array|object, key: string) | $buzz | Assigns a short-hand key for the audio source. In case of object the properties are `url`, `format` and `sprite`. | 234 | | unregister(src: string|Array|object, key: string) | $buzz | Removes the assigned key for the audio source. | 235 | | getSource(key: string) | string,Array,object | Returns the assigned audio source for the passed key. | 236 | | suspend() | $buzz | Stops all the playing sounds and suspends the engine immediately. | 237 | | resume() | $buzz | Resumes the engine from the suspended mode. | 238 | | terminate() | $buzz | Shuts down the engine. | 239 | | muted() | boolean | Returns whether the engine is currently muted or not. | 240 | | state() | EngineState | Returns the state of the engine. The different values are "notready", "ready", "suspending", "suspended", "resuming", "destroying", "done" and "no-audio". | 241 | | buzz(id: number) | Buzz | Returns the buzz for the passed id. | 242 | | buzzes() | Array | Returns all the buzzes. | 243 | | sound(id: number) | Sound | Returns the sound for the passed id. `Sound` is an internal object and you don't have to deal with it usually. | 244 | | sounds() | Array | Returns all the sounds created directly by engine. Sounds created by sound groups are not included. | 245 | | context() | AudioContext | Returns the created audio context. | 246 | | isAudioAvailable() | boolean | Returns true if Web Audio API is available. | 247 | | on(eventName: string, handler: function, once = false) | $buzz | Subscribes to an event. | 248 | | off(eventName: string, handler: function) | $buzz | Un-subscribes from an event. | 249 | | masterGain() | GainNode | Returns the master gain node. | 250 | | bufferLoader() | BufferLoader | Returns buffer loader. | 251 | | mediaLoader() | MediaLoader | Returns media loader. | 252 | 253 | 254 | ### `$buzz` function 255 | 256 | **`$buzz(args: string|Array|object)`** 257 | 258 | `$buzz` is the single API that helps you to create and manage sounds. It's a function that returns a `Buzz` object. The `Buzz` object 259 | helps to control group of sounds created for a single audio source. 260 | 261 | You can pass a single audio source, array of audio sources or an object. If an array of audio sources is passed then the first compatible one is picked for playing. 262 | If you need to pass additional information like initial volume, playback speed then you need to pass an object. 263 | 264 | #### The different options you can pass in arguments object for the `$buzz` function. 265 | 266 | | Name | Type | Required | Default | Description | 267 | |------|------|:--------:|:-------:|-------------| 268 | | src | string, Array | yes | | Single or array of audio sources. If an array of audio sources is passed then the first compatible one is picked for playing. | 269 | | id | number | no | Auto-generated | The unique identifier for the `Buzz` object. | 270 | | volume | number | no | 1.0 | The initial volume of the sound. The value should be from `0.0` to `1.0`. | 271 | | rate | number | no | 1.0 | The initial playback speed. The value should be from `0.5` to `5.0`. | 272 | | loop | boolean | no | false | Pass `true` to play the sound repeatedly. | 273 | | muted | boolean | no | false | Pass `true` to keep the sound muted initially. | 274 | | preload | boolean | no | false | Pass `true` to pre-load the sound. | 275 | | autoplay | boolean | no | false | Pass `true` to play the sound at-once created. | 276 | | stream | boolean | no | false | Passing `true` will use HTML5 audio node for playing the sound. This option you can use to play long audio files like background music in a game. This feature is available only in version v2. | 277 | | format | string, Array | no | false | Single or array of audio formats for the passed audio sources. | 278 | | sprite | object | no | | The sprite definition object that contains the starting and ending positions of each sound embedded in the sprite. | 279 | | onload | function | no | | The event handler for "load" event. | 280 | | onloadprogress | function | no | | The event handler for "loadprogress" event. | 281 | | onunload | function | no | | The event handler for "unload" event. | 282 | | onplaystart | function | no | | The event handler for "playstart" event. | 283 | | onplayend | function | no | | The event handler for "playend" event. | 284 | | onstop | function | no | | The event handler for "stop" event. | 285 | | onpause | function | no | | The event handler for "pause" event. | 286 | | onmute | function | no | | The event handler for "mute" event. | 287 | | onvolume | function | no | | The event handler for "volume" event. | 288 | | onrate | function | no | | The event handler for "rate" event. | 289 | | onseek | function | no | | The event handler for "seek" event. | 290 | | onerror | function | no | | The event handler for "error" event. | 291 | | ondestroy | function | no | | The event handler for "destroy" event. | 292 | 293 | 294 | ### `Buzz` object methods 295 | 296 | | Method | Returns | Description | 297 | |--------|:-------:|-------------| 298 | | load(soundId?: string) | Buzz | Loads the audio buffer or preloads a HTML5 audio node. The `soundId` can be passed only in the case stream buzz type. When you pass it that particular sound's audio node will be pre-loaded. | 299 | | play(soundOrId?: string, number) | Buzz | Plays a new sound or the passed sound defined in the sprite or the sound that belongs to the passed id. | 300 | | pause(id?: number) | Buzz | Pauses the sound belongs to the passed id or all the sounds belongs to this group. | 301 | | stop(id?: number) | Buzz | Stops the sound belongs to the passed id or all the sounds belongs to this group. | 302 | | mute(id?: number) | Buzz | Mutes the sound belongs to the passed id or all the sounds belongs to this group. | 303 | | unmute(id?: number) | Buzz | Un-mutes the sound belongs to the passed id or all the sounds belongs to this group. | 304 | | volume(volume?: number, id?: number) | Buzz, number | Gets/sets the volume of the passed sound or the group. The passed value should be from `0.0` to `1.0`. | 305 | | rate(rate?: number, id?: number) | Buzz, number | Gets/sets the rate of the passed sound or the group. The passed value should be from `0.5` to `5.0`. | 306 | | seek(id: number, seek?: number) | Buzz, number | Gets/sets the current playback position of the sound. | 307 | | loop(loop?: boolean, id?: number) | Buzz, boolean | Gets/sets the looping behavior of a sound or the group. | 308 | | fade(to: number, duration: number, type = 'linear', id?: number) | Buzz | Fades the volume of a playing sound or all sounds belongs to the group. | 309 | | fadeStop(id?: number) | Buzz | Stops the current running fade of the passed sound or all sounds belongs to the group. | 310 | | playing(id: number) | boolean | Returns true if the passed sound is playing. | 311 | | muted(id?: number) | boolean | Returns true if the passed sound is muted or the group is muted. | 312 | | state(id?: number) | BuzzState, SoundState | Returns the state of the passed sound or the group. | 313 | | duration(id?: number) | number | Returns the duration of the passed sound or the total duration of the sound. | 314 | | unload() | Buzz | Unloads the loaded audio buffer. | 315 | | destroy() | Buzz | Stops and destroys all the sounds belong to this group and release other dependencies. | 316 | | on(eventName: string, handler: function, once = false, id?: number) | Buzz | Subscribes to an event for the sound or the group. | 317 | | off(eventName: string, handler: function, id?: number) | Buzz | Un-subscribes from an event for the sound or the group. | 318 | | id() | number | Returns the unique id of the sound. | 319 | | loadState() | LoadState | Returns the audio resource loading status. The different values are "notloaded", "loading" and "loaded". | 320 | | isLoaded() | boolean | Returns true if the audio source is loaded. | 321 | | sound(id: number) | Sound | Returns the sound for the passed id. | 322 | | sounds() | Array | Returns all the sounds belongs to this buzz group. | 323 | | alive(id: number) | boolean | Returns `true` if the passed sound exists. | 324 | | gain() | GainNode | Returns the gain node. | 325 | 326 | 327 | ## License 328 | 329 | MIT 330 | 331 | Copyright © 2020 · Vijaya Anand · All Rights Reserved. 332 | 333 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 334 | 335 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 336 | 337 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 338 | -------------------------------------------------------------------------------- /src/Sound.js: -------------------------------------------------------------------------------- 1 | import engine from './Engine'; 2 | import utility from './Utility'; 3 | import workerTimer from './WorkerTimer'; 4 | 5 | /** 6 | * Enum that represents the different states of a sound. 7 | * @enum {string} 8 | */ 9 | const SoundState = { 10 | Ready: 'ready', 11 | Playing: 'playing', 12 | Paused: 'paused', 13 | Destroyed: 'destroyed' 14 | }; 15 | 16 | /** 17 | * Represents a sound created using Web Audio API. 18 | * @class 19 | */ 20 | class Sound { 21 | 22 | /** 23 | * Unique id. 24 | * @type {number} 25 | * @private 26 | */ 27 | _id = -1; 28 | 29 | /** 30 | * The current volume of the sound. Should be from 0.0 to 1.0. 31 | * @type {number} 32 | * @private 33 | */ 34 | _volume = 1.0; 35 | 36 | /** 37 | * The current playback speed. Should be from 0.5 to 5. 38 | * @type {number} 39 | * @private 40 | */ 41 | _rate = 1; 42 | 43 | /** 44 | * True if the sound is currently muted. 45 | * @type {boolean} 46 | * @private 47 | */ 48 | _muted = false; 49 | 50 | /** 51 | * True if the sound should play repeatedly. 52 | * @type {boolean} 53 | * @private 54 | */ 55 | _loop = false; 56 | 57 | /** 58 | * The current state (playing, paused etc.) of the sound. 59 | * @type {SoundState} 60 | * @private 61 | */ 62 | _state = SoundState.Ready; 63 | 64 | /** 65 | * Web API's audio context. 66 | * @type {AudioContext} 67 | * @private 68 | */ 69 | _context = null; 70 | 71 | /** 72 | * The gain node to control the volume of the sound. 73 | * @type {GainNode} 74 | * @private 75 | */ 76 | _gainNode = null; 77 | 78 | /** 79 | * True to use HTML5 audio node. 80 | * @type {boolean} 81 | * @private 82 | */ 83 | _stream = false; 84 | 85 | /** 86 | * The audio buffer. 87 | * @type {AudioBuffer} 88 | * @private 89 | */ 90 | _buffer = null; 91 | 92 | /** 93 | * The HTML5 Audio element. 94 | * @type {Audio} 95 | * @private 96 | */ 97 | _audio = null; 98 | 99 | /** 100 | * The AudioBufferSourceNode that plays the audio buffer assigned to it. 101 | * @type {AudioBufferSourceNode} 102 | * @private 103 | */ 104 | _bufferSourceNode = null; 105 | 106 | /** 107 | * Duration of the playback in seconds. 108 | * @type {number} 109 | * @private 110 | */ 111 | _duration = 0; 112 | 113 | /** 114 | * The playback start position. 115 | * @type {number} 116 | * @private 117 | */ 118 | _startPos = 0; 119 | 120 | /** 121 | * The playback end position. 122 | * @type {number} 123 | * @private 124 | */ 125 | _endPos = 0; 126 | 127 | /** 128 | * The current position of the playback. 129 | * @type {number} 130 | * @private 131 | */ 132 | _currentPos = 0; 133 | 134 | /** 135 | * The position of the playback during rate change. 136 | * @type {number} 137 | * @private 138 | */ 139 | _rateSeek = 0; 140 | 141 | /** 142 | * The time at which the playback started. 143 | * This property is required for getting the seek position of the playback. 144 | * @type {number} 145 | * @private 146 | */ 147 | _startTime = 0; 148 | 149 | /** 150 | * The callback that will be called when the underlying HTML5 audio node is loaded. 151 | * @type {function} 152 | * @private 153 | */ 154 | _loadCallback = null; 155 | 156 | /** 157 | * The callback that will be invoked after the play ends. 158 | * @type {function} 159 | * @private 160 | */ 161 | _playEndCallback = null; 162 | 163 | /** 164 | * The callback that will be invoked after the sound destroyed. 165 | * @type {function} 166 | * @private 167 | */ 168 | _destroyCallback = null; 169 | 170 | /** 171 | * True if the sound is currently fading. 172 | * @type {boolean} 173 | * @private 174 | */ 175 | _fading = false; 176 | 177 | /** 178 | * The timer that runs function after the fading is complete. 179 | * @type {number|null} 180 | * @private 181 | */ 182 | _fadeTimer = null; 183 | 184 | /** 185 | * The callback that will be invoked after the fade is completed. 186 | * @type {function} 187 | * @private 188 | */ 189 | _fadeEndCallback = null; 190 | 191 | /** 192 | * The callback that will be invoked when there is error in HTML5 audio node. 193 | * @type {function} 194 | * @private 195 | */ 196 | _audioErrorCallback = null; 197 | 198 | /** 199 | * Web Audio API's audio node to control media element. 200 | * @type {MediaElementAudioSourceNode} 201 | * @private 202 | */ 203 | _mediaElementAudioSourceNode = null; 204 | 205 | /** 206 | * Represents the timer that is used to reset the variables once the sprite sound is played. 207 | * @type {number|null} 208 | * @private 209 | */ 210 | _endTimer = null; 211 | 212 | /** 213 | * True for sprite. 214 | * @type {boolean} 215 | * @private 216 | */ 217 | _isSprite = false; 218 | 219 | /** 220 | * Last played time. 221 | * @type {Date} 222 | * @private 223 | */ 224 | _lastPlayed = new Date(); 225 | 226 | /** 227 | * True to not auto-destroy. 228 | * @type {boolean} 229 | * @private 230 | */ 231 | _persist = false; 232 | 233 | /** 234 | * True if the audio source (buffer or html5 audio) exists. 235 | * @type {boolean} 236 | * @private 237 | */ 238 | _sourceExists = false; 239 | 240 | /** 241 | * True if the HTML5 audio node is pre-loaded. 242 | * @type {boolean} 243 | * @private 244 | */ 245 | _loaded = false; 246 | 247 | /** 248 | * Initializes the internal properties of the sound. 249 | * @param {object} args The input parameters of the sound. 250 | * @param {number} args.id The unique id of the sound. 251 | * @param {boolean} [args.stream = false] True to use HTML5 audio node for playing sound. 252 | * @param {Audio} [args.audio] The pre-loaded HTML5 audio object. 253 | * @param {AudioBuffer} [args.buffer] Audio source buffer. 254 | * @param {number} [args.volume = 1.0] The initial volume of the sound. Should be from 0.0 to 1.0. 255 | * @param {number} [args.rate = 1] The initial playback rate of the sound. Should be from 0.5 to 5.0. 256 | * @param {boolean} [args.loop = false] True to play the sound repeatedly. 257 | * @param {boolean} [args.muted = false] True to be muted initially. 258 | * @param {number} [args.startPos] The playback start position. 259 | * @param {number} [args.endPos] The playback end position. 260 | * @param {function} [args.loadCallback] The callback that will be called when the underlying HTML5 audio node is loaded. 261 | * @param {function} [args.playEndCallback] The callback that will be invoked after the play ends. 262 | * @param {function} [args.destroyCallback] The callback that will be invoked after destroyed. 263 | * @param {function} [args.fadeEndCallback] The callback that will be invoked the fade is completed. 264 | * @param {function} [args.audioErrorCallback] The callback that will be invoked when there is error in HTML5 audio node. 265 | * @constructor 266 | */ 267 | constructor(args) { 268 | this._onBufferEnded = this._onBufferEnded.bind(this); 269 | this._onHtml5Ended = this._onHtml5Ended.bind(this); 270 | this._onCanPlayThrough = this._onCanPlayThrough.bind(this); 271 | this._onAudioError = this._onAudioError.bind(this); 272 | 273 | const { 274 | id, 275 | stream, 276 | buffer, 277 | audio, 278 | volume, 279 | rate, 280 | loop, 281 | muted, 282 | startPos, 283 | endPos, 284 | loadCallback, 285 | playEndCallback, 286 | destroyCallback, 287 | fadeEndCallback, 288 | audioErrorCallback 289 | } = args; 290 | 291 | // Set the passed id or the random one. 292 | this._id = typeof id === 'number' ? id : utility.id(); 293 | 294 | // Set the passed audio buffer or HTML5 audio node. 295 | this._buffer = buffer; 296 | this._audio = audio; 297 | this._sourceExists = Boolean(this._buffer) || Boolean(this._audio); 298 | 299 | // Set other properties. 300 | this._stream = stream; 301 | this._sourceExists && (this._endPos = this._stream ? this._audio.duration : this._buffer.duration); 302 | volume && (this._volume = volume); 303 | rate && (this._rate = rate); 304 | muted && (this._muted = muted); 305 | loop && (this._loop = loop); 306 | startPos && (this._startPos = startPos); 307 | endPos && (this._endPos = endPos); 308 | this._loadCallback = loadCallback; 309 | this._playEndCallback = playEndCallback; 310 | this._destroyCallback = destroyCallback; 311 | this._fadeEndCallback = fadeEndCallback; 312 | this._audioErrorCallback = audioErrorCallback; 313 | 314 | this._duration = this._endPos - this._startPos; 315 | this._isSprite = typeof endPos !== 'undefined'; 316 | 317 | // If stream is `true` then set the playback rate, looping and listen to `error` event. 318 | if (this._stream && this._audio) { 319 | this._audio.playbackRate = this._rate; 320 | this._setLoop(this._loop); 321 | this._audio.addEventListener('error', this._onAudioError); 322 | } 323 | 324 | // If web audio is available, create gain node and set the volume.. 325 | if (engine.isAudioAvailable()) { 326 | this._context = engine.context(); 327 | this._gainNode = this._context.createGain(); 328 | this._gainNode.gain.setValueAtTime(this._muted ? 0 : this._volume, this._context.currentTime); 329 | 330 | // Create media element audio source node. 331 | if (this._stream && this._audio) { 332 | this._mediaElementAudioSourceNode = this._context.createMediaElementSource(this._audio); 333 | this._mediaElementAudioSourceNode.connect(this._gainNode); 334 | } 335 | } 336 | } 337 | 338 | /** 339 | * Sets the audio source for the sound. 340 | * @param {AudioBuffer | Audio} source The audio source. 341 | */ 342 | source(source) { 343 | if (this._sourceExists) { 344 | return; 345 | } 346 | 347 | if (this._stream) { 348 | this._audio = source; 349 | !this._isSprite && (this._endPos = this._audio.duration); 350 | this._audio.playbackRate = this._rate; 351 | 352 | this._setLoop(this._loop); 353 | this._audio.addEventListener('error', this._onAudioError); 354 | 355 | this._mediaElementAudioSourceNode = this._context.createMediaElementSource(this._audio); 356 | this._mediaElementAudioSourceNode.connect(this._gainNode); 357 | } else { 358 | this._buffer = source; 359 | !this._isSprite && (this._endPos = this._buffer.duration); 360 | } 361 | 362 | this._sourceExists = true; 363 | this._duration = this._endPos - this._startPos; 364 | this._loaded = true; 365 | } 366 | 367 | /** 368 | * Pre-loads the underlying HTML audio node (only in case of stream). 369 | */ 370 | load() { 371 | if (!this._stream || 372 | !this._sourceExists || 373 | this.isPlaying() || 374 | this.state() === SoundState.Destroyed) { 375 | return; 376 | } 377 | 378 | this._audio.addEventListener('canplaythrough', this._onCanPlayThrough); 379 | this._audio.currentTime = 0; 380 | this.canPlay() && this._onCanPlayThrough(); 381 | } 382 | 383 | /** 384 | * Plays the sound or the sound defined in the sprite. 385 | * @return {Sound} 386 | */ 387 | play() { 388 | // If the source not exists or sound is already playing then return. 389 | if (!this._sourceExists || this.isPlaying()) { 390 | return this; 391 | } 392 | 393 | this._stream ? this._playHtml5() : this._playBuffer(); 394 | 395 | // Record the starting time and set the state. 396 | this._startTime = this._context.currentTime; 397 | this._state = SoundState.Playing; 398 | 399 | return this; 400 | } 401 | 402 | /** 403 | * Pauses the playing sound. 404 | * @return {Sound} 405 | */ 406 | pause() { 407 | // If the source not exists or the sound is already playing return. 408 | if (!this._sourceExists || !this.isPlaying()) { 409 | return this; 410 | } 411 | 412 | // Stop the current running fade. 413 | this.fadeStop(); 414 | 415 | if (this._stream) { 416 | this._audio.removeEventListener('ended', this._onHtml5Ended); 417 | this._clearEndTimer(); 418 | this._audio.pause(); 419 | } else { 420 | this._rateSeek = 0; 421 | this._destroyBufferNode(); 422 | } 423 | 424 | this._currentPos = this.seek(); 425 | this._state = SoundState.Paused; 426 | 427 | return this; 428 | } 429 | 430 | /** 431 | * Stops the sound that is playing or in paused state. 432 | * @return {Sound} 433 | */ 434 | stop() { 435 | // If the source not exists or the sound is not playing or paused return. 436 | if (!this._sourceExists || (!this.isPlaying() && !this.isPaused())) { 437 | return this; 438 | } 439 | 440 | // Stop the current running fade. 441 | this.fadeStop(); 442 | 443 | if (this._stream) { 444 | this._audio.removeEventListener('ended', this._onHtml5Ended); 445 | this._clearEndTimer(); 446 | this._audio.pause(); 447 | this._audio.currentTime = this._startPos || 0; 448 | } else { 449 | this._currentPos = 0; 450 | this._rateSeek = 0; 451 | this._destroyBufferNode(); 452 | } 453 | 454 | this._lastPlayed = new Date(); 455 | 456 | this._state = SoundState.Ready; 457 | 458 | return this; 459 | } 460 | 461 | /** 462 | * Mutes the sound. 463 | * @return {Sound} 464 | */ 465 | mute() { 466 | // Stop the current running fade. 467 | this.fadeStop(); 468 | 469 | // Set the value of gain node to 0. 470 | this._gainNode.gain.setValueAtTime(0, this._context.currentTime); 471 | 472 | // Set the muted property true. 473 | this._muted = true; 474 | 475 | return this; 476 | } 477 | 478 | /** 479 | * Un-mutes the sound. 480 | * @return {Sound} 481 | */ 482 | unmute() { 483 | // Stop the current running fade. 484 | this.fadeStop(); 485 | 486 | // Reset the gain node's value back to volume. 487 | this._gainNode.gain.setValueAtTime(this._volume, this._context.currentTime); 488 | 489 | // Set the muted property to false. 490 | this._muted = false; 491 | 492 | return this; 493 | } 494 | 495 | /** 496 | * Gets/sets the volume. 497 | * @param {number} [vol] Should be from 0.0 to 1.0. 498 | * @return {Sound|number} 499 | */ 500 | volume(vol) { 501 | // If no input parameter is passed then return the volume. 502 | if (typeof vol === 'undefined') { 503 | return this._volume; 504 | } 505 | 506 | // Stop the current running fade. 507 | this.fadeStop(); 508 | 509 | // Set the gain's value to the passed volume. 510 | this._gainNode.gain.setValueAtTime(this._muted ? 0 : vol, this._context.currentTime); 511 | 512 | // Set the volume to the property. 513 | this._volume = vol; 514 | 515 | return this; 516 | } 517 | 518 | /** 519 | * Fades the sound volume to the passed value in the passed duration. 520 | * @param {number} to The destination volume. 521 | * @param {number} duration The period of fade. 522 | * @param {string} [type = linear] The fade type (linear or exponential). 523 | * @return {Sound} 524 | */ 525 | fade(to, duration, type = 'linear') { 526 | // If a fade is already running stop it. 527 | if (this._fading) { 528 | this.fadeStop(); 529 | } 530 | 531 | this._fading = true; 532 | 533 | if (type === 'linear') { 534 | this._gainNode.gain.linearRampToValueAtTime(to, this._context.currentTime + duration); 535 | } else { 536 | this._gainNode.gain.exponentialRampToValueAtTime(to, this._context.currentTime + duration); 537 | } 538 | 539 | this._fadeTimer = workerTimer.setTimeout(() => { 540 | this.volume(to); 541 | 542 | workerTimer.clearTimeout(this._fadeTimer); 543 | 544 | this._fadeTimer = null; 545 | this._fading = false; 546 | 547 | this._fadeEndCallback && this._fadeEndCallback(this); 548 | }, duration * 1000); 549 | 550 | return this; 551 | } 552 | 553 | /** 554 | * Stops the current running fade. 555 | * @return {Sound} 556 | */ 557 | fadeStop() { 558 | if (!this._fading) { 559 | return this; 560 | } 561 | 562 | this._gainNode.gain.cancelScheduledValues(this._context.currentTime); 563 | 564 | if (this._fadeTimer) { 565 | workerTimer.clearTimeout(this._fadeTimer); 566 | this._fadeTimer = null; 567 | } 568 | 569 | this._fading = false; 570 | this.volume(this._gainNode.gain.value); 571 | 572 | return this; 573 | } 574 | 575 | /** 576 | * Gets/sets the playback rate. 577 | * @param {number} [rate] The playback rate. Should be from 0.5 to 5. 578 | * @return {Sound|number} 579 | */ 580 | rate(rate) { 581 | // If no input parameter is passed return the current rate. 582 | if (typeof rate === 'undefined') { 583 | return this._rate; 584 | } 585 | 586 | this._rate = rate; 587 | this._rateSeek = this.seek(); 588 | 589 | if (this.isPlaying()) { 590 | if (this._stream) { 591 | this._audio.playbackRate = rate; 592 | 593 | if (this._isSprite) { 594 | this._clearEndTimer(); 595 | let [, duration] = this._getTimeVars(); 596 | this._endTimer = workerTimer.setTimeout(this._onHtml5Ended, (duration * 1000) / Math.abs(rate)); 597 | } 598 | } else { 599 | this._startTime = this._context.currentTime; 600 | this._bufferSourceNode && (this._bufferSourceNode.playbackRate.setValueAtTime(rate, this._context.currentTime)); 601 | } 602 | } 603 | 604 | return this; 605 | } 606 | 607 | /** 608 | * Gets/sets the seek position. 609 | * @param {number} [seek] The seek position. 610 | * @return {Sound|number} 611 | */ 612 | seek(seek) { 613 | // If no parameter is passed return the current position. 614 | if (typeof seek === 'undefined') { 615 | if (this._stream) { 616 | return this._audio ? this._audio.currentTime : null; 617 | } 618 | 619 | const realTime = this.isPlaying() ? this._context.currentTime - this._startTime : 0; 620 | const rateElapsed = this._rateSeek ? this._rateSeek - this._currentPos : 0; 621 | return this._currentPos + (rateElapsed + realTime * this._rate); 622 | } 623 | 624 | // If seeking outside the borders then return. 625 | if (seek < this._startPos || seek > this._endPos) { 626 | return this; 627 | } 628 | 629 | // If the sound is currently playing... pause it, set the seek position and then continue playing. 630 | const isPlaying = this.isPlaying(); 631 | 632 | if (isPlaying) { 633 | this.pause(); 634 | } 635 | 636 | this._currentPos = seek; 637 | 638 | if (isPlaying) { 639 | this.play(); 640 | } 641 | 642 | return this; 643 | } 644 | 645 | /** 646 | * Gets/sets the loop parameter of the sound. 647 | * @param {boolean} [loop] True to loop the sound. 648 | * @return {Sound/boolean} 649 | */ 650 | loop(loop) { 651 | if (typeof loop !== 'boolean') { 652 | return this._loop; 653 | } 654 | 655 | this._loop = loop; 656 | this._setLoop(loop); 657 | 658 | return this; 659 | } 660 | 661 | /** 662 | * Destroys the dependencies and release the memory. 663 | * @return {Sound} 664 | */ 665 | destroy() { 666 | // If the sound is already destroyed return. 667 | if (this._state === SoundState.Destroyed) { 668 | return this; 669 | } 670 | 671 | // Stop the sound. 672 | this.stop(); 673 | 674 | // Destroy the audio node and media element audio source node. 675 | this._destroyAudio(); 676 | this._destroyMediaSourceNode(); 677 | 678 | // Disconnect from the master gain. 679 | this._gainNode && this._gainNode.disconnect(); 680 | 681 | this._buffer = null; 682 | this._context = null; 683 | this._gainNode = null; 684 | 685 | // Set the state to "destroyed". 686 | this._state = SoundState.Destroyed; 687 | 688 | this._destroyCallback && this._destroyCallback(this); 689 | 690 | return this; 691 | } 692 | 693 | /** 694 | * Returns the unique id of the sound. 695 | * @return {number} 696 | */ 697 | id() { 698 | return this._id; 699 | } 700 | 701 | /** 702 | * Returns whether the sound is muted or not. 703 | * @return {boolean} 704 | */ 705 | muted() { 706 | return this._muted; 707 | } 708 | 709 | /** 710 | * Returns the state of the sound. 711 | * @return {SoundState} 712 | */ 713 | state() { 714 | return this._state; 715 | } 716 | 717 | /** 718 | * Returns the total duration of the playback. 719 | * @return {number} 720 | */ 721 | duration() { 722 | return this._duration; 723 | } 724 | 725 | /** 726 | * Returns true if the buzz is playing. 727 | * @return {boolean} 728 | */ 729 | isPlaying() { 730 | return this._state === SoundState.Playing; 731 | } 732 | 733 | /** 734 | * Returns true if buzz is paused. 735 | * @return {boolean} 736 | */ 737 | isPaused() { 738 | return this._state === SoundState.Paused; 739 | } 740 | 741 | /** 742 | * Returns last played time. 743 | * @return {number} 744 | */ 745 | lastPlayed() { 746 | return this._lastPlayed; 747 | } 748 | 749 | /** 750 | * Disables auto-destroy. 751 | */ 752 | persist() { 753 | if (this._state === SoundState.Destroyed) { 754 | return; 755 | } 756 | 757 | this._persist = true; 758 | } 759 | 760 | /** 761 | * Enables auto-destroy. 762 | */ 763 | abandon() { 764 | if (this._state === SoundState.Destroyed) { 765 | return; 766 | } 767 | 768 | this._persist = false; 769 | } 770 | 771 | /** 772 | * Returns true if auto-destroy enabled. 773 | * @return {boolean} 774 | */ 775 | isPersistent() { 776 | return this._persist; 777 | } 778 | 779 | /** 780 | * Returns true if the audio can play without delay. 781 | * @return {boolean} 782 | */ 783 | canPlay() { 784 | return this._audio ? this._audio.readyState >= 3 : false; 785 | } 786 | 787 | /** 788 | * HTML5 Audio error handler. 789 | * @param {object} err Error object. 790 | * @private 791 | */ 792 | _onAudioError(err) { 793 | this._audioErrorCallback && this._audioErrorCallback(this, err); 794 | } 795 | 796 | /** 797 | * Returns the seek, duration and timeout for the playback. 798 | * @return {[number, number, number]} 799 | * @private 800 | */ 801 | _getTimeVars() { 802 | let seek = Math.max(0, this._currentPos > 0 ? this._currentPos : this._startPos), 803 | duration = this._endPos - this._startPos, 804 | timeout = (duration * 1000) / this._rate; 805 | 806 | return [seek, duration, timeout]; 807 | } 808 | 809 | /** 810 | * Plays the audio using audio buffer. 811 | * @private 812 | */ 813 | _playBuffer() { 814 | let [seek, duration] = this._getTimeVars(); 815 | 816 | // Create a new buffersourcenode to play the sound. 817 | this._bufferSourceNode = this._context.createBufferSource(); 818 | 819 | // Set the buffer, playback rate and loop parameters 820 | this._bufferSourceNode.buffer = this._buffer; 821 | this._bufferSourceNode.playbackRate.setValueAtTime(this._rate, this._context.currentTime); 822 | this._setLoop(this._loop); 823 | 824 | // Connect the node to the audio graph. 825 | this._bufferSourceNode.connect(this._gainNode); 826 | 827 | // Listen to the "ended" event to reset/clean things. 828 | this._bufferSourceNode.addEventListener('ended', this._onBufferEnded); 829 | 830 | const startTime = this._context.currentTime; 831 | 832 | // Call the supported method to play the sound. 833 | if (typeof this._bufferSourceNode.start !== 'undefined') { 834 | this._bufferSourceNode.start(startTime, seek, this._loop ? undefined : duration); 835 | } else { 836 | this._bufferSourceNode.noteGrainOn(startTime, seek, this._loop ? undefined : duration); 837 | } 838 | } 839 | 840 | /** 841 | * Plays the audio using HTML5 audio object. 842 | * @private 843 | */ 844 | _playHtml5() { 845 | let [seek, , timeout] = this._getTimeVars(); 846 | 847 | this._audio.currentTime = seek; 848 | 849 | if (this._isSprite) { 850 | this._endTimer = workerTimer.setTimeout(this._onHtml5Ended, timeout); 851 | } else { 852 | this._audio.addEventListener('ended', this._onHtml5Ended); 853 | } 854 | 855 | this._audio.play(); 856 | } 857 | 858 | /** 859 | * Callback that is invoked after the buffer playback is ended. 860 | * @private 861 | */ 862 | _onBufferEnded() { 863 | this._lastPlayed = new Date(); 864 | 865 | // Reset the seek positions 866 | this._currentPos = 0; 867 | this._rateSeek = 0; 868 | 869 | // Destroy the node (AudioBufferSourceNodes are one-time use and throw objects). 870 | this._destroyBufferNode(); 871 | 872 | // Reset the state to allow future actions. 873 | this._state = SoundState.Ready; 874 | 875 | // Invoke the callback if there is one. 876 | this._playEndCallback && this._playEndCallback(this); 877 | } 878 | 879 | /** 880 | * Callback that is invoked after the html audio playback is ended. 881 | * @private 882 | */ 883 | _onHtml5Ended() { 884 | if (this._loop) { 885 | this.stop().play(); 886 | } else { 887 | this.stop(); 888 | this._state = SoundState.Ready; 889 | this._playEndCallback && this._playEndCallback(this); 890 | } 891 | } 892 | 893 | /** 894 | * Clears the end-timer. 895 | * @private 896 | */ 897 | _clearEndTimer() { 898 | if (!this._endTimer) { 899 | return; 900 | } 901 | 902 | workerTimer.clearTimeout(this._endTimer); 903 | this._endTimer = null; 904 | } 905 | 906 | /** 907 | * Event handler for audio's "canplaythrough" event. 908 | * @private 909 | */ 910 | _onCanPlayThrough() { 911 | this._loadCallback(); 912 | this._audio.removeEventListener('canplaythrough', this._onCanPlayThrough); 913 | }; 914 | 915 | /** 916 | * Returns the gain node. 917 | * @return {GainNode} 918 | */ 919 | _gain() { 920 | return this._gainNode; 921 | } 922 | 923 | /** 924 | * Stops the playing buffer source node and destroys it. 925 | * @private 926 | */ 927 | _destroyBufferNode() { 928 | if (!this._bufferSourceNode) { 929 | return; 930 | } 931 | 932 | if (typeof this._bufferSourceNode.stop !== 'undefined') { 933 | this._bufferSourceNode.stop(); 934 | } else { 935 | this._bufferSourceNode.noteGrainOff(); 936 | } 937 | 938 | this._bufferSourceNode.disconnect(); 939 | this._bufferSourceNode.removeEventListener('ended', this._onBufferEnded); 940 | this._bufferSourceNode = null; 941 | } 942 | 943 | /** 944 | * Destroys the media audio source node. 945 | * @private 946 | */ 947 | _destroyMediaSourceNode() { 948 | if (!this._mediaElementAudioSourceNode) { 949 | return; 950 | } 951 | 952 | this._mediaElementAudioSourceNode.disconnect(); 953 | this._mediaElementAudioSourceNode = null; 954 | } 955 | 956 | /** 957 | * Destroys the passed audio node. 958 | * @private 959 | */ 960 | _destroyAudio() { 961 | if (!this._audio) { 962 | return; 963 | } 964 | 965 | this._audio.removeEventListener('canplaythrough', this._onCanPlayThrough); 966 | this._audio.removeEventListener('error', this._onAudioError); 967 | this._audio.pause(); 968 | utility.isIE() && (this._audio.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'); 969 | this._audio.onerror = null; 970 | this._audio.onend = null; 971 | this._audio.canplaythrough = null; 972 | this._audio = null; 973 | } 974 | 975 | /** 976 | * Sets the sound to play repeatedly or not. 977 | * @param {boolean} loop True to play the sound repeatedly. 978 | * @private 979 | */ 980 | _setLoop(loop) { 981 | if (!this._sourceExists) { 982 | return; 983 | } 984 | 985 | if (this._stream) { 986 | this._audio.loop = loop; 987 | } else { 988 | this._bufferSourceNode.loop = loop; 989 | 990 | if (loop) { 991 | this._bufferSourceNode.loopStart = this._startPos; 992 | this._bufferSourceNode.loopEnd = this._endPos; 993 | } 994 | } 995 | } 996 | } 997 | 998 | export { Sound as default, SoundState }; 999 | -------------------------------------------------------------------------------- /src/Buzz.js: -------------------------------------------------------------------------------- 1 | import engine, { EngineEvents, EngineState, ErrorType } from './Engine'; 2 | import Queue from './Queue'; 3 | import utility from './Utility'; 4 | import emitter from './Emitter'; 5 | import Sound from './Sound'; 6 | import DownloadStatus from './DownloadStatus'; 7 | import LoadState from './LoadState'; 8 | 9 | /** 10 | * Enum that represents the different states of a buzz (sound group). 11 | * @enum {string} 12 | */ 13 | const BuzzState = { 14 | Ready: 'ready', 15 | Destroyed: 'destroyed' 16 | }; 17 | 18 | /** 19 | * Enum that represents the different events fired by a buzz. 20 | * @enum {string} 21 | */ 22 | const BuzzEvents = { 23 | Load: 'load', 24 | LoadProgress: 'loadprogress', 25 | UnLoad: 'unload', 26 | PlayStart: 'playstart', 27 | PlayEnd: 'playend', 28 | Pause: 'pause', 29 | Stop: 'stop', 30 | Volume: 'volume', 31 | Mute: 'mute', 32 | Seek: 'seek', 33 | Rate: 'rate', 34 | FadeStart: 'fadestart', 35 | FadeEnd: 'fadeend', 36 | FadeStop: 'fadestop', 37 | Error: 'error', 38 | Destroy: 'destroy' 39 | }; 40 | 41 | /** 42 | * A wrapper class that simplifies managing multiple sounds created for a single source. 43 | */ 44 | class Buzz { 45 | 46 | /** 47 | * Unique id. 48 | * @type {number} 49 | * @private 50 | */ 51 | _id = -1; 52 | 53 | /** 54 | * Represents the source of the sound. The source can be an url or base64 string. 55 | * @type {*} 56 | * @private 57 | */ 58 | _src = null; 59 | 60 | /** 61 | * The formats of the passed audio sources. 62 | * @type {Array} 63 | * @private 64 | */ 65 | _format = []; 66 | 67 | /** 68 | * The sprite definition. 69 | * @type {object} 70 | * @private 71 | */ 72 | _sprite = null; 73 | 74 | /** 75 | * True to use HTML5 audio node. 76 | * @type {boolean} 77 | * @private 78 | */ 79 | _stream = false; 80 | 81 | /** 82 | * The current volume of the sound. Should be from 0.0 to 1.0. 83 | * @type {number} 84 | * @private 85 | */ 86 | _volume = 1.0; 87 | 88 | /** 89 | * The current rate of the playback. Should be from 0.5 to 5. 90 | * @type {number} 91 | * @private 92 | */ 93 | _rate = 1; 94 | 95 | /** 96 | * True if the sound is currently muted. 97 | * @type {boolean} 98 | * @private 99 | */ 100 | _muted = false; 101 | 102 | /** 103 | * True if the sound should play repeatedly. 104 | * @type {boolean} 105 | * @private 106 | */ 107 | _loop = false; 108 | 109 | /** 110 | * True to pre-loaded the sound on construction. 111 | * @type {boolean} 112 | * @private 113 | */ 114 | _preload = false; 115 | 116 | /** 117 | * True to auto-play the sound on construction. 118 | * @type {boolean} 119 | * @private 120 | */ 121 | _autoplay = false; 122 | 123 | /** 124 | * Duration of the playback in seconds. 125 | * @type {number} 126 | * @private 127 | */ 128 | _duration = 0; 129 | 130 | /** 131 | * The best compatible source in the audio sources passed. 132 | * @type {string|null} 133 | * @private 134 | */ 135 | _compatibleSrc = null; 136 | 137 | /** 138 | * Represents the different states that occurs while loading the sound. 139 | * @type {LoadState} 140 | * @private 141 | */ 142 | _loadState = LoadState.NotLoaded; 143 | 144 | /** 145 | * Represents the state of this group. 146 | * @type {BuzzState} 147 | * @private 148 | */ 149 | _state = BuzzState.Ready; 150 | 151 | /** 152 | * True if the group is currently fading. 153 | * @type {boolean} 154 | * @private 155 | */ 156 | _fading = false; 157 | 158 | /** 159 | * The timer that runs function after the fading is complete. 160 | * @type {number|null} 161 | * @private 162 | */ 163 | _fadeTimer = null; 164 | 165 | /** 166 | * Number of audio resource loading calls in progress. 167 | * @type {number} 168 | * @private 169 | */ 170 | _noOfLoadCalls = 0; 171 | 172 | /** 173 | * Array of sounds belongs to this group. 174 | * @type {Array} 175 | * @private 176 | */ 177 | _soundsArray = []; 178 | 179 | /** 180 | * Array of pre-loaded HTML5 audio elements. 181 | * @type {Array