├── .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 | [](https://travis-ci.org/skylerlee/wavebell)
4 | [](https://coveralls.io/github/skylerlee/wavebell)
5 | [](https://www.npmjs.com/package/wavebell)
6 |
7 | Catch realtime audio wave from microphone with JavaScript!
8 |
9 | ## Screenshot
10 | 
11 | 
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 |
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 |
--------------------------------------------------------------------------------