├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build ├── minify.js ├── project.config.js ├── rollup.config.esm.js ├── rollup.config.prod.js └── rollup.config.test.js ├── coffeelint.json ├── demo ├── index.html ├── style.css └── waveform.html ├── dist ├── wavebell.esm.js ├── wavebell.esm.js.map ├── wavebell.js ├── wavebell.js.map ├── wavebell.min.js └── wavebell.min.js.map ├── lib ├── media │ ├── audio_filter.js │ ├── recorder.js │ ├── user_media.js │ └── volume_meter.js ├── util │ ├── assert.js │ ├── emitter.js │ └── props.js └── wavebell.js ├── package.json ├── test ├── README.md ├── bootstrap.js ├── index.html ├── launch.js └── unit │ ├── assert.spec.coffee │ ├── emitter.spec.coffee │ └── props.spec.coffee └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "browsers": ["last 2 versions", "ie 10"] 6 | }, 7 | "modules": false 8 | }] 9 | ], 10 | "plugins": [ 11 | "external-helpers" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "env": { 4 | "browser": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # nyc test coverage 15 | .nyc_output 16 | 17 | # Dependency directories 18 | node_modules/ 19 | 20 | # Optional npm cache directory 21 | .npm 22 | 23 | # Optional eslint cache 24 | .eslintcache 25 | 26 | # Optional REPL history 27 | .node_repl_history 28 | 29 | # Output of 'npm pack' 30 | *.tgz 31 | 32 | # Yarn Integrity file 33 | .yarn-integrity 34 | 35 | # Generated test file 36 | test_gen/ 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | dist: trusty 6 | addons: 7 | chrome: stable 8 | notifications: 9 | email: 10 | on_success: never 11 | on_failure: change 12 | env: 13 | - NODE_ENV=testing 14 | install: 15 | - yarn install 16 | before_script: 17 | - export DISPLAY=:99.0 18 | - export CHROME_PATH="$(pwd)/chrome-linux/chrome" 19 | - sh -e /etc/init.d/xvfb start 20 | - sleep 3 # wait for xvfb to boot 21 | script: 22 | - yarn run test 23 | after_success: 24 | - yarn run coverage 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Skyler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wavebell 2 | 3 | [![Build Status](https://travis-ci.org/skylerlee/wavebell.svg?branch=master)](https://travis-ci.org/skylerlee/wavebell) 4 | [![Coverage Status](https://coveralls.io/repos/github/skylerlee/wavebell/badge.svg?branch=master)](https://coveralls.io/github/skylerlee/wavebell) 5 | [![npm](https://img.shields.io/npm/v/wavebell.svg)](https://www.npmjs.com/package/wavebell) 6 | 7 | Catch realtime audio wave from microphone with JavaScript! 8 | 9 | ## Screenshot 10 | ![wavebell](https://user-images.githubusercontent.com/6789491/33554578-3d9067de-d938-11e7-8946-4dd6870d4495.gif) 11 | ![waveform](https://user-images.githubusercontent.com/6789491/33908149-63942ce6-dfc2-11e7-94ca-a2023943a9ab.gif) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | # Install with npm 17 | npm install --save wavebell 18 | # Install with yarn 19 | yarn add wavebell 20 | ``` 21 | 22 | ## Example 23 | 24 | ```javascript 25 | var bell = new WaveBell(); 26 | 27 | bell.on('wave', function (e) { 28 | // draw oscilloscope 29 | drawColumn(e.value); 30 | }); 31 | 32 | bell.on('stop', function () { 33 | var blob = bell.result; 34 | // play recorded audio 35 | playback(URL.createObjectURL(blob)); 36 | }); 37 | 38 | // 25 frames per second 39 | bell.start(1000 / 25); 40 | ``` 41 | 42 | ## Notice 43 | In Chrome 47 or above, `getUserMedia` requires HTTPS to work. 44 | So it'd be better to setup SSL for your server. 45 | 46 | ## Thanks 47 | * **Mozilla web docs** [visualizations with web audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API) 48 | * **Jos Dirksen** for his [great blog post about audio visualization](http://www.smartjava.org/content/exploring-html5-web-audio-visualizing-sound) 49 | 50 | ## License 51 | The MIT License. 52 | -------------------------------------------------------------------------------- /build/minify.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fs from 'fs-extra' 4 | import path from 'path' 5 | import uglify from 'uglify-js' 6 | 7 | const defaultOptions = { 8 | output: { 9 | ascii_only: true 10 | } 11 | } 12 | 13 | function suffixed (name, suffix) { 14 | let parts = path.parse(name) 15 | let filename = parts.name + suffix + parts.ext 16 | return path.join(parts.dir, filename) 17 | } 18 | 19 | function createFileWrap (name, content) { 20 | let file = {} 21 | file[name] = content 22 | return file 23 | } 24 | 25 | /** 26 | * rollup minify plugin 27 | * This plugin minifies the input file using uglify-js. It's created as a helper 28 | * to generate a minified bundle without touch the rollup env config. 29 | * @param {string} input - the file to minify 30 | * @param {object} option - minify option 31 | */ 32 | export default function minify (input, option = {}) { 33 | let minFile = suffixed(input, '.min') 34 | let mapFile = minFile + '.map' 35 | if (option.sourceMap) { 36 | defaultOptions.sourceMap = { 37 | filename: path.basename(minFile), 38 | url: path.basename(mapFile) 39 | } 40 | } 41 | return { 42 | name: 'minify', 43 | // hook onwrite phase 44 | onwrite () { 45 | fs.read(input).then(source => { 46 | let file = createFileWrap(path.basename(input), source) 47 | return uglify.minify(file, defaultOptions) 48 | }).then(minified => { 49 | fs.write(minFile, minified.code) 50 | if (minified.map) { 51 | fs.write(mapFile, minified.map) 52 | } 53 | }) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /build/project.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import path from 'path' 4 | import rollupAlias from 'rollup-plugin-alias' 5 | 6 | function suffixed (name, suffix) { 7 | let parts = path.parse(name) 8 | let filename = parts.name + suffix + parts.ext 9 | return path.join(parts.dir, filename) 10 | } 11 | 12 | let conf = { 13 | main: 'lib/wavebell.js', 14 | dest: 'dist/wavebell.js' 15 | } 16 | 17 | let alias = () => rollupAlias({ 18 | '@': path.resolve(__dirname, '../lib') 19 | }) 20 | 21 | export { 22 | suffixed, 23 | conf, 24 | alias 25 | } 26 | -------------------------------------------------------------------------------- /build/rollup.config.esm.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { suffixed, conf, alias } from './project.config' 4 | 5 | export default { 6 | input: conf.main, 7 | output: { 8 | file: suffixed(conf.dest, '.esm'), 9 | name: 'WaveBell', 10 | format: 'es', 11 | sourcemap: true 12 | }, 13 | plugins: [ 14 | alias() 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /build/rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { conf, alias } from './project.config' 4 | import minify from './minify' 5 | import babel from 'rollup-plugin-babel' 6 | 7 | export default { 8 | input: conf.main, 9 | output: { 10 | file: conf.dest, 11 | name: 'WaveBell', 12 | format: 'umd', 13 | sourcemap: true 14 | }, 15 | plugins: [ 16 | alias(), 17 | babel(), 18 | minify(conf.dest, { 19 | sourceMap: true 20 | }) 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /build/rollup.config.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { alias } from './project.config' 4 | import multiEntry from 'rollup-plugin-multi-entry' 5 | import replace from 'rollup-plugin-replace' 6 | import coffee from 'rollup-plugin-coffee-script' 7 | import babel from 'rollup-plugin-babel' 8 | import istanbul from 'rollup-plugin-istanbul' 9 | 10 | const replaceConfig = { 11 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 12 | } 13 | const istanbulConfig = { 14 | exclude: ['./test/**/*'] 15 | } 16 | 17 | export default { 18 | input: [ 19 | './test/bootstrap.js', 20 | './test/**/*.spec.coffee' 21 | ], 22 | output: { 23 | file: './test_gen/specs.bundle.js', 24 | name: 'specs', 25 | format: 'umd', 26 | sourcemap: true 27 | }, 28 | plugins: [ 29 | multiEntry(), 30 | alias(), 31 | replace(replaceConfig), 32 | coffee(), 33 | babel(), 34 | istanbul(istanbulConfig) 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WaveBell Demo 6 | 7 | 29 | 30 | 31 |
32 |

Click button to start recorder

33 | 34 | 35 |
36 |
37 |
38 | More demos: 39 | 42 |
43 | 44 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /demo/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Demo page stylesheet 3 | */ 4 | 5 | body { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 7 | font-size: 16px; 8 | background-color: #3c4242; 9 | color: #f0f0f0; 10 | } 11 | 12 | a { 13 | color: #f0f0f0; 14 | } 15 | 16 | .container { 17 | width: 500px; 18 | margin: 5em auto; 19 | } 20 | 21 | .btn { 22 | background-color: #ccc; 23 | color: #000; 24 | font-size: inherit; 25 | line-height: 1.2; 26 | min-width: 5em; 27 | padding: 0.25em 0.5em; 28 | border: none; 29 | border-radius: 3px; 30 | outline: none; 31 | } 32 | 33 | .label { 34 | margin: 0 1em; 35 | } 36 | -------------------------------------------------------------------------------- /demo/waveform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WaveBell Demo 6 | 7 | 13 | 14 | 15 |
16 |

Draw audio waveform

17 | 18 | 19 |
20 | 21 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /dist/wavebell.esm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | function slice (args, from) { 8 | return Array.prototype.slice.call(args, from) 9 | } 10 | 11 | class Emitter { 12 | constructor () { 13 | this.handlerMap = {}; 14 | } 15 | 16 | on (event, handler) { 17 | if (typeof event !== 'string') { 18 | throw new TypeError(event + ' is not a string') 19 | } 20 | if (typeof handler !== 'function') { 21 | throw new TypeError(handler + ' is not a function') 22 | } 23 | let map = this.handlerMap; 24 | let handlers = map[event] = map[event] || []; 25 | let i = handlers.indexOf(handler); 26 | if (i === -1) { 27 | handlers.push(handler); 28 | } 29 | return this 30 | } 31 | 32 | off (event, handler) { 33 | if (handler === undefined) { 34 | // remove all handlers 35 | delete this.handlerMap[event]; 36 | return this 37 | } 38 | // remove registered handler 39 | let handlers = this.handlerMap[event]; 40 | if (handlers) { 41 | let i = handlers.indexOf(handler); 42 | if (i >= 0) { 43 | handlers.splice(i, 1); 44 | } 45 | // cleanup empty handlers 46 | if (handlers.length === 0) { 47 | this.off(event); 48 | } 49 | } 50 | return this 51 | } 52 | 53 | emit (event) { 54 | let args = slice(arguments, 1); 55 | let handlers = this.handlerMap[event]; 56 | if (handlers) { 57 | for (let i = 0, len = handlers.length; i < len; i++) { 58 | handlers[i].apply(undefined, args); 59 | } 60 | } 61 | return this 62 | } 63 | } 64 | 65 | /* 66 | * Copyright (C) 2017, Skyler. 67 | * Use of this source code is governed by the MIT license that can be 68 | * found in the LICENSE file. 69 | */ 70 | 71 | class Assertion { 72 | constructor (value) { 73 | this.value = value; 74 | this._negative = false; 75 | this._error = new Error('Assertion failed'); 76 | } 77 | 78 | get to () { 79 | return this 80 | } 81 | 82 | get not () { 83 | this._negative = !this._negative; 84 | return this 85 | } 86 | 87 | that (error) { 88 | this._error = error; 89 | return this 90 | } 91 | 92 | equal (value) { 93 | if ((value === this.value) === this._negative) { 94 | throw this._error 95 | } 96 | } 97 | } 98 | 99 | function assert (value) { 100 | return new Assertion(value) 101 | } 102 | 103 | /* 104 | * Copyright (C) 2017, Skyler. 105 | * Use of this source code is governed by the MIT license that can be 106 | * found in the LICENSE file. 107 | */ 108 | 109 | const AudioContext = window.AudioContext || window.webkitAudioContext; 110 | 111 | // AudioContext singleton shared by filters 112 | let audioContext = null; 113 | 114 | class AudioFilter { 115 | /** 116 | * Get AudioContext instance 117 | * @returns {AudioContext} - Shared instance 118 | */ 119 | get context () { 120 | if (!audioContext) { 121 | audioContext = new AudioContext(); 122 | } 123 | return audioContext 124 | } 125 | } 126 | 127 | /* 128 | * Copyright (C) 2017, Skyler. 129 | * Use of this source code is governed by the MIT license that can be 130 | * found in the LICENSE file. 131 | */ 132 | 133 | class VolumeMeter extends AudioFilter { 134 | constructor (mainbus, options) { 135 | super(); 136 | this.mainbus = mainbus; 137 | this.options = Object.assign({ 138 | minLimit: 0, 139 | maxLimit: 128, 140 | fftSize: 1024, 141 | smoothing: 0.3 142 | }, options); 143 | this._checkOptions(this.options); 144 | this._cache = null; 145 | this.source = null; 146 | this.analyser = this._initAnalyser(this.options); 147 | } 148 | 149 | _checkOptions (options) { 150 | if (options.maxLimit <= options.minLimit) { 151 | throw new RangeError('Wrong limit range for volume') 152 | } 153 | } 154 | 155 | _initAnalyser (options) { 156 | // init analyser from options 157 | /// ref: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode 158 | let analyser = this.context.createAnalyser(); 159 | analyser.fftSize = options.fftSize; 160 | analyser.smoothingTimeConstant = options.smoothing; 161 | // process data when available 162 | this.mainbus.on('dataavailable', e => this._processData()); 163 | return analyser 164 | } 165 | 166 | pipe (stream) { 167 | // connect stream pipe 168 | this.source = this.context.createMediaStreamSource(stream); 169 | this.source.connect(this.analyser); 170 | } 171 | 172 | cutoff () { 173 | this.source.disconnect(this.analyser); 174 | this.source = null; 175 | } 176 | 177 | _processData () { 178 | // half of the fftSize 179 | if (!this._cache) { 180 | this._cache = new Uint8Array(this.analyser.frequencyBinCount); 181 | } 182 | this.analyser.getByteFrequencyData(this._cache); 183 | let volume = this._calcAvgVolume(this._cache); 184 | this.mainbus.emit('wave', { 185 | value: this._alignVolume(volume) 186 | }); 187 | } 188 | 189 | _alignVolume (volume) { 190 | let opts = this.options; 191 | if (volume < opts.minLimit) { 192 | volume = opts.minLimit; 193 | } 194 | if (volume > opts.maxLimit) { 195 | volume = opts.maxLimit; 196 | } 197 | return (volume - opts.minLimit) / (opts.maxLimit - opts.minLimit) 198 | } 199 | 200 | _calcAvgVolume (data) { 201 | let sum = 0; 202 | let len = data.length; 203 | for (let i = 0; i < len; i++) { 204 | sum += data[i]; 205 | } 206 | return sum / len 207 | } 208 | } 209 | 210 | /* 211 | * Copyright (C) 2017, Skyler. 212 | * Use of this source code is governed by the MIT license that can be 213 | * found in the LICENSE file. 214 | */ 215 | 216 | const hasOwn = Object.prototype.hasOwnProperty; 217 | 218 | class PropPath { 219 | constructor (path) { 220 | this.steps = path.split('.'); 221 | this.fallback = undefined; 222 | } 223 | 224 | travel (target, fn) { 225 | if (typeof fn !== 'function') { 226 | throw new TypeError(fn + ' is not a function') 227 | } 228 | let len = this.steps.length; 229 | let i = 0; 230 | let step = this.steps[i]; 231 | while (fn(target, step) && i < len) { 232 | target = target[step]; 233 | step = this.steps[++i]; 234 | } 235 | return { 236 | step: i, 237 | value: target 238 | } 239 | } 240 | 241 | or (defaultValue) { 242 | this.fallback = defaultValue; 243 | return this 244 | } 245 | 246 | from (obj) { 247 | let ret = this.travel(obj, (target, step) => { 248 | return target != null && step in Object(target) 249 | }); 250 | if (ret.step === this.steps.length) { 251 | return ret.value 252 | } else { 253 | return this.fallback 254 | } 255 | } 256 | 257 | hadBy (obj) { 258 | let ret = this.travel(obj, (target, step) => { 259 | return target != null && step in Object(target) 260 | }); 261 | return ret.step === this.steps.length 262 | } 263 | 264 | ownedBy (obj) { 265 | let ret = this.travel(obj, (target, step) => { 266 | return target != null && hasOwn.call(target, step) 267 | }); 268 | return ret.step === this.steps.length 269 | } 270 | } 271 | 272 | function props (path) { 273 | if (typeof path !== 'string') { 274 | throw new TypeError(path + ' is not a string') 275 | } 276 | return new PropPath(path) 277 | } 278 | 279 | /* 280 | * Copyright (C) 2017, Skyler. 281 | * Use of this source code is governed by the MIT license that can be 282 | * found in the LICENSE file. 283 | */ 284 | 285 | /** 286 | * Shim for MediaDevices#getUserMedia method 287 | * @param {object} constraints - The user media constraints 288 | */ 289 | function getUserMedia (constraints) { 290 | if (props('navigator.mediaDevices.getUserMedia').hadBy(window)) { 291 | let medias = props('navigator.mediaDevices').from(window); 292 | return medias.getUserMedia(constraints) 293 | } 294 | let userMediaGetter = navigator.getUserMedia || 295 | navigator.webkitGetUserMedia || 296 | navigator.mozGetUserMedia || 297 | navigator.msGetUserMedia; 298 | if (!userMediaGetter) { 299 | throw new Error('getUserMedia is not supported by this browser') 300 | } 301 | return new Promise((resolve, reject) => { 302 | userMediaGetter(constraints, resolve, reject); 303 | }) 304 | } 305 | 306 | /** 307 | * Access audio input from microphone device 308 | */ 309 | function getUserMicrophone () { 310 | return getUserMedia({ audio: true, video: false }) 311 | } 312 | 313 | /* 314 | * Copyright (C) 2017, Skyler. 315 | * Use of this source code is governed by the MIT license that can be 316 | * found in the LICENSE file. 317 | */ 318 | 319 | function buildError (callee, that) { 320 | return new Error(`Failed to execute '${callee}' on 'Recorder'` + 321 | (that ? `:\nThe Recorder's state is '${that.state}'.` : '')) 322 | } 323 | 324 | class Recorder extends Emitter { 325 | constructor (options) { 326 | super(); 327 | this.options = Object.assign({ 328 | mimeType: 'audio/webm', 329 | audioBitsPerSecond: 96000 330 | }, options); 331 | this._intern = null; 332 | this._result = null; 333 | this._filter = new VolumeMeter(this, this.options.meter); 334 | } 335 | 336 | get state () { 337 | if (this._intern === null) { 338 | return 'inactive' 339 | } else { 340 | return this._intern.state 341 | } 342 | } 343 | 344 | get ready () { 345 | return this._intern !== null 346 | } 347 | 348 | get result () { 349 | if (!this._result) { 350 | return null 351 | } 352 | return new Blob(this._result, { 353 | type: this.options.mimeType 354 | }) 355 | } 356 | 357 | open () { 358 | assert(this.ready).that(buildError('open')).to.equal(false); 359 | return getUserMicrophone().then(stream => { 360 | // create internal recorder 361 | this._intern = new MediaRecorder(stream, this.options); 362 | // register event listeners 363 | let eventTypes = ['error', 'pause', 'resume', 'start', 'stop']; 364 | eventTypes.map(type => { 365 | this._intern.addEventListener(type, e => this.emit(type, e)); 366 | }); 367 | this._intern.addEventListener('dataavailable', e => { 368 | this._result.push(e.data); 369 | this.emit('dataavailable', e); 370 | }); 371 | // pipe stream to filter 372 | this._filter.pipe(stream); 373 | }) 374 | } 375 | 376 | close () { 377 | assert(this.ready).that(buildError('close')).to.equal(true); 378 | // close all stream tracks 379 | let tracks = this._intern.stream.getTracks(); 380 | for (let i = 0; i < tracks.length; i++) { 381 | tracks[i].stop(); 382 | } 383 | // close stream filter 384 | this._filter.cutoff(); 385 | this._intern = null; 386 | } 387 | 388 | start (timeslice) { 389 | assert(this.state).that(buildError('start', this)).to.equal('inactive'); 390 | // init result data on every start 391 | this._result = []; 392 | // use lazy open policy 393 | if (!this.ready) { 394 | this.open().then(() => { 395 | this._intern.start(timeslice); 396 | }); 397 | } else { 398 | this._intern.start(timeslice); 399 | } 400 | } 401 | 402 | stop () { 403 | assert(this.state).that(buildError('stop', this)).to.not.equal('inactive'); 404 | this._intern.stop(); 405 | } 406 | 407 | pause () { 408 | assert(this.state).that(buildError('pause', this)).to.equal('recording'); 409 | this._intern.pause(); 410 | } 411 | 412 | resume () { 413 | assert(this.state).that(buildError('resume', this)).to.equal('paused'); 414 | this._intern.resume(); 415 | } 416 | } 417 | 418 | /* 419 | * Copyright (C) 2017, Skyler. 420 | * Use of this source code is governed by the MIT license that can be 421 | * found in the LICENSE file. 422 | */ 423 | 424 | class WaveBell extends Recorder {} 425 | 426 | export default WaveBell; 427 | //# sourceMappingURL=wavebell.esm.js.map 428 | -------------------------------------------------------------------------------- /dist/wavebell.esm.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"wavebell.esm.js","sources":["../lib/util/emitter.js","../lib/util/assert.js","../lib/media/audio_filter.js","../lib/media/volume_meter.js","../lib/util/props.js","../lib/media/user_media.js","../lib/media/recorder.js","../lib/wavebell.js"],"sourcesContent":["/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nfunction slice (args, from) {\n return Array.prototype.slice.call(args, from)\n}\n\nclass Emitter {\n constructor () {\n this.handlerMap = {}\n }\n\n on (event, handler) {\n if (typeof event !== 'string') {\n throw new TypeError(event + ' is not a string')\n }\n if (typeof handler !== 'function') {\n throw new TypeError(handler + ' is not a function')\n }\n let map = this.handlerMap\n let handlers = map[event] = map[event] || []\n let i = handlers.indexOf(handler)\n if (i === -1) {\n handlers.push(handler)\n }\n return this\n }\n\n off (event, handler) {\n if (handler === undefined) {\n // remove all handlers\n delete this.handlerMap[event]\n return this\n }\n // remove registered handler\n let handlers = this.handlerMap[event]\n if (handlers) {\n let i = handlers.indexOf(handler)\n if (i >= 0) {\n handlers.splice(i, 1)\n }\n // cleanup empty handlers\n if (handlers.length === 0) {\n this.off(event)\n }\n }\n return this\n }\n\n emit (event) {\n let args = slice(arguments, 1)\n let handlers = this.handlerMap[event]\n if (handlers) {\n for (let i = 0, len = handlers.length; i < len; i++) {\n handlers[i].apply(undefined, args)\n }\n }\n return this\n }\n}\n\nexport default Emitter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nclass Assertion {\n constructor (value) {\n this.value = value\n this._negative = false\n this._error = new Error('Assertion failed')\n }\n\n get to () {\n return this\n }\n\n get not () {\n this._negative = !this._negative\n return this\n }\n\n that (error) {\n this._error = error\n return this\n }\n\n equal (value) {\n if ((value === this.value) === this._negative) {\n throw this._error\n }\n }\n}\n\nfunction assert (value) {\n return new Assertion(value)\n}\n\nexport default assert\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nconst AudioContext = window.AudioContext || window.webkitAudioContext\n\n// AudioContext singleton shared by filters\nlet audioContext = null\n\nclass AudioFilter {\n /**\n * Get AudioContext instance\n * @returns {AudioContext} - Shared instance\n */\n get context () {\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n return audioContext\n }\n}\n\nexport default AudioFilter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport AudioFilter from './audio_filter'\n\nclass VolumeMeter extends AudioFilter {\n constructor (mainbus, options) {\n super()\n this.mainbus = mainbus\n this.options = Object.assign({\n minLimit: 0,\n maxLimit: 128,\n fftSize: 1024,\n smoothing: 0.3\n }, options)\n this._checkOptions(this.options)\n this._cache = null\n this.source = null\n this.analyser = this._initAnalyser(this.options)\n }\n\n _checkOptions (options) {\n if (options.maxLimit <= options.minLimit) {\n throw new RangeError('Wrong limit range for volume')\n }\n }\n\n _initAnalyser (options) {\n // init analyser from options\n /// ref: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode\n let analyser = this.context.createAnalyser()\n analyser.fftSize = options.fftSize\n analyser.smoothingTimeConstant = options.smoothing\n // process data when available\n this.mainbus.on('dataavailable', e => this._processData())\n return analyser\n }\n\n pipe (stream) {\n // connect stream pipe\n this.source = this.context.createMediaStreamSource(stream)\n this.source.connect(this.analyser)\n }\n\n cutoff () {\n this.source.disconnect(this.analyser)\n this.source = null\n }\n\n _processData () {\n // half of the fftSize\n if (!this._cache) {\n this._cache = new Uint8Array(this.analyser.frequencyBinCount)\n }\n this.analyser.getByteFrequencyData(this._cache)\n let volume = this._calcAvgVolume(this._cache)\n this.mainbus.emit('wave', {\n value: this._alignVolume(volume)\n })\n }\n\n _alignVolume (volume) {\n let opts = this.options\n if (volume < opts.minLimit) {\n volume = opts.minLimit\n }\n if (volume > opts.maxLimit) {\n volume = opts.maxLimit\n }\n return (volume - opts.minLimit) / (opts.maxLimit - opts.minLimit)\n }\n\n _calcAvgVolume (data) {\n let sum = 0\n let len = data.length\n for (let i = 0; i < len; i++) {\n sum += data[i]\n }\n return sum / len\n }\n}\n\nexport default VolumeMeter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nconst hasOwn = Object.prototype.hasOwnProperty\n\nclass PropPath {\n constructor (path) {\n this.steps = path.split('.')\n this.fallback = undefined\n }\n\n travel (target, fn) {\n if (typeof fn !== 'function') {\n throw new TypeError(fn + ' is not a function')\n }\n let len = this.steps.length\n let i = 0\n let step = this.steps[i]\n while (fn(target, step) && i < len) {\n target = target[step]\n step = this.steps[++i]\n }\n return {\n step: i,\n value: target\n }\n }\n\n or (defaultValue) {\n this.fallback = defaultValue\n return this\n }\n\n from (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && step in Object(target)\n })\n if (ret.step === this.steps.length) {\n return ret.value\n } else {\n return this.fallback\n }\n }\n\n hadBy (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && step in Object(target)\n })\n return ret.step === this.steps.length\n }\n\n ownedBy (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && hasOwn.call(target, step)\n })\n return ret.step === this.steps.length\n }\n}\n\nfunction props (path) {\n if (typeof path !== 'string') {\n throw new TypeError(path + ' is not a string')\n }\n return new PropPath(path)\n}\n\nexport default props\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport props from '@/util/props'\n\n/**\n * Shim for MediaDevices#getUserMedia method\n * @param {object} constraints - The user media constraints\n */\nfunction getUserMedia (constraints) {\n if (props('navigator.mediaDevices.getUserMedia').hadBy(window)) {\n let medias = props('navigator.mediaDevices').from(window)\n return medias.getUserMedia(constraints)\n }\n let userMediaGetter = navigator.getUserMedia ||\n navigator.webkitGetUserMedia ||\n navigator.mozGetUserMedia ||\n navigator.msGetUserMedia\n if (!userMediaGetter) {\n throw new Error('getUserMedia is not supported by this browser')\n }\n return new Promise((resolve, reject) => {\n userMediaGetter(constraints, resolve, reject)\n })\n}\n\n/**\n * Access audio input from microphone device\n */\nfunction getUserMicrophone () {\n return getUserMedia({ audio: true, video: false })\n}\n\nexport {\n getUserMedia,\n getUserMicrophone\n}\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport Emitter from '@/util/emitter'\nimport assert from '@/util/assert'\nimport VolumeMeter from './volume_meter'\nimport { getUserMicrophone } from './user_media'\n\nfunction buildError (callee, that) {\n return new Error(`Failed to execute '${callee}' on 'Recorder'` +\n (that ? `:\\nThe Recorder's state is '${that.state}'.` : ''))\n}\n\nclass Recorder extends Emitter {\n constructor (options) {\n super()\n this.options = Object.assign({\n mimeType: 'audio/webm',\n audioBitsPerSecond: 96000\n }, options)\n this._intern = null\n this._result = null\n this._filter = new VolumeMeter(this, this.options.meter)\n }\n\n get state () {\n if (this._intern === null) {\n return 'inactive'\n } else {\n return this._intern.state\n }\n }\n\n get ready () {\n return this._intern !== null\n }\n\n get result () {\n if (!this._result) {\n return null\n }\n return new Blob(this._result, {\n type: this.options.mimeType\n })\n }\n\n open () {\n assert(this.ready).that(buildError('open')).to.equal(false)\n return getUserMicrophone().then(stream => {\n // create internal recorder\n this._intern = new MediaRecorder(stream, this.options)\n // register event listeners\n let eventTypes = ['error', 'pause', 'resume', 'start', 'stop']\n eventTypes.map(type => {\n this._intern.addEventListener(type, e => this.emit(type, e))\n })\n this._intern.addEventListener('dataavailable', e => {\n this._result.push(e.data)\n this.emit('dataavailable', e)\n })\n // pipe stream to filter\n this._filter.pipe(stream)\n })\n }\n\n close () {\n assert(this.ready).that(buildError('close')).to.equal(true)\n // close all stream tracks\n let tracks = this._intern.stream.getTracks()\n for (let i = 0; i < tracks.length; i++) {\n tracks[i].stop()\n }\n // close stream filter\n this._filter.cutoff()\n this._intern = null\n }\n\n start (timeslice) {\n assert(this.state).that(buildError('start', this)).to.equal('inactive')\n // init result data on every start\n this._result = []\n // use lazy open policy\n if (!this.ready) {\n this.open().then(() => {\n this._intern.start(timeslice)\n })\n } else {\n this._intern.start(timeslice)\n }\n }\n\n stop () {\n assert(this.state).that(buildError('stop', this)).to.not.equal('inactive')\n this._intern.stop()\n }\n\n pause () {\n assert(this.state).that(buildError('pause', this)).to.equal('recording')\n this._intern.pause()\n }\n\n resume () {\n assert(this.state).that(buildError('resume', this)).to.equal('paused')\n this._intern.resume()\n }\n}\n\nexport default Recorder\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport Recorder from '@/media/recorder'\n\nclass WaveBell extends Recorder {}\n\nexport default WaveBell\n"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/wavebell.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.WaveBell = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var classCallCheck = function (instance, Constructor) { 8 | if (!(instance instanceof Constructor)) { 9 | throw new TypeError("Cannot call a class as a function"); 10 | } 11 | }; 12 | 13 | var createClass = function () { 14 | function defineProperties(target, props) { 15 | for (var i = 0; i < props.length; i++) { 16 | var descriptor = props[i]; 17 | descriptor.enumerable = descriptor.enumerable || false; 18 | descriptor.configurable = true; 19 | if ("value" in descriptor) descriptor.writable = true; 20 | Object.defineProperty(target, descriptor.key, descriptor); 21 | } 22 | } 23 | 24 | return function (Constructor, protoProps, staticProps) { 25 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 26 | if (staticProps) defineProperties(Constructor, staticProps); 27 | return Constructor; 28 | }; 29 | }(); 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | var inherits = function (subClass, superClass) { 40 | if (typeof superClass !== "function" && superClass !== null) { 41 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 42 | } 43 | 44 | subClass.prototype = Object.create(superClass && superClass.prototype, { 45 | constructor: { 46 | value: subClass, 47 | enumerable: false, 48 | writable: true, 49 | configurable: true 50 | } 51 | }); 52 | if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 53 | }; 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | var possibleConstructorReturn = function (self, call) { 66 | if (!self) { 67 | throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 68 | } 69 | 70 | return call && (typeof call === "object" || typeof call === "function") ? call : self; 71 | }; 72 | 73 | /* 74 | * Copyright (C) 2017, Skyler. 75 | * Use of this source code is governed by the MIT license that can be 76 | * found in the LICENSE file. 77 | */ 78 | 79 | function slice(args, from) { 80 | return Array.prototype.slice.call(args, from); 81 | } 82 | 83 | var Emitter = function () { 84 | function Emitter() { 85 | classCallCheck(this, Emitter); 86 | 87 | this.handlerMap = {}; 88 | } 89 | 90 | createClass(Emitter, [{ 91 | key: 'on', 92 | value: function on(event, handler) { 93 | if (typeof event !== 'string') { 94 | throw new TypeError(event + ' is not a string'); 95 | } 96 | if (typeof handler !== 'function') { 97 | throw new TypeError(handler + ' is not a function'); 98 | } 99 | var map = this.handlerMap; 100 | var handlers = map[event] = map[event] || []; 101 | var i = handlers.indexOf(handler); 102 | if (i === -1) { 103 | handlers.push(handler); 104 | } 105 | return this; 106 | } 107 | }, { 108 | key: 'off', 109 | value: function off(event, handler) { 110 | if (handler === undefined) { 111 | // remove all handlers 112 | delete this.handlerMap[event]; 113 | return this; 114 | } 115 | // remove registered handler 116 | var handlers = this.handlerMap[event]; 117 | if (handlers) { 118 | var i = handlers.indexOf(handler); 119 | if (i >= 0) { 120 | handlers.splice(i, 1); 121 | } 122 | // cleanup empty handlers 123 | if (handlers.length === 0) { 124 | this.off(event); 125 | } 126 | } 127 | return this; 128 | } 129 | }, { 130 | key: 'emit', 131 | value: function emit(event) { 132 | var args = slice(arguments, 1); 133 | var handlers = this.handlerMap[event]; 134 | if (handlers) { 135 | for (var i = 0, len = handlers.length; i < len; i++) { 136 | handlers[i].apply(undefined, args); 137 | } 138 | } 139 | return this; 140 | } 141 | }]); 142 | return Emitter; 143 | }(); 144 | 145 | /* 146 | * Copyright (C) 2017, Skyler. 147 | * Use of this source code is governed by the MIT license that can be 148 | * found in the LICENSE file. 149 | */ 150 | 151 | var Assertion = function () { 152 | function Assertion(value) { 153 | classCallCheck(this, Assertion); 154 | 155 | this.value = value; 156 | this._negative = false; 157 | this._error = new Error('Assertion failed'); 158 | } 159 | 160 | createClass(Assertion, [{ 161 | key: 'that', 162 | value: function that(error) { 163 | this._error = error; 164 | return this; 165 | } 166 | }, { 167 | key: 'equal', 168 | value: function equal(value) { 169 | if (value === this.value === this._negative) { 170 | throw this._error; 171 | } 172 | } 173 | }, { 174 | key: 'to', 175 | get: function get$$1() { 176 | return this; 177 | } 178 | }, { 179 | key: 'not', 180 | get: function get$$1() { 181 | this._negative = !this._negative; 182 | return this; 183 | } 184 | }]); 185 | return Assertion; 186 | }(); 187 | 188 | function assert(value) { 189 | return new Assertion(value); 190 | } 191 | 192 | /* 193 | * Copyright (C) 2017, Skyler. 194 | * Use of this source code is governed by the MIT license that can be 195 | * found in the LICENSE file. 196 | */ 197 | 198 | var AudioContext = window.AudioContext || window.webkitAudioContext; 199 | 200 | // AudioContext singleton shared by filters 201 | var audioContext = null; 202 | 203 | var AudioFilter = function () { 204 | function AudioFilter() { 205 | classCallCheck(this, AudioFilter); 206 | } 207 | 208 | createClass(AudioFilter, [{ 209 | key: "context", 210 | 211 | /** 212 | * Get AudioContext instance 213 | * @returns {AudioContext} - Shared instance 214 | */ 215 | get: function get$$1() { 216 | if (!audioContext) { 217 | audioContext = new AudioContext(); 218 | } 219 | return audioContext; 220 | } 221 | }]); 222 | return AudioFilter; 223 | }(); 224 | 225 | /* 226 | * Copyright (C) 2017, Skyler. 227 | * Use of this source code is governed by the MIT license that can be 228 | * found in the LICENSE file. 229 | */ 230 | 231 | var VolumeMeter = function (_AudioFilter) { 232 | inherits(VolumeMeter, _AudioFilter); 233 | 234 | function VolumeMeter(mainbus, options) { 235 | classCallCheck(this, VolumeMeter); 236 | 237 | var _this = possibleConstructorReturn(this, (VolumeMeter.__proto__ || Object.getPrototypeOf(VolumeMeter)).call(this)); 238 | 239 | _this.mainbus = mainbus; 240 | _this.options = Object.assign({ 241 | minLimit: 0, 242 | maxLimit: 128, 243 | fftSize: 1024, 244 | smoothing: 0.3 245 | }, options); 246 | _this._checkOptions(_this.options); 247 | _this._cache = null; 248 | _this.source = null; 249 | _this.analyser = _this._initAnalyser(_this.options); 250 | return _this; 251 | } 252 | 253 | createClass(VolumeMeter, [{ 254 | key: '_checkOptions', 255 | value: function _checkOptions(options) { 256 | if (options.maxLimit <= options.minLimit) { 257 | throw new RangeError('Wrong limit range for volume'); 258 | } 259 | } 260 | }, { 261 | key: '_initAnalyser', 262 | value: function _initAnalyser(options) { 263 | var _this2 = this; 264 | 265 | // init analyser from options 266 | /// ref: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode 267 | var analyser = this.context.createAnalyser(); 268 | analyser.fftSize = options.fftSize; 269 | analyser.smoothingTimeConstant = options.smoothing; 270 | // process data when available 271 | this.mainbus.on('dataavailable', function (e) { 272 | return _this2._processData(); 273 | }); 274 | return analyser; 275 | } 276 | }, { 277 | key: 'pipe', 278 | value: function pipe(stream) { 279 | // connect stream pipe 280 | this.source = this.context.createMediaStreamSource(stream); 281 | this.source.connect(this.analyser); 282 | } 283 | }, { 284 | key: 'cutoff', 285 | value: function cutoff() { 286 | this.source.disconnect(this.analyser); 287 | this.source = null; 288 | } 289 | }, { 290 | key: '_processData', 291 | value: function _processData() { 292 | // half of the fftSize 293 | if (!this._cache) { 294 | this._cache = new Uint8Array(this.analyser.frequencyBinCount); 295 | } 296 | this.analyser.getByteFrequencyData(this._cache); 297 | var volume = this._calcAvgVolume(this._cache); 298 | this.mainbus.emit('wave', { 299 | value: this._alignVolume(volume) 300 | }); 301 | } 302 | }, { 303 | key: '_alignVolume', 304 | value: function _alignVolume(volume) { 305 | var opts = this.options; 306 | if (volume < opts.minLimit) { 307 | volume = opts.minLimit; 308 | } 309 | if (volume > opts.maxLimit) { 310 | volume = opts.maxLimit; 311 | } 312 | return (volume - opts.minLimit) / (opts.maxLimit - opts.minLimit); 313 | } 314 | }, { 315 | key: '_calcAvgVolume', 316 | value: function _calcAvgVolume(data) { 317 | var sum = 0; 318 | var len = data.length; 319 | for (var i = 0; i < len; i++) { 320 | sum += data[i]; 321 | } 322 | return sum / len; 323 | } 324 | }]); 325 | return VolumeMeter; 326 | }(AudioFilter); 327 | 328 | /* 329 | * Copyright (C) 2017, Skyler. 330 | * Use of this source code is governed by the MIT license that can be 331 | * found in the LICENSE file. 332 | */ 333 | 334 | var hasOwn = Object.prototype.hasOwnProperty; 335 | 336 | var PropPath = function () { 337 | function PropPath(path) { 338 | classCallCheck(this, PropPath); 339 | 340 | this.steps = path.split('.'); 341 | this.fallback = undefined; 342 | } 343 | 344 | createClass(PropPath, [{ 345 | key: 'travel', 346 | value: function travel(target, fn) { 347 | if (typeof fn !== 'function') { 348 | throw new TypeError(fn + ' is not a function'); 349 | } 350 | var len = this.steps.length; 351 | var i = 0; 352 | var step = this.steps[i]; 353 | while (fn(target, step) && i < len) { 354 | target = target[step]; 355 | step = this.steps[++i]; 356 | } 357 | return { 358 | step: i, 359 | value: target 360 | }; 361 | } 362 | }, { 363 | key: 'or', 364 | value: function or(defaultValue) { 365 | this.fallback = defaultValue; 366 | return this; 367 | } 368 | }, { 369 | key: 'from', 370 | value: function from(obj) { 371 | var ret = this.travel(obj, function (target, step) { 372 | return target != null && step in Object(target); 373 | }); 374 | if (ret.step === this.steps.length) { 375 | return ret.value; 376 | } else { 377 | return this.fallback; 378 | } 379 | } 380 | }, { 381 | key: 'hadBy', 382 | value: function hadBy(obj) { 383 | var ret = this.travel(obj, function (target, step) { 384 | return target != null && step in Object(target); 385 | }); 386 | return ret.step === this.steps.length; 387 | } 388 | }, { 389 | key: 'ownedBy', 390 | value: function ownedBy(obj) { 391 | var ret = this.travel(obj, function (target, step) { 392 | return target != null && hasOwn.call(target, step); 393 | }); 394 | return ret.step === this.steps.length; 395 | } 396 | }]); 397 | return PropPath; 398 | }(); 399 | 400 | function props(path) { 401 | if (typeof path !== 'string') { 402 | throw new TypeError(path + ' is not a string'); 403 | } 404 | return new PropPath(path); 405 | } 406 | 407 | /* 408 | * Copyright (C) 2017, Skyler. 409 | * Use of this source code is governed by the MIT license that can be 410 | * found in the LICENSE file. 411 | */ 412 | 413 | /** 414 | * Shim for MediaDevices#getUserMedia method 415 | * @param {object} constraints - The user media constraints 416 | */ 417 | function getUserMedia(constraints) { 418 | if (props('navigator.mediaDevices.getUserMedia').hadBy(window)) { 419 | var medias = props('navigator.mediaDevices').from(window); 420 | return medias.getUserMedia(constraints); 421 | } 422 | var userMediaGetter = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 423 | if (!userMediaGetter) { 424 | throw new Error('getUserMedia is not supported by this browser'); 425 | } 426 | return new Promise(function (resolve, reject) { 427 | userMediaGetter(constraints, resolve, reject); 428 | }); 429 | } 430 | 431 | /** 432 | * Access audio input from microphone device 433 | */ 434 | function getUserMicrophone() { 435 | return getUserMedia({ audio: true, video: false }); 436 | } 437 | 438 | /* 439 | * Copyright (C) 2017, Skyler. 440 | * Use of this source code is governed by the MIT license that can be 441 | * found in the LICENSE file. 442 | */ 443 | 444 | function buildError(callee, that) { 445 | return new Error('Failed to execute \'' + callee + '\' on \'Recorder\'' + (that ? ':\nThe Recorder\'s state is \'' + that.state + '\'.' : '')); 446 | } 447 | 448 | var Recorder = function (_Emitter) { 449 | inherits(Recorder, _Emitter); 450 | 451 | function Recorder(options) { 452 | classCallCheck(this, Recorder); 453 | 454 | var _this = possibleConstructorReturn(this, (Recorder.__proto__ || Object.getPrototypeOf(Recorder)).call(this)); 455 | 456 | _this.options = Object.assign({ 457 | mimeType: 'audio/webm', 458 | audioBitsPerSecond: 96000 459 | }, options); 460 | _this._intern = null; 461 | _this._result = null; 462 | _this._filter = new VolumeMeter(_this, _this.options.meter); 463 | return _this; 464 | } 465 | 466 | createClass(Recorder, [{ 467 | key: 'open', 468 | value: function open() { 469 | var _this2 = this; 470 | 471 | assert(this.ready).that(buildError('open')).to.equal(false); 472 | return getUserMicrophone().then(function (stream) { 473 | // create internal recorder 474 | _this2._intern = new MediaRecorder(stream, _this2.options); 475 | // register event listeners 476 | var eventTypes = ['error', 'pause', 'resume', 'start', 'stop']; 477 | eventTypes.map(function (type) { 478 | _this2._intern.addEventListener(type, function (e) { 479 | return _this2.emit(type, e); 480 | }); 481 | }); 482 | _this2._intern.addEventListener('dataavailable', function (e) { 483 | _this2._result.push(e.data); 484 | _this2.emit('dataavailable', e); 485 | }); 486 | // pipe stream to filter 487 | _this2._filter.pipe(stream); 488 | }); 489 | } 490 | }, { 491 | key: 'close', 492 | value: function close() { 493 | assert(this.ready).that(buildError('close')).to.equal(true); 494 | // close all stream tracks 495 | var tracks = this._intern.stream.getTracks(); 496 | for (var i = 0; i < tracks.length; i++) { 497 | tracks[i].stop(); 498 | } 499 | // close stream filter 500 | this._filter.cutoff(); 501 | this._intern = null; 502 | } 503 | }, { 504 | key: 'start', 505 | value: function start(timeslice) { 506 | var _this3 = this; 507 | 508 | assert(this.state).that(buildError('start', this)).to.equal('inactive'); 509 | // init result data on every start 510 | this._result = []; 511 | // use lazy open policy 512 | if (!this.ready) { 513 | this.open().then(function () { 514 | _this3._intern.start(timeslice); 515 | }); 516 | } else { 517 | this._intern.start(timeslice); 518 | } 519 | } 520 | }, { 521 | key: 'stop', 522 | value: function stop() { 523 | assert(this.state).that(buildError('stop', this)).to.not.equal('inactive'); 524 | this._intern.stop(); 525 | } 526 | }, { 527 | key: 'pause', 528 | value: function pause() { 529 | assert(this.state).that(buildError('pause', this)).to.equal('recording'); 530 | this._intern.pause(); 531 | } 532 | }, { 533 | key: 'resume', 534 | value: function resume() { 535 | assert(this.state).that(buildError('resume', this)).to.equal('paused'); 536 | this._intern.resume(); 537 | } 538 | }, { 539 | key: 'state', 540 | get: function get$$1() { 541 | if (this._intern === null) { 542 | return 'inactive'; 543 | } else { 544 | return this._intern.state; 545 | } 546 | } 547 | }, { 548 | key: 'ready', 549 | get: function get$$1() { 550 | return this._intern !== null; 551 | } 552 | }, { 553 | key: 'result', 554 | get: function get$$1() { 555 | if (!this._result) { 556 | return null; 557 | } 558 | return new Blob(this._result, { 559 | type: this.options.mimeType 560 | }); 561 | } 562 | }]); 563 | return Recorder; 564 | }(Emitter); 565 | 566 | /* 567 | * Copyright (C) 2017, Skyler. 568 | * Use of this source code is governed by the MIT license that can be 569 | * found in the LICENSE file. 570 | */ 571 | 572 | var WaveBell = function (_Recorder) { 573 | inherits(WaveBell, _Recorder); 574 | 575 | function WaveBell() { 576 | classCallCheck(this, WaveBell); 577 | return possibleConstructorReturn(this, (WaveBell.__proto__ || Object.getPrototypeOf(WaveBell)).apply(this, arguments)); 578 | } 579 | 580 | return WaveBell; 581 | }(Recorder); 582 | 583 | return WaveBell; 584 | 585 | }))); 586 | //# sourceMappingURL=wavebell.js.map 587 | -------------------------------------------------------------------------------- /dist/wavebell.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"wavebell.js","sources":["../lib/util/emitter.js","../lib/util/assert.js","../lib/media/audio_filter.js","../lib/media/volume_meter.js","../lib/util/props.js","../lib/media/user_media.js","../lib/media/recorder.js","../lib/wavebell.js"],"sourcesContent":["/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nfunction slice (args, from) {\n return Array.prototype.slice.call(args, from)\n}\n\nclass Emitter {\n constructor () {\n this.handlerMap = {}\n }\n\n on (event, handler) {\n if (typeof event !== 'string') {\n throw new TypeError(event + ' is not a string')\n }\n if (typeof handler !== 'function') {\n throw new TypeError(handler + ' is not a function')\n }\n let map = this.handlerMap\n let handlers = map[event] = map[event] || []\n let i = handlers.indexOf(handler)\n if (i === -1) {\n handlers.push(handler)\n }\n return this\n }\n\n off (event, handler) {\n if (handler === undefined) {\n // remove all handlers\n delete this.handlerMap[event]\n return this\n }\n // remove registered handler\n let handlers = this.handlerMap[event]\n if (handlers) {\n let i = handlers.indexOf(handler)\n if (i >= 0) {\n handlers.splice(i, 1)\n }\n // cleanup empty handlers\n if (handlers.length === 0) {\n this.off(event)\n }\n }\n return this\n }\n\n emit (event) {\n let args = slice(arguments, 1)\n let handlers = this.handlerMap[event]\n if (handlers) {\n for (let i = 0, len = handlers.length; i < len; i++) {\n handlers[i].apply(undefined, args)\n }\n }\n return this\n }\n}\n\nexport default Emitter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nclass Assertion {\n constructor (value) {\n this.value = value\n this._negative = false\n this._error = new Error('Assertion failed')\n }\n\n get to () {\n return this\n }\n\n get not () {\n this._negative = !this._negative\n return this\n }\n\n that (error) {\n this._error = error\n return this\n }\n\n equal (value) {\n if ((value === this.value) === this._negative) {\n throw this._error\n }\n }\n}\n\nfunction assert (value) {\n return new Assertion(value)\n}\n\nexport default assert\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nconst AudioContext = window.AudioContext || window.webkitAudioContext\n\n// AudioContext singleton shared by filters\nlet audioContext = null\n\nclass AudioFilter {\n /**\n * Get AudioContext instance\n * @returns {AudioContext} - Shared instance\n */\n get context () {\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n return audioContext\n }\n}\n\nexport default AudioFilter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport AudioFilter from './audio_filter'\n\nclass VolumeMeter extends AudioFilter {\n constructor (mainbus, options) {\n super()\n this.mainbus = mainbus\n this.options = Object.assign({\n minLimit: 0,\n maxLimit: 128,\n fftSize: 1024,\n smoothing: 0.3\n }, options)\n this._checkOptions(this.options)\n this._cache = null\n this.source = null\n this.analyser = this._initAnalyser(this.options)\n }\n\n _checkOptions (options) {\n if (options.maxLimit <= options.minLimit) {\n throw new RangeError('Wrong limit range for volume')\n }\n }\n\n _initAnalyser (options) {\n // init analyser from options\n /// ref: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode\n let analyser = this.context.createAnalyser()\n analyser.fftSize = options.fftSize\n analyser.smoothingTimeConstant = options.smoothing\n // process data when available\n this.mainbus.on('dataavailable', e => this._processData())\n return analyser\n }\n\n pipe (stream) {\n // connect stream pipe\n this.source = this.context.createMediaStreamSource(stream)\n this.source.connect(this.analyser)\n }\n\n cutoff () {\n this.source.disconnect(this.analyser)\n this.source = null\n }\n\n _processData () {\n // half of the fftSize\n if (!this._cache) {\n this._cache = new Uint8Array(this.analyser.frequencyBinCount)\n }\n this.analyser.getByteFrequencyData(this._cache)\n let volume = this._calcAvgVolume(this._cache)\n this.mainbus.emit('wave', {\n value: this._alignVolume(volume)\n })\n }\n\n _alignVolume (volume) {\n let opts = this.options\n if (volume < opts.minLimit) {\n volume = opts.minLimit\n }\n if (volume > opts.maxLimit) {\n volume = opts.maxLimit\n }\n return (volume - opts.minLimit) / (opts.maxLimit - opts.minLimit)\n }\n\n _calcAvgVolume (data) {\n let sum = 0\n let len = data.length\n for (let i = 0; i < len; i++) {\n sum += data[i]\n }\n return sum / len\n }\n}\n\nexport default VolumeMeter\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nconst hasOwn = Object.prototype.hasOwnProperty\n\nclass PropPath {\n constructor (path) {\n this.steps = path.split('.')\n this.fallback = undefined\n }\n\n travel (target, fn) {\n if (typeof fn !== 'function') {\n throw new TypeError(fn + ' is not a function')\n }\n let len = this.steps.length\n let i = 0\n let step = this.steps[i]\n while (fn(target, step) && i < len) {\n target = target[step]\n step = this.steps[++i]\n }\n return {\n step: i,\n value: target\n }\n }\n\n or (defaultValue) {\n this.fallback = defaultValue\n return this\n }\n\n from (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && step in Object(target)\n })\n if (ret.step === this.steps.length) {\n return ret.value\n } else {\n return this.fallback\n }\n }\n\n hadBy (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && step in Object(target)\n })\n return ret.step === this.steps.length\n }\n\n ownedBy (obj) {\n let ret = this.travel(obj, (target, step) => {\n return target != null && hasOwn.call(target, step)\n })\n return ret.step === this.steps.length\n }\n}\n\nfunction props (path) {\n if (typeof path !== 'string') {\n throw new TypeError(path + ' is not a string')\n }\n return new PropPath(path)\n}\n\nexport default props\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport props from '@/util/props'\n\n/**\n * Shim for MediaDevices#getUserMedia method\n * @param {object} constraints - The user media constraints\n */\nfunction getUserMedia (constraints) {\n if (props('navigator.mediaDevices.getUserMedia').hadBy(window)) {\n let medias = props('navigator.mediaDevices').from(window)\n return medias.getUserMedia(constraints)\n }\n let userMediaGetter = navigator.getUserMedia ||\n navigator.webkitGetUserMedia ||\n navigator.mozGetUserMedia ||\n navigator.msGetUserMedia\n if (!userMediaGetter) {\n throw new Error('getUserMedia is not supported by this browser')\n }\n return new Promise((resolve, reject) => {\n userMediaGetter(constraints, resolve, reject)\n })\n}\n\n/**\n * Access audio input from microphone device\n */\nfunction getUserMicrophone () {\n return getUserMedia({ audio: true, video: false })\n}\n\nexport {\n getUserMedia,\n getUserMicrophone\n}\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport Emitter from '@/util/emitter'\nimport assert from '@/util/assert'\nimport VolumeMeter from './volume_meter'\nimport { getUserMicrophone } from './user_media'\n\nfunction buildError (callee, that) {\n return new Error(`Failed to execute '${callee}' on 'Recorder'` +\n (that ? `:\\nThe Recorder's state is '${that.state}'.` : ''))\n}\n\nclass Recorder extends Emitter {\n constructor (options) {\n super()\n this.options = Object.assign({\n mimeType: 'audio/webm',\n audioBitsPerSecond: 96000\n }, options)\n this._intern = null\n this._result = null\n this._filter = new VolumeMeter(this, this.options.meter)\n }\n\n get state () {\n if (this._intern === null) {\n return 'inactive'\n } else {\n return this._intern.state\n }\n }\n\n get ready () {\n return this._intern !== null\n }\n\n get result () {\n if (!this._result) {\n return null\n }\n return new Blob(this._result, {\n type: this.options.mimeType\n })\n }\n\n open () {\n assert(this.ready).that(buildError('open')).to.equal(false)\n return getUserMicrophone().then(stream => {\n // create internal recorder\n this._intern = new MediaRecorder(stream, this.options)\n // register event listeners\n let eventTypes = ['error', 'pause', 'resume', 'start', 'stop']\n eventTypes.map(type => {\n this._intern.addEventListener(type, e => this.emit(type, e))\n })\n this._intern.addEventListener('dataavailable', e => {\n this._result.push(e.data)\n this.emit('dataavailable', e)\n })\n // pipe stream to filter\n this._filter.pipe(stream)\n })\n }\n\n close () {\n assert(this.ready).that(buildError('close')).to.equal(true)\n // close all stream tracks\n let tracks = this._intern.stream.getTracks()\n for (let i = 0; i < tracks.length; i++) {\n tracks[i].stop()\n }\n // close stream filter\n this._filter.cutoff()\n this._intern = null\n }\n\n start (timeslice) {\n assert(this.state).that(buildError('start', this)).to.equal('inactive')\n // init result data on every start\n this._result = []\n // use lazy open policy\n if (!this.ready) {\n this.open().then(() => {\n this._intern.start(timeslice)\n })\n } else {\n this._intern.start(timeslice)\n }\n }\n\n stop () {\n assert(this.state).that(buildError('stop', this)).to.not.equal('inactive')\n this._intern.stop()\n }\n\n pause () {\n assert(this.state).that(buildError('pause', this)).to.equal('recording')\n this._intern.pause()\n }\n\n resume () {\n assert(this.state).that(buildError('resume', this)).to.equal('paused')\n this._intern.resume()\n }\n}\n\nexport default Recorder\n","/*\n * Copyright (C) 2017, Skyler.\n * Use of this source code is governed by the MIT license that can be\n * found in the LICENSE file.\n */\n\nimport Recorder from '@/media/recorder'\n\nclass WaveBell extends Recorder {}\n\nexport default WaveBell\n"],"names":["slice","args","from","Array","prototype","call","Emitter","handlerMap","event","handler","TypeError","map","handlers","i","indexOf","push","undefined","splice","length","off","arguments","len","apply","Assertion","value","_negative","_error","Error","error","assert","AudioContext","window","webkitAudioContext","audioContext","AudioFilter","VolumeMeter","mainbus","options","Object","assign","_checkOptions","_cache","source","analyser","_initAnalyser","maxLimit","minLimit","RangeError","context","createAnalyser","fftSize","smoothingTimeConstant","smoothing","on","_processData","stream","createMediaStreamSource","connect","disconnect","Uint8Array","frequencyBinCount","getByteFrequencyData","volume","_calcAvgVolume","emit","_alignVolume","opts","data","sum","hasOwn","hasOwnProperty","PropPath","path","steps","split","fallback","target","fn","step","defaultValue","obj","ret","travel","props","getUserMedia","constraints","hadBy","medias","userMediaGetter","navigator","webkitGetUserMedia","mozGetUserMedia","msGetUserMedia","Promise","resolve","reject","getUserMicrophone","audio","video","buildError","callee","that","state","Recorder","_intern","_result","_filter","meter","ready","to","equal","then","MediaRecorder","eventTypes","addEventListener","type","e","pipe","tracks","getTracks","stop","cutoff","timeslice","open","start","not","pause","resume","Blob","mimeType","WaveBell"],"mappings":"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"} -------------------------------------------------------------------------------- /dist/wavebell.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.WaveBell=e()}(this,function(){"use strict";function t(t){return new c(t)}function e(t){if("string"!=typeof t)throw new TypeError(t+" is not a string");return new v(t)}function n(){return function(t){if(e("navigator.mediaDevices.getUserMedia").hadBy(window))return e("navigator.mediaDevices").from(window).getUserMedia(t);var n=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;if(!n)throw new Error("getUserMedia is not supported by this browser");return new Promise(function(e,i){n(t,e,i)})}({audio:!0,video:!1})}function i(t,e){return new Error("Failed to execute '"+t+"' on 'Recorder'"+(e?":\nThe Recorder's state is '"+e.state+"'.":""))}var r=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},o=function(){function t(t,e){for(var n=0;n=0&&n.splice(i,1),0===n.length&&this.off(t)}return this}},{key:"emit",value:function(t){var e=function(t,e){return Array.prototype.slice.call(t,e)}(arguments,1),n=this.handlerMap[t];if(n)for(var i=0,r=n.length;ie.maxLimit&&(t=e.maxLimit),(t-e.minLimit)/(e.maxLimit-e.minLimit)}},{key:"_calcAvgVolume",value:function(t){for(var e=0,n=t.length,i=0;i { 53 | // create internal recorder 54 | this._intern = new MediaRecorder(stream, this.options) 55 | // register event listeners 56 | let eventTypes = ['error', 'pause', 'resume', 'start', 'stop'] 57 | eventTypes.map(type => { 58 | this._intern.addEventListener(type, e => this.emit(type, e)) 59 | }) 60 | this._intern.addEventListener('dataavailable', e => { 61 | this._result.push(e.data) 62 | this.emit('dataavailable', e) 63 | }) 64 | // pipe stream to filter 65 | this._filter.pipe(stream) 66 | }) 67 | } 68 | 69 | close () { 70 | assert(this.ready).that(buildError('close')).to.equal(true) 71 | // close all stream tracks 72 | let tracks = this._intern.stream.getTracks() 73 | for (let i = 0; i < tracks.length; i++) { 74 | tracks[i].stop() 75 | } 76 | // close stream filter 77 | this._filter.cutoff() 78 | this._intern = null 79 | } 80 | 81 | start (timeslice) { 82 | assert(this.state).that(buildError('start', this)).to.equal('inactive') 83 | // init result data on every start 84 | this._result = [] 85 | // use lazy open policy 86 | if (!this.ready) { 87 | this.open().then(() => { 88 | this._intern.start(timeslice) 89 | }) 90 | } else { 91 | this._intern.start(timeslice) 92 | } 93 | } 94 | 95 | stop () { 96 | assert(this.state).that(buildError('stop', this)).to.not.equal('inactive') 97 | this._intern.stop() 98 | } 99 | 100 | pause () { 101 | assert(this.state).that(buildError('pause', this)).to.equal('recording') 102 | this._intern.pause() 103 | } 104 | 105 | resume () { 106 | assert(this.state).that(buildError('resume', this)).to.equal('paused') 107 | this._intern.resume() 108 | } 109 | } 110 | 111 | export default Recorder 112 | -------------------------------------------------------------------------------- /lib/media/user_media.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | import props from '@/util/props' 8 | 9 | /** 10 | * Shim for MediaDevices#getUserMedia method 11 | * @param {object} constraints - The user media constraints 12 | */ 13 | function getUserMedia (constraints) { 14 | if (props('navigator.mediaDevices.getUserMedia').hadBy(window)) { 15 | let medias = props('navigator.mediaDevices').from(window) 16 | return medias.getUserMedia(constraints) 17 | } 18 | let userMediaGetter = navigator.getUserMedia || 19 | navigator.webkitGetUserMedia || 20 | navigator.mozGetUserMedia || 21 | navigator.msGetUserMedia 22 | if (!userMediaGetter) { 23 | throw new Error('getUserMedia is not supported by this browser') 24 | } 25 | return new Promise((resolve, reject) => { 26 | userMediaGetter(constraints, resolve, reject) 27 | }) 28 | } 29 | 30 | /** 31 | * Access audio input from microphone device 32 | */ 33 | function getUserMicrophone () { 34 | return getUserMedia({ audio: true, video: false }) 35 | } 36 | 37 | export { 38 | getUserMedia, 39 | getUserMicrophone 40 | } 41 | -------------------------------------------------------------------------------- /lib/media/volume_meter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | import AudioFilter from './audio_filter' 8 | 9 | class VolumeMeter extends AudioFilter { 10 | constructor (mainbus, options) { 11 | super() 12 | this.mainbus = mainbus 13 | this.options = Object.assign({ 14 | minLimit: 0, 15 | maxLimit: 128, 16 | fftSize: 1024, 17 | smoothing: 0.3 18 | }, options) 19 | this._checkOptions(this.options) 20 | this._cache = null 21 | this.source = null 22 | this.analyser = this._initAnalyser(this.options) 23 | } 24 | 25 | _checkOptions (options) { 26 | if (options.maxLimit <= options.minLimit) { 27 | throw new RangeError('Wrong limit range for volume') 28 | } 29 | } 30 | 31 | _initAnalyser (options) { 32 | // init analyser from options 33 | /// ref: https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode 34 | let analyser = this.context.createAnalyser() 35 | analyser.fftSize = options.fftSize 36 | analyser.smoothingTimeConstant = options.smoothing 37 | // process data when available 38 | this.mainbus.on('dataavailable', e => this._processData()) 39 | return analyser 40 | } 41 | 42 | pipe (stream) { 43 | // connect stream pipe 44 | this.source = this.context.createMediaStreamSource(stream) 45 | this.source.connect(this.analyser) 46 | } 47 | 48 | cutoff () { 49 | this.source.disconnect(this.analyser) 50 | this.source = null 51 | } 52 | 53 | _processData () { 54 | // half of the fftSize 55 | if (!this._cache) { 56 | this._cache = new Uint8Array(this.analyser.frequencyBinCount) 57 | } 58 | this.analyser.getByteFrequencyData(this._cache) 59 | let volume = this._calcAvgVolume(this._cache) 60 | this.mainbus.emit('wave', { 61 | value: this._alignVolume(volume) 62 | }) 63 | } 64 | 65 | _alignVolume (volume) { 66 | let opts = this.options 67 | if (volume < opts.minLimit) { 68 | volume = opts.minLimit 69 | } 70 | if (volume > opts.maxLimit) { 71 | volume = opts.maxLimit 72 | } 73 | return (volume - opts.minLimit) / (opts.maxLimit - opts.minLimit) 74 | } 75 | 76 | _calcAvgVolume (data) { 77 | let sum = 0 78 | let len = data.length 79 | for (let i = 0; i < len; i++) { 80 | sum += data[i] 81 | } 82 | return sum / len 83 | } 84 | } 85 | 86 | export default VolumeMeter 87 | -------------------------------------------------------------------------------- /lib/util/assert.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | class Assertion { 8 | constructor (value) { 9 | this.value = value 10 | this._negative = false 11 | this._error = new Error('Assertion failed') 12 | } 13 | 14 | get to () { 15 | return this 16 | } 17 | 18 | get not () { 19 | this._negative = !this._negative 20 | return this 21 | } 22 | 23 | that (error) { 24 | this._error = error 25 | return this 26 | } 27 | 28 | equal (value) { 29 | if ((value === this.value) === this._negative) { 30 | throw this._error 31 | } 32 | } 33 | } 34 | 35 | function assert (value) { 36 | return new Assertion(value) 37 | } 38 | 39 | export default assert 40 | -------------------------------------------------------------------------------- /lib/util/emitter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | function slice (args, from) { 8 | return Array.prototype.slice.call(args, from) 9 | } 10 | 11 | class Emitter { 12 | constructor () { 13 | this.handlerMap = {} 14 | } 15 | 16 | on (event, handler) { 17 | if (typeof event !== 'string') { 18 | throw new TypeError(event + ' is not a string') 19 | } 20 | if (typeof handler !== 'function') { 21 | throw new TypeError(handler + ' is not a function') 22 | } 23 | let map = this.handlerMap 24 | let handlers = map[event] = map[event] || [] 25 | let i = handlers.indexOf(handler) 26 | if (i === -1) { 27 | handlers.push(handler) 28 | } 29 | return this 30 | } 31 | 32 | off (event, handler) { 33 | if (handler === undefined) { 34 | // remove all handlers 35 | delete this.handlerMap[event] 36 | return this 37 | } 38 | // remove registered handler 39 | let handlers = this.handlerMap[event] 40 | if (handlers) { 41 | let i = handlers.indexOf(handler) 42 | if (i >= 0) { 43 | handlers.splice(i, 1) 44 | } 45 | // cleanup empty handlers 46 | if (handlers.length === 0) { 47 | this.off(event) 48 | } 49 | } 50 | return this 51 | } 52 | 53 | emit (event) { 54 | let args = slice(arguments, 1) 55 | let handlers = this.handlerMap[event] 56 | if (handlers) { 57 | for (let i = 0, len = handlers.length; i < len; i++) { 58 | handlers[i].apply(undefined, args) 59 | } 60 | } 61 | return this 62 | } 63 | } 64 | 65 | export default Emitter 66 | -------------------------------------------------------------------------------- /lib/util/props.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | const hasOwn = Object.prototype.hasOwnProperty 8 | 9 | class PropPath { 10 | constructor (path) { 11 | this.steps = path.split('.') 12 | this.fallback = undefined 13 | } 14 | 15 | travel (target, fn) { 16 | if (typeof fn !== 'function') { 17 | throw new TypeError(fn + ' is not a function') 18 | } 19 | let len = this.steps.length 20 | let i = 0 21 | let step = this.steps[i] 22 | while (fn(target, step) && i < len) { 23 | target = target[step] 24 | step = this.steps[++i] 25 | } 26 | return { 27 | step: i, 28 | value: target 29 | } 30 | } 31 | 32 | or (defaultValue) { 33 | this.fallback = defaultValue 34 | return this 35 | } 36 | 37 | from (obj) { 38 | let ret = this.travel(obj, (target, step) => { 39 | return target != null && step in Object(target) 40 | }) 41 | if (ret.step === this.steps.length) { 42 | return ret.value 43 | } else { 44 | return this.fallback 45 | } 46 | } 47 | 48 | hadBy (obj) { 49 | let ret = this.travel(obj, (target, step) => { 50 | return target != null && step in Object(target) 51 | }) 52 | return ret.step === this.steps.length 53 | } 54 | 55 | ownedBy (obj) { 56 | let ret = this.travel(obj, (target, step) => { 57 | return target != null && hasOwn.call(target, step) 58 | }) 59 | return ret.step === this.steps.length 60 | } 61 | } 62 | 63 | function props (path) { 64 | if (typeof path !== 'string') { 65 | throw new TypeError(path + ' is not a string') 66 | } 67 | return new PropPath(path) 68 | } 69 | 70 | export default props 71 | -------------------------------------------------------------------------------- /lib/wavebell.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | import Recorder from '@/media/recorder' 8 | 9 | class WaveBell extends Recorder {} 10 | 11 | export default WaveBell 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wavebell", 3 | "version": "0.1.6", 4 | "description": "A javascript voice recorder with realtime waveform", 5 | "main": "dist/wavebell.esm.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/skylerlee/wavebell.git" 9 | }, 10 | "author": "skylerlee ", 11 | "license": "MIT", 12 | "files": [ 13 | "dist", 14 | "lib" 15 | ], 16 | "scripts": { 17 | "build:prod": "rollup -c build/rollup.config.prod.js", 18 | "build:test": "rollup -c build/rollup.config.test.js", 19 | "build:esm": "rollup -c build/rollup.config.esm.js", 20 | "build:clean": "rimraf ./dist", 21 | "lint:src": "eslint ./lib", 22 | "lint:spec": "coffeelint ./test", 23 | "build": "run-s lint:src build:prod build:esm", 24 | "rebuild": "run-s build:clean build", 25 | "start": "live-server --open=/demo --watch=demo,dist", 26 | "test:clean": "rimraf ./test_gen", 27 | "test:launch": "node test/launch.js", 28 | "test": "run-s test:clean lint:spec build:test test:launch", 29 | "coverage": "nyc report --reporter=text-lcov | coveralls" 30 | }, 31 | "devDependencies": { 32 | "babel-core": "^6.26.0", 33 | "babel-plugin-external-helpers": "^6.22.0", 34 | "babel-preset-env": "^1.6.1", 35 | "chai": "^4.1.2", 36 | "chrome-launcher": "^0.8.1", 37 | "coffeelint": "^2.0.7", 38 | "coffeescript": "^2.0.2", 39 | "coveralls": "^3.0.0", 40 | "eslint": "^4.10.0", 41 | "eslint-config-standard": "^10.2.1", 42 | "eslint-plugin-import": "^2.8.0", 43 | "eslint-plugin-node": "^5.2.1", 44 | "eslint-plugin-promise": "^3.6.0", 45 | "eslint-plugin-standard": "^3.0.1", 46 | "fs-extra": "^4.0.3", 47 | "live-server": "^1.2.1", 48 | "mocha": "^4.0.1", 49 | "npm-run-all": "^4.1.2", 50 | "nyc": "^11.3.0", 51 | "rimraf": "^2.6.2", 52 | "rollup": "^0.51.7", 53 | "rollup-plugin-alias": "^1.4.0", 54 | "rollup-plugin-babel": "^3.0.2", 55 | "rollup-plugin-coffee-script": "^2.0.0", 56 | "rollup-plugin-istanbul": "^2.0.0", 57 | "rollup-plugin-multi-entry": "^2.0.2", 58 | "rollup-plugin-replace": "^2.0.0", 59 | "uglify-js": "^3.1.9", 60 | "ws": "^3.3.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | The test file should be named as `*.spec.coffee`. 4 | 5 | All test files are bundled to `specs.bundle.js` file, and it will be executed 6 | under browser(chrome) environment. 7 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017, Skyler. 3 | * Use of this source code is governed by the MIT license that can be 4 | * found in the LICENSE file. 5 | */ 6 | 7 | // Add auto object serialization 8 | let origSend = WebSocket.prototype.send 9 | 10 | WebSocket.prototype.send = function (msg) { 11 | if (typeof msg === 'object') { 12 | msg = JSON.stringify(msg) 13 | } 14 | origSend.call(this, msg) 15 | } 16 | 17 | function redirect (socket) { 18 | console.log = function () { 19 | let msg = Array.prototype.slice.call(arguments, 0) 20 | socket.send({ 21 | type: 'log', 22 | data: msg 23 | }) 24 | } 25 | } 26 | 27 | function register (mocha) { 28 | // establish connection 29 | let socket = new WebSocket('ws://localhost:9020') 30 | socket.addEventListener('open', () => { 31 | if (process.env.NODE_ENV === 'testing') { 32 | redirect(socket) 33 | // use spec reporter 34 | mocha.setup({ 35 | reporter: 'spec' 36 | }) 37 | } 38 | // start runner 39 | let runner = mocha.run() 40 | runner.on('end', () => { 41 | socket.send({ 42 | type: 'done', 43 | failures: runner.failures, 44 | coverage: window.__coverage__ 45 | }) 46 | }) 47 | }) 48 | } 49 | 50 | exports.register = register 51 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha - Tests 6 | 7 | 8 | 9 |
10 | 11 | 12 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/launch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | const ws = require('ws') 5 | const fs = require('fs-extra') 6 | const path = require('path') 7 | const launcher = require('chrome-launcher') 8 | 9 | const TESTING_MODE = process.env.NODE_ENV === 'testing' 10 | const PROJECT_ROOT = path.resolve(__dirname, '..') 11 | const NYC_OUTPUT = path.join(PROJECT_ROOT, '.nyc_output') 12 | 13 | // chrome launcher options 14 | let chromeOpts = { 15 | chromeFlags: ['--allow-file-access-from-files'], 16 | startingUrl: `file://${__dirname}/index.html` 17 | } 18 | 19 | let server = new ws.Server({ 20 | port: 9020 21 | }) 22 | 23 | let browser = { 24 | inst: null, 25 | open () { 26 | launcher.launch(chromeOpts).then(chrome => { 27 | // promise not resolve in headless mode 28 | this.inst = chrome 29 | }) 30 | }, 31 | close () { 32 | if (TESTING_MODE) { 33 | this.inst.kill() 34 | } 35 | this.inst = null 36 | } 37 | } 38 | 39 | // browser message handler 40 | let handler = { 41 | init (socket) { 42 | socket.on('close', () => this.destroy()) 43 | socket.on('message', data => { 44 | let msg = JSON.parse(data) 45 | let slot = this[msg.type] || this.noop 46 | slot.call(this, msg) 47 | }) 48 | }, 49 | log (msg) { 50 | console.log.apply(console, msg.data) 51 | }, 52 | destroy () { 53 | let delay = 1000 54 | setTimeout(() => { 55 | // close server if not reconnected 56 | if (server.clients.size === 0) { 57 | server.close() 58 | } 59 | }, delay) 60 | }, 61 | report (output) { 62 | fs.ensureDir(NYC_OUTPUT).then(() => { 63 | let covFile = path.join(NYC_OUTPUT, 'coverage.json') 64 | fs.writeJson(covFile, output) 65 | }) 66 | }, 67 | done (msg) { 68 | if (msg.coverage) { 69 | this.report(msg.coverage) 70 | } 71 | if (msg.failures > 0) { 72 | process.exitCode = 1 73 | } 74 | browser.close() 75 | }, 76 | noop () {} 77 | } 78 | 79 | server.on('connection', socket => handler.init(socket)) 80 | server.on('listening', () => browser.open()) 81 | -------------------------------------------------------------------------------- /test/unit/assert.spec.coffee: -------------------------------------------------------------------------------- 1 | import assert from '@/util/assert' 2 | 3 | describe 'util/assert', -> 4 | it 'should accept an argument as value', -> 5 | expect(assert('a string').value).to.equal('a string') 6 | expect(assert(true).value).to.equal(true) 7 | expect(assert(100).value).to.equal(100) 8 | 9 | describe '.to', -> 10 | it 'should continue the chaining', -> 11 | assertion = assert('a value') 12 | expect(assertion.to).to.equal(assertion) 13 | expect(assertion.to).to.deep.equal(assertion) 14 | 15 | describe '.equal', -> 16 | it 'should meet basic functions', -> 17 | expect(-> assert('value1').to.equal('value1')).not.to.throw() 18 | expect(-> assert(100).to.equal(100)).not.to.throw() 19 | expect(-> assert('value2').to.equal('value0')).to.throw(Error, 'Assertion failed') 20 | expect(-> assert([]).to.equal([])).to.throw() 21 | expect(-> assert({}).to.equal({})).to.throw() 22 | 23 | describe '.not', -> 24 | it 'should negate the assertion', -> 25 | expect(-> assert('value1').to.not.equal('value0')).not.to.throw() 26 | expect(-> assert(100).to.not.equal(101)).not.to.throw() 27 | expect(-> assert({}).to.not.equal({})).not.to.throw() 28 | expect(-> assert('value2').to.not.equal('value2')).to.throw() 29 | 30 | it 'should work multiple times in the chaining', -> 31 | expect(-> assert(true).not.to.equal(false)).not.to.throw() 32 | expect(-> assert(true).not.to.not.equal(true)).not.to.throw() 33 | expect(-> assert(100).not.not.not.equal(101)).not.to.throw() 34 | 35 | describe '.that', -> 36 | it 'should set the custom error to throw', -> 37 | expect -> 38 | assert(typeof 'value1').that(new TypeError('Wrong type')) 39 | .to.equal('number') 40 | .to.throw(TypeError, 'Wrong type') 41 | 42 | expect -> 43 | assert(typeof 'value1').that(new TypeError('Mismatched type')) 44 | .not.to.equal('string') 45 | .to.throw(TypeError, 'Mismatched type') 46 | 47 | expect -> 48 | assert(typeof 100).that(new TypeError('Bad type')) 49 | .to.equal('number') 50 | .not.to.throw() 51 | -------------------------------------------------------------------------------- /test/unit/emitter.spec.coffee: -------------------------------------------------------------------------------- 1 | import Emitter from '@/util/emitter' 2 | 3 | describe 'util/emitter', -> 4 | describe '#on', -> 5 | it 'should only accept a string as first argument', -> 6 | e = new Emitter() 7 | expect(-> e.on('foo', ->)).not.to.throw() 8 | expect(-> e.on(12, ->)).to.throw(TypeError) 9 | expect(-> e.on(null, ->)).to.throw(TypeError) 10 | expect(-> e.on(undefined, ->)).to.throw(TypeError) 11 | expect(-> e.on({}, ->)).to.throw(TypeError) 12 | 13 | it 'should only accept a function as second argument', -> 14 | e = new Emitter() 15 | expect(-> e.on('foo', ->)).not.to.throw() 16 | expect(-> e.on('foo')).to.throw(TypeError) 17 | expect(-> e.on('foo', null)).to.throw(TypeError) 18 | expect(-> e.on('foo', 10)).to.throw(TypeError) 19 | expect(-> e.on('foo', 'callback')).to.throw(TypeError) 20 | 21 | it 'should create a handler array when event not registered', -> 22 | e = new Emitter() 23 | expect(Object.keys(e.handlerMap).length).to.equal(0) 24 | e.on('foo', ->) 25 | expect(Object.keys(e.handlerMap).length).to.equal(1) 26 | expect(e.handlerMap['foo'] instanceof Array).to.be.true 27 | e.on('bar', ->) 28 | expect(Object.keys(e.handlerMap).length).to.equal(2) 29 | expect(e.handlerMap['bar'] instanceof Array).to.be.true 30 | 31 | it 'should append an event handler otherwise', -> 32 | e = new Emitter() 33 | expect(Object.keys(e.handlerMap).length).to.equal(0) 34 | e.on('foo', callback1 = ->) 35 | e.on('foo', callback2 = ->) 36 | expect(Object.keys(e.handlerMap).length).to.equal(1) 37 | expect(e.handlerMap['foo'].length).to.equal(2) 38 | expect(e.handlerMap['foo'][0]).to.equal(callback1) 39 | expect(e.handlerMap['foo'][1]).to.equal(callback2) 40 | 41 | it 'should ignore appending duplicate event handler', -> 42 | e = new Emitter() 43 | callback = -> 44 | e.on('foo', callback) 45 | e.on('foo', callback) 46 | expect(Object.keys(e.handlerMap).length).to.equal(1) 47 | expect(e.handlerMap['foo'].length).to.equal(1) 48 | e.on('bar', ->) 49 | e.on('bar', callback) 50 | e.on('bar', callback) 51 | expect(Object.keys(e.handlerMap).length).to.equal(2) 52 | expect(e.handlerMap['bar'].length).to.equal(2) 53 | 54 | describe '#off', -> 55 | it 'should remove an event handler if it is found', -> 56 | e = new Emitter() 57 | e.on('foo', callback1 = ->) 58 | e.on('foo', callback2 = ->) 59 | e.on('foo', callback3 = ->) 60 | expect(Object.keys(e.handlerMap).length).to.equal(1) 61 | expect(e.handlerMap['foo'].length).to.equal(3) 62 | e.off('foo', callback2) 63 | expect(Object.keys(e.handlerMap).length).to.equal(1) 64 | expect(e.handlerMap['foo'].length).to.equal(2) 65 | expect(e.handlerMap['foo'][0]).to.equal(callback1) 66 | expect(e.handlerMap['foo'][1]).to.equal(callback3) 67 | 68 | it 'should remove all handlers if called with only one argument', -> 69 | e = new Emitter() 70 | e.on('foo', ->) 71 | e.on('foo', ->) 72 | expect(Object.keys(e.handlerMap).length).to.equal(1) 73 | e.off('foo') 74 | expect(Object.keys(e.handlerMap).length).to.equal(0) 75 | 76 | it 'should ignore removing handler of unregistered event', -> 77 | e = new Emitter() 78 | e.on('foo', callback1 = ->) 79 | e.on('foo', callback2 = ->) 80 | expect(e.handlerMap['foo'].length).to.equal(2) 81 | e.off('bar', callback1) 82 | expect(e.handlerMap['foo'].length).to.equal(2) 83 | e.off('bar') 84 | expect(e.handlerMap['foo'].length).to.equal(2) 85 | 86 | it 'should ignore removing handler if it is not found', -> 87 | e = new Emitter() 88 | e.on('foo', callback1 = ->) 89 | e.on('foo', callback2 = ->) 90 | expect(e.handlerMap['foo'].length).to.equal(2) 91 | e.off('foo', callback3 = ->) 92 | expect(e.handlerMap['foo'].length).to.equal(2) 93 | 94 | it 'should remove the handler array if it is empty', -> 95 | e = new Emitter() 96 | e.on('foo', callback1 = ->) 97 | e.on('foo', callback2 = ->) 98 | e.on('bar', ->) 99 | e.on('bar', ->) 100 | expect(Object.keys(e.handlerMap).length).to.equal(2) 101 | e.off('foo', callback1) 102 | e.off('foo', callback2) 103 | expect(Object.keys(e.handlerMap).length).to.equal(1) 104 | e.off('bar') 105 | expect(Object.keys(e.handlerMap).length).to.equal(0) 106 | 107 | describe '#emit', -> 108 | it 'should meet basic functions', -> 109 | called = false 110 | e = new Emitter() 111 | e.on('foo', -> called = true) 112 | e.emit('foo') 113 | expect(called).to.be.true 114 | 115 | it 'should support methods chaining', -> 116 | num = 0 117 | e = new Emitter() 118 | e.on('foo', -> num++).on('bar', -> num++).emit('foo').emit('bar') 119 | expect(num).to.equal(2) 120 | 121 | it 'should ignore emitting unregistered event', -> 122 | called = false 123 | e = new Emitter() 124 | e.on('foo', -> called = true) 125 | e.emit('bar') 126 | expect(called).to.be.false 127 | 128 | it 'should pass correct arguments to handlers', -> 129 | e = new Emitter() 130 | e.on 'foo', (a) -> 131 | expect(a).to.equal('foo data') 132 | data = { 133 | qux: 'qux data' 134 | } 135 | e.on 'bar', (a, b, c, d) -> 136 | expect(a).to.equal(1) 137 | expect(b).to.equal('bar data') 138 | expect(c).to.equal(true) 139 | expect(d).to.equal(data) 140 | e.on 'baz', (a) -> 141 | expect(a).to.be.undefined 142 | e.emit('foo', 'foo data') 143 | e.emit('bar', 1, 'bar data', true, data) 144 | e.emit('baz') 145 | 146 | it 'should call event handler at correct times', -> 147 | counter = { 148 | num: 0, 149 | acc: -> @num++, 150 | reset: -> @num = 0 151 | } 152 | e = new Emitter() 153 | e.on('foo', callback1 = -> counter.acc()) 154 | e.on('foo', callback2 = -> counter.acc()) 155 | e.emit('foo') 156 | expect(counter.num).to.equal(2) 157 | counter.reset() 158 | e.on('bar', -> counter.acc()) 159 | e.emit('bar') 160 | expect(counter.num).to.equal(1) 161 | counter.reset() 162 | e.emit('foo').emit('bar') 163 | expect(counter.num).to.equal(3) 164 | counter.reset() 165 | e.off('foo', callback1) 166 | e.off('bar') 167 | e.emit('foo').emit('bar') 168 | expect(counter.num).to.equal(1) 169 | counter.reset() 170 | e.off('foo', callback2) 171 | e.emit('foo').emit('bar') 172 | expect(counter.num).to.equal(0) 173 | counter.reset() 174 | -------------------------------------------------------------------------------- /test/unit/props.spec.coffee: -------------------------------------------------------------------------------- 1 | import props from '@/util/props' 2 | 3 | describe 'util/props', -> 4 | it 'should accept a string argument', -> 5 | expect(-> props('a')).not.to.throw() 6 | expect(-> props()).to.throw(TypeError) 7 | expect(-> props(['a'])).to.throw(TypeError) 8 | 9 | describe '.steps', -> 10 | it 'should match path argument', -> 11 | p = props('a.b.c') 12 | expect(p.steps.length).to.equal(3) 13 | expect(p.steps[0]).to.equal('a') 14 | expect(p.steps[1]).to.equal('b') 15 | expect(p.steps[2]).to.equal('c') 16 | p = props('a') 17 | expect(p.steps.length).to.equal(1) 18 | expect(p.steps[0]).to.equal('a') 19 | 20 | describe '.travel', -> 21 | it 'should accept function callback as second argument', -> 22 | expect(-> props('a').travel(null, (val) -> val)).not.to.throw() 23 | expect(-> props('a').travel(null, null)).to.throw(TypeError) 24 | 25 | describe '.from', -> 26 | obj = { 27 | a: { 28 | b: { 29 | c: 'value1' 30 | }, 31 | d: 'value2' 32 | }, 33 | e: 10 34 | f: [2, 4, 6, { 35 | g: 'value3', 36 | h: [1, 3, 5] 37 | }] 38 | } 39 | 40 | it 'should get correct value', -> 41 | expect(props('a.b.c').from(obj)).to.equal('value1') 42 | expect(props('a.d').from(obj)).to.equal('value2') 43 | expect(props('e').from(obj)).to.equal(10) 44 | expect(props('a.b').from(obj)).to.equal(obj.a.b) 45 | expect(props('a').from(obj)).to.equal(obj.a) 46 | 47 | it 'should also apply for array', -> 48 | expect(props('f.0').from(obj)).to.equal(2) 49 | expect(props('f.1').from(obj)).to.equal(4) 50 | expect(props('f.3.g').from(obj)).to.equal('value3') 51 | expect(props('f.3.h.length').from(obj)).to.equal(3) 52 | expect(props('f.3.h.2').from(obj)).to.equal(5) 53 | expect(props('f.3.h').from(obj)).to.equal(obj.f[3].h) 54 | 55 | it 'should get undefined otherwise', -> 56 | expect(props('a.b.d').from(obj)).to.be.undefined 57 | expect(props('a.d.f').from(obj)).to.be.undefined 58 | expect(props('e.g.h').from(obj)).to.be.undefined 59 | 60 | it 'should tolerate bad path', -> 61 | expect(props('').from(obj)).to.be.undefined 62 | expect(props('a.b.').from(obj)).to.be.undefined 63 | expect(props('.').from(obj)).to.be.undefined 64 | expect(props('..').from(obj)).to.be.undefined 65 | 66 | describe '.or', -> 67 | obj = { 68 | a: 'value1', 69 | b: undefined, 70 | c: { 71 | d: 10, 72 | e: undefined 73 | f: [2, 4, 6] 74 | } 75 | } 76 | 77 | it 'should set a default value for `from` method', -> 78 | expect(props('a').or('').from(obj)).to.equal('value1') 79 | expect(props('b').or('').from(obj)).to.be.undefined 80 | expect(props('h').or('').from(obj)).to.equal('') 81 | expect(props('c.d').or('').from(obj)).to.equal(10) 82 | expect(props('c.e').or('').from(obj)).to.be.undefined 83 | expect(props('c.h').or('').from(obj)).to.equal('') 84 | expect(props('c.f.1').or('').from(obj)).to.equal(4) 85 | expect(props('c.f.3').or(0).from(obj)).to.equal(0) 86 | 87 | plain = { 88 | a: { 89 | b: 'value1' 90 | }, 91 | c: 0, 92 | d: null, 93 | e: undefined 94 | } 95 | 96 | # Basic JS OOP 97 | Base = -> 98 | this.a = 'value1' 99 | Base.prototype.methodB = -> 100 | return 'value2' 101 | 102 | # Proto inheritance 103 | Derived = -> 104 | Base.call(this) 105 | this.c = 'value3' 106 | Derived.prototype = Object.create(Base.prototype) 107 | Derived.prototype.methodD = -> 108 | return 'value3' 109 | 110 | describe '.hadBy', -> 111 | it 'should meet basic functions', -> 112 | expect(props('a.b').hadBy(plain)).to.be.true 113 | expect(props('a').hadBy(plain)).to.be.true 114 | expect(props('c').hadBy(plain)).to.be.true 115 | expect(props('d').hadBy(plain)).to.be.true 116 | expect(props('e').hadBy(plain)).to.be.true 117 | expect(props('h').hadBy(plain)).to.be.false 118 | expect(props('a.b.c').hadBy(plain)).to.be.false 119 | expect(props('c.f').hadBy(plain)).to.be.false 120 | expect(props('d.f').hadBy(plain)).to.be.false 121 | expect(props('e.f').hadBy(plain)).to.be.false 122 | 123 | it 'should check for owned or inherited properties', -> 124 | base = new Base() 125 | derived = new Derived() 126 | expect(props('a').hadBy(base)).to.be.true 127 | expect(props('methodB').hadBy(base)).to.be.true 128 | expect(props('a').hadBy(derived)).to.be.true 129 | expect(props('c').hadBy(derived)).to.be.true 130 | expect(props('methodB').hadBy(derived)).to.be.true 131 | expect(props('methodD').hadBy(derived)).to.be.true 132 | 133 | describe '.ownedBy', -> 134 | it 'should meet basic functions', -> 135 | expect(props('a.b').ownedBy(plain)).to.be.true 136 | expect(props('a').ownedBy(plain)).to.be.true 137 | expect(props('c').ownedBy(plain)).to.be.true 138 | expect(props('d').ownedBy(plain)).to.be.true 139 | expect(props('e').ownedBy(plain)).to.be.true 140 | expect(props('h').ownedBy(plain)).to.be.false 141 | expect(props('a.b.c').ownedBy(plain)).to.be.false 142 | expect(props('c.f').ownedBy(plain)).to.be.false 143 | expect(props('d.f').ownedBy(plain)).to.be.false 144 | expect(props('e.f').ownedBy(plain)).to.be.false 145 | 146 | it 'should check for owned properties only', -> 147 | base = new Base() 148 | derived = new Derived() 149 | expect(props('a').ownedBy(base)).to.be.true 150 | expect(props('methodB').ownedBy(base)).to.be.false 151 | expect(props('a').ownedBy(derived)).to.be.true 152 | expect(props('c').ownedBy(derived)).to.be.true 153 | expect(props('methodB').ownedBy(derived)).to.be.false 154 | expect(props('methodD').ownedBy(derived)).to.be.false 155 | --------------------------------------------------------------------------------