audiomotion-analyzer 3.5.0-beta.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +67 -37
- package/README.md +151 -65
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +515 -459
- package/src/index.d.ts +23 -7
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* audioMotion-analyzer
|
|
3
3
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
4
4
|
*
|
|
5
|
-
* @version 3.
|
|
5
|
+
* @version 3.6.0
|
|
6
6
|
* @author Henrique Avila Vianna <hvianna@gmail.com> <https://henriquevianna.com>
|
|
7
7
|
* @license AGPL-3.0-or-later
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const VERSION = '3.
|
|
10
|
+
const VERSION = '3.6.0';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
13
|
const TAU = 2 * Math.PI,
|
|
@@ -231,7 +231,14 @@ export default class AudioMotionAnalyzer {
|
|
|
231
231
|
* ==========================================================================
|
|
232
232
|
*/
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
|
|
235
|
+
get alphaBars() {
|
|
236
|
+
return this._alphaBars;
|
|
237
|
+
}
|
|
238
|
+
set alphaBars( value ) {
|
|
239
|
+
this._alphaBars = !! value;
|
|
240
|
+
this._calcAux();
|
|
241
|
+
}
|
|
235
242
|
|
|
236
243
|
get barSpace() {
|
|
237
244
|
return this._barSpace;
|
|
@@ -241,8 +248,6 @@ export default class AudioMotionAnalyzer {
|
|
|
241
248
|
this._calcAux();
|
|
242
249
|
}
|
|
243
250
|
|
|
244
|
-
// FFT size
|
|
245
|
-
|
|
246
251
|
get fftSize() {
|
|
247
252
|
return this._analyzer[0].fftSize;
|
|
248
253
|
}
|
|
@@ -254,8 +259,6 @@ export default class AudioMotionAnalyzer {
|
|
|
254
259
|
this._calcBars();
|
|
255
260
|
}
|
|
256
261
|
|
|
257
|
-
// Gradient
|
|
258
|
-
|
|
259
262
|
get gradient() {
|
|
260
263
|
return this._gradient;
|
|
261
264
|
}
|
|
@@ -267,8 +270,6 @@ export default class AudioMotionAnalyzer {
|
|
|
267
270
|
this._makeGrad();
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
// Canvas size
|
|
271
|
-
|
|
272
273
|
get height() {
|
|
273
274
|
return this._height;
|
|
274
275
|
}
|
|
@@ -276,15 +277,72 @@ export default class AudioMotionAnalyzer {
|
|
|
276
277
|
this._height = h;
|
|
277
278
|
this._setCanvas('user');
|
|
278
279
|
}
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
|
|
281
|
+
get ledBars() {
|
|
282
|
+
return this._showLeds;
|
|
281
283
|
}
|
|
282
|
-
set
|
|
283
|
-
this.
|
|
284
|
-
this.
|
|
284
|
+
set ledBars( value ) {
|
|
285
|
+
this._showLeds = !! value;
|
|
286
|
+
this._calcAux();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
get loRes() {
|
|
290
|
+
return this._loRes;
|
|
291
|
+
}
|
|
292
|
+
set loRes( value ) {
|
|
293
|
+
this._loRes = !! value;
|
|
294
|
+
this._setCanvas('lores');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get lumiBars() {
|
|
298
|
+
return this._lumiBars;
|
|
299
|
+
}
|
|
300
|
+
set lumiBars( value ) {
|
|
301
|
+
this._lumiBars = !! value;
|
|
302
|
+
this._calcAux();
|
|
303
|
+
this._calcLeds();
|
|
304
|
+
this._makeGrad();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
get maxDecibels() {
|
|
308
|
+
return this._analyzer[0].maxDecibels;
|
|
309
|
+
}
|
|
310
|
+
set maxDecibels( value ) {
|
|
311
|
+
for ( const i of [0,1] )
|
|
312
|
+
this._analyzer[ i ].maxDecibels = value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get maxFreq() {
|
|
316
|
+
return this._maxFreq;
|
|
317
|
+
}
|
|
318
|
+
set maxFreq( value ) {
|
|
319
|
+
if ( value < 1 )
|
|
320
|
+
throw new AudioMotionError( 'ERR_FREQUENCY_TOO_LOW', `Frequency values must be >= 1` );
|
|
321
|
+
else {
|
|
322
|
+
this._maxFreq = value;
|
|
323
|
+
this._calcBars();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get minDecibels() {
|
|
328
|
+
return this._analyzer[0].minDecibels;
|
|
329
|
+
}
|
|
330
|
+
set minDecibels( value ) {
|
|
331
|
+
for ( const i of [0,1] )
|
|
332
|
+
this._analyzer[ i ].minDecibels = value;
|
|
285
333
|
}
|
|
286
334
|
|
|
287
|
-
|
|
335
|
+
get minFreq() {
|
|
336
|
+
return this._minFreq;
|
|
337
|
+
}
|
|
338
|
+
set minFreq( value ) {
|
|
339
|
+
if ( value < 1 )
|
|
340
|
+
throw new AudioMotionError( 'ERR_FREQUENCY_TOO_LOW', `Frequency values must be >= 1` );
|
|
341
|
+
else {
|
|
342
|
+
this._minFreq = value;
|
|
343
|
+
this._calcBars();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
288
346
|
|
|
289
347
|
get mirror() {
|
|
290
348
|
return this._mirror;
|
|
@@ -296,8 +354,6 @@ export default class AudioMotionAnalyzer {
|
|
|
296
354
|
this._makeGrad();
|
|
297
355
|
}
|
|
298
356
|
|
|
299
|
-
// Visualization mode
|
|
300
|
-
|
|
301
357
|
get mode() {
|
|
302
358
|
return this._mode;
|
|
303
359
|
}
|
|
@@ -313,30 +369,14 @@ export default class AudioMotionAnalyzer {
|
|
|
313
369
|
throw new AudioMotionError( 'ERR_INVALID_MODE', `Invalid mode: ${value}` );
|
|
314
370
|
}
|
|
315
371
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
get loRes() {
|
|
319
|
-
return this._loRes;
|
|
320
|
-
}
|
|
321
|
-
set loRes( value ) {
|
|
322
|
-
this._loRes = !! value;
|
|
323
|
-
this._setCanvas('lores');
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Luminance bars
|
|
327
|
-
|
|
328
|
-
get lumiBars() {
|
|
329
|
-
return this._lumiBars;
|
|
372
|
+
get outlineBars() {
|
|
373
|
+
return this._outlineBars;
|
|
330
374
|
}
|
|
331
|
-
set
|
|
332
|
-
this.
|
|
375
|
+
set outlineBars( value ) {
|
|
376
|
+
this._outlineBars = !! value;
|
|
333
377
|
this._calcAux();
|
|
334
|
-
this._calcLeds();
|
|
335
|
-
this._makeGrad();
|
|
336
378
|
}
|
|
337
379
|
|
|
338
|
-
// Radial mode
|
|
339
|
-
|
|
340
380
|
get radial() {
|
|
341
381
|
return this._radial;
|
|
342
382
|
}
|
|
@@ -347,20 +387,6 @@ export default class AudioMotionAnalyzer {
|
|
|
347
387
|
this._makeGrad();
|
|
348
388
|
}
|
|
349
389
|
|
|
350
|
-
// Radial spin speed
|
|
351
|
-
|
|
352
|
-
get spinSpeed() {
|
|
353
|
-
return this._spinSpeed;
|
|
354
|
-
}
|
|
355
|
-
set spinSpeed( value ) {
|
|
356
|
-
value = +value || 0;
|
|
357
|
-
if ( this._spinSpeed === undefined || value == 0 )
|
|
358
|
-
this._spinAngle = -HALF_PI; // initialize or reset the rotation angle
|
|
359
|
-
this._spinSpeed = value;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Reflex
|
|
363
|
-
|
|
364
390
|
get reflexRatio() {
|
|
365
391
|
return this._reflexRatio;
|
|
366
392
|
}
|
|
@@ -376,60 +402,14 @@ export default class AudioMotionAnalyzer {
|
|
|
376
402
|
}
|
|
377
403
|
}
|
|
378
404
|
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
get minFreq() {
|
|
382
|
-
return this._minFreq;
|
|
383
|
-
}
|
|
384
|
-
set minFreq( value ) {
|
|
385
|
-
if ( value < 1 )
|
|
386
|
-
throw new AudioMotionError( 'ERR_FREQUENCY_TOO_LOW', `Frequency values must be >= 1` );
|
|
387
|
-
else {
|
|
388
|
-
this._minFreq = value;
|
|
389
|
-
this._calcBars();
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
get maxFreq() {
|
|
393
|
-
return this._maxFreq;
|
|
394
|
-
}
|
|
395
|
-
set maxFreq( value ) {
|
|
396
|
-
if ( value < 1 )
|
|
397
|
-
throw new AudioMotionError( 'ERR_FREQUENCY_TOO_LOW', `Frequency values must be >= 1` );
|
|
398
|
-
else {
|
|
399
|
-
this._maxFreq = value;
|
|
400
|
-
this._calcBars();
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Analyzer's sensitivity
|
|
405
|
-
|
|
406
|
-
get minDecibels() {
|
|
407
|
-
return this._analyzer[0].minDecibels;
|
|
408
|
-
}
|
|
409
|
-
set minDecibels( value ) {
|
|
410
|
-
for ( const i of [0,1] )
|
|
411
|
-
this._analyzer[ i ].minDecibels = value;
|
|
412
|
-
}
|
|
413
|
-
get maxDecibels() {
|
|
414
|
-
return this._analyzer[0].maxDecibels;
|
|
415
|
-
}
|
|
416
|
-
set maxDecibels( value ) {
|
|
417
|
-
for ( const i of [0,1] )
|
|
418
|
-
this._analyzer[ i ].maxDecibels = value;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// LEDs effect
|
|
422
|
-
|
|
405
|
+
// DEPRECATED - use ledBars instead
|
|
423
406
|
get showLeds() {
|
|
424
|
-
return this.
|
|
407
|
+
return this.ledBars;
|
|
425
408
|
}
|
|
426
409
|
set showLeds( value ) {
|
|
427
|
-
this.
|
|
428
|
-
this._calcAux();
|
|
410
|
+
this.ledBars = value;
|
|
429
411
|
}
|
|
430
412
|
|
|
431
|
-
// Analyzer's smoothing time constant
|
|
432
|
-
|
|
433
413
|
get smoothing() {
|
|
434
414
|
return this._analyzer[0].smoothingTimeConstant;
|
|
435
415
|
}
|
|
@@ -438,7 +418,15 @@ export default class AudioMotionAnalyzer {
|
|
|
438
418
|
this._analyzer[ i ].smoothingTimeConstant = value;
|
|
439
419
|
}
|
|
440
420
|
|
|
441
|
-
|
|
421
|
+
get spinSpeed() {
|
|
422
|
+
return this._spinSpeed;
|
|
423
|
+
}
|
|
424
|
+
set spinSpeed( value ) {
|
|
425
|
+
value = +value || 0;
|
|
426
|
+
if ( this._spinSpeed === undefined || value == 0 )
|
|
427
|
+
this._spinAngle = -HALF_PI; // initialize or reset the rotation angle
|
|
428
|
+
this._spinSpeed = value;
|
|
429
|
+
}
|
|
442
430
|
|
|
443
431
|
get splitGradient() {
|
|
444
432
|
return this._splitGradient;
|
|
@@ -448,8 +436,6 @@ export default class AudioMotionAnalyzer {
|
|
|
448
436
|
this._makeGrad();
|
|
449
437
|
}
|
|
450
438
|
|
|
451
|
-
// Stereo
|
|
452
|
-
|
|
453
439
|
get stereo() {
|
|
454
440
|
return this._stereo;
|
|
455
441
|
}
|
|
@@ -470,8 +456,6 @@ export default class AudioMotionAnalyzer {
|
|
|
470
456
|
this._makeGrad();
|
|
471
457
|
}
|
|
472
458
|
|
|
473
|
-
// Volume
|
|
474
|
-
|
|
475
459
|
get volume() {
|
|
476
460
|
return this._output.gain.value;
|
|
477
461
|
}
|
|
@@ -479,6 +463,14 @@ export default class AudioMotionAnalyzer {
|
|
|
479
463
|
this._output.gain.value = value;
|
|
480
464
|
}
|
|
481
465
|
|
|
466
|
+
get width() {
|
|
467
|
+
return this._width;
|
|
468
|
+
}
|
|
469
|
+
set width( w ) {
|
|
470
|
+
this._width = w;
|
|
471
|
+
this._setCanvas('user');
|
|
472
|
+
}
|
|
473
|
+
|
|
482
474
|
// Read only properties
|
|
483
475
|
|
|
484
476
|
get audioCtx() {
|
|
@@ -496,36 +488,43 @@ export default class AudioMotionAnalyzer {
|
|
|
496
488
|
get connectedTo() {
|
|
497
489
|
return this._outNodes;
|
|
498
490
|
}
|
|
499
|
-
get energy() {
|
|
500
|
-
// DEPRECATED - to be removed in v4.0.0
|
|
491
|
+
get energy() { // DEPRECATED - use getEnergy() instead
|
|
501
492
|
return this.getEnergy();
|
|
502
493
|
}
|
|
503
|
-
get
|
|
504
|
-
return this.
|
|
494
|
+
get fps() {
|
|
495
|
+
return this._fps;
|
|
505
496
|
}
|
|
506
497
|
get fsHeight() {
|
|
507
498
|
return this._fsHeight;
|
|
508
499
|
}
|
|
509
|
-
get
|
|
510
|
-
return this.
|
|
500
|
+
get fsWidth() {
|
|
501
|
+
return this._fsWidth;
|
|
502
|
+
}
|
|
503
|
+
get isAlphaBars() {
|
|
504
|
+
return this._isAlphaBars;
|
|
511
505
|
}
|
|
512
506
|
get isFullscreen() {
|
|
513
507
|
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
514
508
|
}
|
|
515
|
-
get
|
|
516
|
-
return this._isOctaveBands;
|
|
517
|
-
}
|
|
518
|
-
get isLedDisplay() {
|
|
509
|
+
get isLedBars() {
|
|
519
510
|
return this._isLedDisplay;
|
|
520
511
|
}
|
|
512
|
+
get isLedDisplay() { // DEPRECATED - use isLedBars instead
|
|
513
|
+
return this.isLedBars;
|
|
514
|
+
}
|
|
521
515
|
get isLumiBars() {
|
|
522
516
|
return this._isLumiBars;
|
|
523
517
|
}
|
|
518
|
+
get isOctaveBands() {
|
|
519
|
+
return this._isOctaveBands;
|
|
520
|
+
}
|
|
524
521
|
get isOn() {
|
|
525
522
|
return this._runId !== undefined;
|
|
526
523
|
}
|
|
527
|
-
get
|
|
528
|
-
|
|
524
|
+
get isOutlineBars() {
|
|
525
|
+
return this._isOutline;
|
|
526
|
+
}
|
|
527
|
+
get peakEnergy() { // DEPRECATED - use getEnergy('peak') instead
|
|
529
528
|
return this.getEnergy('peak');
|
|
530
529
|
}
|
|
531
530
|
get pixelRatio() {
|
|
@@ -566,6 +565,25 @@ export default class AudioMotionAnalyzer {
|
|
|
566
565
|
return node;
|
|
567
566
|
}
|
|
568
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Connects the analyzer output to another audio node
|
|
570
|
+
*
|
|
571
|
+
* @param [{object}] an AudioNode; if undefined, the output is connected to the audio context destination (speakers)
|
|
572
|
+
*/
|
|
573
|
+
connectOutput( node = this.audioCtx.destination ) {
|
|
574
|
+
if ( this._outNodes.includes( node ) )
|
|
575
|
+
return;
|
|
576
|
+
|
|
577
|
+
this._output.connect( node );
|
|
578
|
+
this._outNodes.push( node );
|
|
579
|
+
|
|
580
|
+
// when connecting the first node, also connect the analyzer nodes to the merger / output nodes
|
|
581
|
+
if ( this._outNodes.length == 1 ) {
|
|
582
|
+
for ( const i of [0,1] )
|
|
583
|
+
this._analyzer[ i ].connect( ( ! this._stereo && ! i ? this._output : this._merger ), 0, i );
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
569
587
|
/**
|
|
570
588
|
* Disconnects audio sources from the analyzer
|
|
571
589
|
*
|
|
@@ -586,25 +604,6 @@ export default class AudioMotionAnalyzer {
|
|
|
586
604
|
}
|
|
587
605
|
}
|
|
588
606
|
|
|
589
|
-
/**
|
|
590
|
-
* Connects the analyzer output to another audio node
|
|
591
|
-
*
|
|
592
|
-
* @param [{object}] an AudioNode; if undefined, the output is connected to the audio context destination (speakers)
|
|
593
|
-
*/
|
|
594
|
-
connectOutput( node = this.audioCtx.destination ) {
|
|
595
|
-
if ( this._outNodes.includes( node ) )
|
|
596
|
-
return;
|
|
597
|
-
|
|
598
|
-
this._output.connect( node );
|
|
599
|
-
this._outNodes.push( node );
|
|
600
|
-
|
|
601
|
-
// when connecting the first node, also connect the analyzer nodes to the merger / output nodes
|
|
602
|
-
if ( this._outNodes.length == 1 ) {
|
|
603
|
-
for ( const i of [0,1] )
|
|
604
|
-
this._analyzer[ i ].connect( ( ! this._stereo && ! i ? this._output : this._merger ), 0, i );
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
607
|
/**
|
|
609
608
|
* Disconnects the analyzer output from other audio nodes
|
|
610
609
|
*
|
|
@@ -646,7 +645,7 @@ export default class AudioMotionAnalyzer {
|
|
|
646
645
|
return this._energy.val;
|
|
647
646
|
|
|
648
647
|
// if startFreq is a string, check for presets
|
|
649
|
-
if ( startFreq !=
|
|
648
|
+
if ( startFreq != +startFreq ) {
|
|
650
649
|
if ( startFreq == 'peak' )
|
|
651
650
|
return this._energy.peak;
|
|
652
651
|
|
|
@@ -693,17 +692,15 @@ export default class AudioMotionAnalyzer {
|
|
|
693
692
|
if ( options.colorStops === undefined || options.colorStops.length < 2 )
|
|
694
693
|
throw new AudioMotionError( 'ERR_GRADIENT_MISSING_COLOR', 'Gradient must define at least two colors' );
|
|
695
694
|
|
|
696
|
-
this._gradients[ name ] = {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
this._gradients[ name ].bgColor = '#111';
|
|
702
|
-
|
|
703
|
-
if ( options.dir !== undefined )
|
|
704
|
-
this._gradients[ name ].dir = options.dir;
|
|
695
|
+
this._gradients[ name ] = {
|
|
696
|
+
bgColor: options.bgColor || '#111',
|
|
697
|
+
dir: options.dir,
|
|
698
|
+
colorStops: options.colorStops
|
|
699
|
+
};
|
|
705
700
|
|
|
706
|
-
|
|
701
|
+
// if the registered gradient is the current one, regenerate it
|
|
702
|
+
if ( name == this._gradient )
|
|
703
|
+
this._makeGrad();
|
|
707
704
|
}
|
|
708
705
|
|
|
709
706
|
/**
|
|
@@ -839,9 +836,11 @@ export default class AudioMotionAnalyzer {
|
|
|
839
836
|
|
|
840
837
|
this._radius = Math.min( canvas.width, canvas.height ) * ( this._stereo ? .375 : .125 ) | 0;
|
|
841
838
|
this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
|
|
842
|
-
this._isOctaveBands =
|
|
843
|
-
this._isLedDisplay =
|
|
844
|
-
this._isLumiBars =
|
|
839
|
+
this._isOctaveBands = this._mode % 10 != 0;
|
|
840
|
+
this._isLedDisplay = this._showLeds && this._isOctaveBands && ! isRadial;
|
|
841
|
+
this._isLumiBars = this._lumiBars && this._isOctaveBands && ! isRadial;
|
|
842
|
+
this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
|
|
843
|
+
this._isOutline = this._outlineBars && this._isOctaveBands && ! this._isLumiBars && ! this._isLedDisplay;
|
|
845
844
|
this._maximizeLeds = ! this._stereo || this._reflexRatio > 0 && ! this._isLumiBars;
|
|
846
845
|
|
|
847
846
|
this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
|
|
@@ -856,31 +855,210 @@ export default class AudioMotionAnalyzer {
|
|
|
856
855
|
}
|
|
857
856
|
|
|
858
857
|
/**
|
|
859
|
-
*
|
|
858
|
+
* Precalculate the actual X-coordinate on screen for each analyzer bar
|
|
860
859
|
*/
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
860
|
+
_calcBars() {
|
|
861
|
+
/*
|
|
862
|
+
Since the frequency scale is logarithmic, each position in the X-axis actually represents a power of 10.
|
|
863
|
+
To improve performace, the position of each frequency is calculated in advance and stored in an array.
|
|
864
|
+
Canvas space usage is optimized to accommodate exactly the frequency range the user needs.
|
|
865
|
+
Positions need to be recalculated whenever the frequency range, FFT size or canvas size change.
|
|
866
|
+
|
|
867
|
+
+-------------------------- canvas --------------------------+
|
|
868
|
+
| |
|
|
869
|
+
|-------------------|-----|-------------|-------------------!-------------------|------|------------|
|
|
870
|
+
1 10 | 100 1K 10K | 100K (Hz)
|
|
871
|
+
(10^0) (10^1) | (10^2) (10^3) (10^4) | (10^5)
|
|
872
|
+
|-------------|<--- logWidth ---->|--------------------------|
|
|
873
|
+
minFreq--> 20 (pixels) 22K <--maxFreq
|
|
874
|
+
(10^1.3) (10^4.34)
|
|
875
|
+
minLog
|
|
876
|
+
*/
|
|
864
877
|
|
|
865
|
-
|
|
866
|
-
const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
|
|
878
|
+
const bars = this._bars = []; // initialize object property
|
|
867
879
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
[ 128, 4, .225 ], // mode 2
|
|
871
|
-
[ 96, 6, .225 ], // mode 3
|
|
872
|
-
[ 80, 6, .225 ], // mode 4
|
|
873
|
-
[ 80, 6, .125 ], // mode 5
|
|
874
|
-
[ 64, 6, .125 ], // mode 6
|
|
875
|
-
[ 48, 8, .125 ], // mode 7
|
|
876
|
-
[ 24, 16, .125 ], // mode 8
|
|
877
|
-
];
|
|
880
|
+
if ( ! this._ready )
|
|
881
|
+
return;
|
|
878
882
|
|
|
879
|
-
//
|
|
880
|
-
const
|
|
881
|
-
|
|
883
|
+
// helper functions
|
|
884
|
+
const binToFreq = bin => bin * this.audioCtx.sampleRate / this.fftSize || 1; // returns 1 for bin 0
|
|
885
|
+
const barsPush = ( posX, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi ) => bars.push( { posX, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi, peak: [0,0], hold: [0], value: [0] } );
|
|
882
886
|
|
|
883
|
-
|
|
887
|
+
const analyzerWidth = this._analyzerWidth,
|
|
888
|
+
initialX = this._initialX,
|
|
889
|
+
maxFreq = this._maxFreq,
|
|
890
|
+
minFreq = this._minFreq;
|
|
891
|
+
|
|
892
|
+
let minLog, logWidth;
|
|
893
|
+
|
|
894
|
+
if ( this._isOctaveBands ) {
|
|
895
|
+
|
|
896
|
+
// generate a 11-octave 24-tone equal tempered scale (16Hz to 33kHz)
|
|
897
|
+
|
|
898
|
+
/*
|
|
899
|
+
A simple linear interpolation is used to obtain an approximate amplitude value for the desired frequency
|
|
900
|
+
from available FFT data, like so:
|
|
901
|
+
|
|
902
|
+
h = hLo + ( hHi - hLo ) * ( f - fLo ) / ( fHi - fLo )
|
|
903
|
+
\___________________________/
|
|
904
|
+
|
|
|
905
|
+
ratio
|
|
906
|
+
where:
|
|
907
|
+
|
|
908
|
+
f - desired frequency
|
|
909
|
+
h - amplitude of desired frequency
|
|
910
|
+
fLo - frequency represented by the lower FFT bin
|
|
911
|
+
fHi - frequency represented by the higher FFT bin
|
|
912
|
+
hLo - amplitude of fLo
|
|
913
|
+
hHi - amplitude of fHi
|
|
914
|
+
|
|
915
|
+
ratio is calculated in advance here, to reduce computational complexity during real-time rendering in the _draw() function
|
|
916
|
+
*/
|
|
917
|
+
|
|
918
|
+
let temperedScale = [];
|
|
919
|
+
|
|
920
|
+
for ( let octave = 0; octave < 11; octave++ ) {
|
|
921
|
+
for ( let note = 0; note < 24; note++ ) {
|
|
922
|
+
|
|
923
|
+
const freq = C0 * ROOT24 ** ( octave * 24 + note ),
|
|
924
|
+
bin = this._freqToBin( freq, 'floor' ),
|
|
925
|
+
binFreq = binToFreq( bin ),
|
|
926
|
+
nextFreq = binToFreq( bin + 1 ),
|
|
927
|
+
ratio = ( freq - binFreq ) / ( nextFreq - binFreq );
|
|
928
|
+
|
|
929
|
+
temperedScale.push( { freq, bin, ratio } );
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// generate the frequency bands according to current analyzer settings
|
|
934
|
+
|
|
935
|
+
const steps = [0,1,2,3,4,6,8,12,24][ this._mode ]; // number of notes grouped per band for each mode
|
|
936
|
+
|
|
937
|
+
for ( let index = 0; index < temperedScale.length; index += steps ) {
|
|
938
|
+
let { freq: freqLo, bin: binLo, ratio: ratioLo } = temperedScale[ index ], // band start
|
|
939
|
+
{ freq: freqHi, bin: binHi, ratio: ratioHi } = temperedScale[ index + steps - 1 ]; // band end
|
|
940
|
+
|
|
941
|
+
const nBars = bars.length,
|
|
942
|
+
prevBar = bars[ nBars - 1 ];
|
|
943
|
+
|
|
944
|
+
// if the ending frequency is out of range, we're done here
|
|
945
|
+
if ( freqHi > maxFreq || binHi >= this.fftSize / 2 ) {
|
|
946
|
+
prevBar.binHi++; // add an extra bin to the last bar, to fully include the last valid band
|
|
947
|
+
prevBar.ratioHi = 0; // disable interpolation
|
|
948
|
+
prevBar.freqHi = binToFreq( prevBar.binHi ); // update ending frequency
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// is the starting frequency in the selected range?
|
|
953
|
+
if ( freqLo >= minFreq ) {
|
|
954
|
+
if ( nBars > 0 ) {
|
|
955
|
+
const diff = binLo - prevBar.binHi;
|
|
956
|
+
|
|
957
|
+
// check if we skipped any available FFT bins since the last bar
|
|
958
|
+
if ( diff > 1 ) {
|
|
959
|
+
// allocate half of the unused bins to the previous bar
|
|
960
|
+
prevBar.binHi = binLo - ( diff >> 1 );
|
|
961
|
+
prevBar.ratioHi = 0;
|
|
962
|
+
prevBar.freqHi = binToFreq( prevBar.binHi ); // update ending frequency
|
|
963
|
+
|
|
964
|
+
// if the previous bar doesn't share any bins with other bars, no need for interpolation
|
|
965
|
+
if ( nBars > 1 && prevBar.binHi > prevBar.binLo && prevBar.binLo > bars[ nBars - 2 ].binHi ) {
|
|
966
|
+
prevBar.ratioLo = 0;
|
|
967
|
+
prevBar.freqLo = binToFreq( prevBar.binLo ); // update starting frequency
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// start the current bar at the bin following the last allocated bin
|
|
971
|
+
binLo = prevBar.binHi + 1;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// if the lower bin is not shared with the ending frequency nor the previous bar, no need to interpolate it
|
|
975
|
+
if ( binHi > binLo && binLo > prevBar.binHi ) {
|
|
976
|
+
ratioLo = 0;
|
|
977
|
+
freqLo = binToFreq( binLo );
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
barsPush( 0, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi );
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
this._barWidth = analyzerWidth / bars.length;
|
|
986
|
+
|
|
987
|
+
bars.forEach( ( bar, index ) => bar.posX = initialX + index * this._barWidth );
|
|
988
|
+
|
|
989
|
+
minLog = Math.log10( bars[0].freqLo );
|
|
990
|
+
logWidth = analyzerWidth / ( Math.log10( bars[ bars.length - 1 ].freqHi ) - minLog );
|
|
991
|
+
}
|
|
992
|
+
else {
|
|
993
|
+
|
|
994
|
+
// Discrete frequencies modes
|
|
995
|
+
|
|
996
|
+
this._barWidth = 1;
|
|
997
|
+
|
|
998
|
+
minLog = Math.log10( minFreq );
|
|
999
|
+
logWidth = analyzerWidth / ( Math.log10( maxFreq ) - minLog );
|
|
1000
|
+
|
|
1001
|
+
const minIndex = this._freqToBin( minFreq, 'floor' ),
|
|
1002
|
+
maxIndex = this._freqToBin( maxFreq );
|
|
1003
|
+
|
|
1004
|
+
let lastPos = -999;
|
|
1005
|
+
|
|
1006
|
+
for ( let i = minIndex; i <= maxIndex; i++ ) {
|
|
1007
|
+
const freq = binToFreq( i ), // frequency represented by this index
|
|
1008
|
+
pos = initialX + Math.round( logWidth * ( Math.log10( freq ) - minLog ) ); // avoid fractionary pixel values
|
|
1009
|
+
|
|
1010
|
+
// if it's on a different X-coordinate, create a new bar for this frequency
|
|
1011
|
+
if ( pos > lastPos ) {
|
|
1012
|
+
barsPush( pos, i, i, freq, freq, 0, 0 );
|
|
1013
|
+
lastPos = pos;
|
|
1014
|
+
} // otherwise, add this frequency to the last bar's range
|
|
1015
|
+
else if ( bars.length ) {
|
|
1016
|
+
bars[ bars.length - 1 ].binHi = i;
|
|
1017
|
+
bars[ bars.length - 1 ].freqHi = freq;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// save these for scale generation
|
|
1023
|
+
this._minLog = minLog;
|
|
1024
|
+
this._logWidth = logWidth;
|
|
1025
|
+
|
|
1026
|
+
// update internal variables
|
|
1027
|
+
this._calcAux();
|
|
1028
|
+
|
|
1029
|
+
// generate the X-axis and radial scales
|
|
1030
|
+
this._createScales();
|
|
1031
|
+
|
|
1032
|
+
// update LED properties
|
|
1033
|
+
this._calcLeds();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Calculate attributes for the vintage LEDs effect, based on visualization mode and canvas resolution
|
|
1038
|
+
*/
|
|
1039
|
+
_calcLeds() {
|
|
1040
|
+
if ( ! this._isOctaveBands || ! this._ready )
|
|
1041
|
+
return;
|
|
1042
|
+
|
|
1043
|
+
// adjustment for high pixel-ratio values on low-resolution screens (Android TV)
|
|
1044
|
+
const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
|
|
1045
|
+
|
|
1046
|
+
const params = [ [],
|
|
1047
|
+
[ 128, 3, .45 ], // mode 1
|
|
1048
|
+
[ 128, 4, .225 ], // mode 2
|
|
1049
|
+
[ 96, 6, .225 ], // mode 3
|
|
1050
|
+
[ 80, 6, .225 ], // mode 4
|
|
1051
|
+
[ 80, 6, .125 ], // mode 5
|
|
1052
|
+
[ 64, 6, .125 ], // mode 6
|
|
1053
|
+
[ 48, 8, .125 ], // mode 7
|
|
1054
|
+
[ 24, 16, .125 ], // mode 8
|
|
1055
|
+
];
|
|
1056
|
+
|
|
1057
|
+
// use custom LED parameters if set, or the default parameters for the current mode
|
|
1058
|
+
const customParams = this._ledParams,
|
|
1059
|
+
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ this._mode ];
|
|
1060
|
+
|
|
1061
|
+
let ledCount, spaceV,
|
|
884
1062
|
analyzerHeight = this._analyzerHeight;
|
|
885
1063
|
|
|
886
1064
|
if ( customParams ) {
|
|
@@ -915,6 +1093,72 @@ export default class AudioMotionAnalyzer {
|
|
|
915
1093
|
];
|
|
916
1094
|
}
|
|
917
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Generate the X-axis and radial scales in auxiliary canvases
|
|
1098
|
+
*/
|
|
1099
|
+
_createScales() {
|
|
1100
|
+
const freqLabels = [ 16, 31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 ],
|
|
1101
|
+
canvas = this._canvasCtx.canvas,
|
|
1102
|
+
scaleX = this._scaleX,
|
|
1103
|
+
scaleR = this._scaleR,
|
|
1104
|
+
canvasX = scaleX.canvas,
|
|
1105
|
+
canvasR = scaleR.canvas,
|
|
1106
|
+
scaleHeight = Math.min( canvas.width, canvas.height ) * .03 | 0; // circular scale height (radial mode)
|
|
1107
|
+
|
|
1108
|
+
// in radial stereo mode, the scale is positioned exactly between both channels, by making the canvas a bit larger than the central diameter
|
|
1109
|
+
canvasR.width = canvasR.height = ( this._radius << 1 ) + ( this._stereo * scaleHeight );
|
|
1110
|
+
|
|
1111
|
+
const radius = canvasR.width >> 1, // this is also used as the center X and Y coordinates of the circular scale canvas
|
|
1112
|
+
radialY = radius - scaleHeight * .7; // vertical position of text labels in the circular scale
|
|
1113
|
+
|
|
1114
|
+
// helper function
|
|
1115
|
+
const radialLabel = ( x, label ) => {
|
|
1116
|
+
const angle = TAU * ( x / canvas.width ),
|
|
1117
|
+
adjAng = angle - HALF_PI, // rotate angles so 0 is at the top
|
|
1118
|
+
posX = radialY * Math.cos( adjAng ),
|
|
1119
|
+
posY = radialY * Math.sin( adjAng );
|
|
1120
|
+
|
|
1121
|
+
scaleR.save();
|
|
1122
|
+
scaleR.translate( radius + posX, radius + posY );
|
|
1123
|
+
scaleR.rotate( angle );
|
|
1124
|
+
scaleR.fillText( label, 0, 0 );
|
|
1125
|
+
scaleR.restore();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// clear scale canvas
|
|
1129
|
+
canvasX.width |= 0;
|
|
1130
|
+
|
|
1131
|
+
scaleX.fillStyle = scaleR.strokeStyle = '#000c';
|
|
1132
|
+
scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1133
|
+
|
|
1134
|
+
scaleR.arc( radius, radius, radius - scaleHeight / 2, 0, TAU );
|
|
1135
|
+
scaleR.lineWidth = scaleHeight;
|
|
1136
|
+
scaleR.stroke();
|
|
1137
|
+
|
|
1138
|
+
scaleX.fillStyle = scaleR.fillStyle = '#fff';
|
|
1139
|
+
scaleX.font = `${ canvasX.height >> 1 }px sans-serif`;
|
|
1140
|
+
scaleR.font = `${ scaleHeight >> 1 }px sans-serif`;
|
|
1141
|
+
scaleX.textAlign = scaleR.textAlign = 'center';
|
|
1142
|
+
|
|
1143
|
+
for ( const freq of freqLabels ) {
|
|
1144
|
+
const label = ( freq >= 1000 ) ? `${ freq / 1000 }k` : freq,
|
|
1145
|
+
x = this._logWidth * ( Math.log10( freq ) - this._minLog );
|
|
1146
|
+
|
|
1147
|
+
if ( x >= 0 && x <= this._analyzerWidth ) {
|
|
1148
|
+
scaleX.fillText( label, this._initialX + x, canvasX.height * .75 );
|
|
1149
|
+
if ( x < this._analyzerWidth ) // avoid wrapping-around the last label and overlapping the first one
|
|
1150
|
+
radialLabel( x, label );
|
|
1151
|
+
|
|
1152
|
+
if ( this._mirror ) {
|
|
1153
|
+
scaleX.fillText( label, ( this._initialX || canvas.width ) - x, canvasX.height * .75 );
|
|
1154
|
+
if ( x > 10 ) // avoid overlapping of first labels on mirror mode
|
|
1155
|
+
radialLabel( -x, label );
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
918
1162
|
/**
|
|
919
1163
|
* Redraw the canvas
|
|
920
1164
|
* this is called 60 times per second by requestAnimationFrame()
|
|
@@ -925,14 +1169,16 @@ export default class AudioMotionAnalyzer {
|
|
|
925
1169
|
canvasX = this._scaleX.canvas,
|
|
926
1170
|
canvasR = this._scaleR.canvas,
|
|
927
1171
|
energy = this._energy,
|
|
928
|
-
|
|
1172
|
+
mode = this._mode,
|
|
1173
|
+
isAlphaBars = this._isAlphaBars,
|
|
929
1174
|
isLedDisplay = this._isLedDisplay,
|
|
930
1175
|
isLumiBars = this._isLumiBars,
|
|
1176
|
+
isOctaveBands = this._isOctaveBands,
|
|
1177
|
+
isOutline = this._isOutline,
|
|
931
1178
|
isRadial = this._radial,
|
|
932
1179
|
isStereo = this._stereo,
|
|
933
1180
|
lineWidth = +this.lineWidth, // make sure the damn thing is a number!
|
|
934
1181
|
mirrorMode = this._mirror,
|
|
935
|
-
mode = this._mode,
|
|
936
1182
|
channelHeight = this._channelHeight,
|
|
937
1183
|
channelGap = this._channelGap,
|
|
938
1184
|
analyzerHeight = this._analyzerHeight,
|
|
@@ -948,6 +1194,15 @@ export default class AudioMotionAnalyzer {
|
|
|
948
1194
|
if ( energy.val > 0 )
|
|
949
1195
|
this._spinAngle += this._spinSpeed * RPM;
|
|
950
1196
|
|
|
1197
|
+
const strokeIf = flag => {
|
|
1198
|
+
if ( flag && lineWidth ) {
|
|
1199
|
+
const alpha = ctx.globalAlpha;
|
|
1200
|
+
ctx.globalAlpha = 1;
|
|
1201
|
+
ctx.stroke();
|
|
1202
|
+
ctx.globalAlpha = alpha;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
951
1206
|
// helper function - convert planar X,Y coordinates to radial coordinates
|
|
952
1207
|
const radialXY = ( x, y, dir ) => {
|
|
953
1208
|
const height = radius + y,
|
|
@@ -957,13 +1212,17 @@ export default class AudioMotionAnalyzer {
|
|
|
957
1212
|
}
|
|
958
1213
|
|
|
959
1214
|
// helper function - draw a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
960
|
-
const radialPoly = ( x, y, w, h ) => {
|
|
1215
|
+
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1216
|
+
ctx.beginPath();
|
|
961
1217
|
for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
|
|
962
1218
|
ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
963
1219
|
ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
964
1220
|
ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
965
1221
|
ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
966
1222
|
}
|
|
1223
|
+
|
|
1224
|
+
strokeIf( stroke );
|
|
1225
|
+
ctx.fill();
|
|
967
1226
|
}
|
|
968
1227
|
|
|
969
1228
|
// LED attributes and helper function for bar height calculation
|
|
@@ -1059,6 +1318,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1059
1318
|
ctx.setLineDash( [ ledHeight, ledSpaceV ] );
|
|
1060
1319
|
ctx.lineWidth = width;
|
|
1061
1320
|
}
|
|
1321
|
+
else // for outline effect ensure linewidth is not greater than half the bar width
|
|
1322
|
+
ctx.lineWidth = isOutline ? Math.min( lineWidth, width / 2 ) : lineWidth;
|
|
1062
1323
|
|
|
1063
1324
|
// set selected gradient for fill and stroke
|
|
1064
1325
|
ctx.fillStyle = ctx.strokeStyle = this._canvasGradient;
|
|
@@ -1071,12 +1332,13 @@ export default class AudioMotionAnalyzer {
|
|
|
1071
1332
|
// helper function for FFT data interpolation
|
|
1072
1333
|
const interpolate = ( bin, ratio ) => fftData[ bin ] + ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio;
|
|
1073
1334
|
|
|
1074
|
-
// start drawing path
|
|
1335
|
+
// start drawing path (for mode 10)
|
|
1075
1336
|
ctx.beginPath();
|
|
1076
1337
|
|
|
1077
|
-
//
|
|
1338
|
+
// store line graph points to create mirror effect in radial mode
|
|
1339
|
+
let points = [];
|
|
1078
1340
|
|
|
1079
|
-
|
|
1341
|
+
// draw bars / lines
|
|
1080
1342
|
|
|
1081
1343
|
for ( let i = 0; i < nBars; i++ ) {
|
|
1082
1344
|
|
|
@@ -1113,9 +1375,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1113
1375
|
if ( ! useCanvas )
|
|
1114
1376
|
continue;
|
|
1115
1377
|
|
|
1116
|
-
// set opacity for
|
|
1117
|
-
if ( isLumiBars )
|
|
1378
|
+
// set opacity for bar effects
|
|
1379
|
+
if ( isLumiBars || isAlphaBars )
|
|
1118
1380
|
ctx.globalAlpha = barHeight;
|
|
1381
|
+
else if ( isOutline )
|
|
1382
|
+
ctx.globalAlpha = this.fillAlpha;
|
|
1119
1383
|
|
|
1120
1384
|
// normalize barHeight
|
|
1121
1385
|
if ( isLedDisplay ) {
|
|
@@ -1206,20 +1470,41 @@ export default class AudioMotionAnalyzer {
|
|
|
1206
1470
|
}
|
|
1207
1471
|
else if ( posX >= initialX ) {
|
|
1208
1472
|
if ( isRadial )
|
|
1209
|
-
radialPoly( posX, 0, adjWidth, barHeight );
|
|
1210
|
-
else
|
|
1211
|
-
|
|
1473
|
+
radialPoly( posX, 0, adjWidth, barHeight, isOutline );
|
|
1474
|
+
else {
|
|
1475
|
+
const x = posX,
|
|
1476
|
+
y = isLumiBars ? channelTop : analyzerBottom,
|
|
1477
|
+
w = adjWidth,
|
|
1478
|
+
h = isLumiBars ? channelBottom : -barHeight;
|
|
1479
|
+
|
|
1480
|
+
ctx.beginPath();
|
|
1481
|
+
ctx.moveTo( x, y );
|
|
1482
|
+
ctx.lineTo( x, y + h );
|
|
1483
|
+
ctx.lineTo( x + w, y + h );
|
|
1484
|
+
ctx.lineTo( x + w, y );
|
|
1485
|
+
|
|
1486
|
+
strokeIf( isOutline );
|
|
1487
|
+
ctx.fill();
|
|
1488
|
+
}
|
|
1212
1489
|
}
|
|
1213
1490
|
}
|
|
1214
1491
|
|
|
1215
1492
|
// Draw peak
|
|
1216
|
-
|
|
1493
|
+
const peak = bar.peak[ channel ];
|
|
1494
|
+
if ( peak > 0 && this.showPeaks && ! isLumiBars && posX >= initialX && posX < finalX ) {
|
|
1495
|
+
// choose the best opacity for the peaks
|
|
1496
|
+
if ( isOutline && lineWidth > 0 )
|
|
1497
|
+
ctx.globalAlpha = 1;
|
|
1498
|
+
else if ( isAlphaBars )
|
|
1499
|
+
ctx.globalAlpha = peak;
|
|
1500
|
+
|
|
1501
|
+
// render peak according to current mode / effect
|
|
1217
1502
|
if ( isLedDisplay )
|
|
1218
|
-
ctx.fillRect( posX, analyzerBottom - ledPosY(
|
|
1503
|
+
ctx.fillRect( posX, analyzerBottom - ledPosY( peak ), width, ledHeight );
|
|
1219
1504
|
else if ( ! isRadial )
|
|
1220
|
-
ctx.fillRect( posX, analyzerBottom -
|
|
1505
|
+
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
|
|
1221
1506
|
else if ( mode != 10 ) // radial - no peaks for mode 10
|
|
1222
|
-
radialPoly( posX,
|
|
1507
|
+
radialPoly( posX, peak * maxBarHeight * ( ! channel || -1 ), adjWidth, -2 );
|
|
1223
1508
|
}
|
|
1224
1509
|
|
|
1225
1510
|
} // for ( let i = 0; i < nBars; i++ )
|
|
@@ -1231,7 +1516,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1231
1516
|
// restore global alpha
|
|
1232
1517
|
ctx.globalAlpha = 1;
|
|
1233
1518
|
|
|
1234
|
-
// Fill/stroke drawing path for mode 10
|
|
1519
|
+
// Fill/stroke drawing path for mode 10
|
|
1235
1520
|
if ( mode == 10 ) {
|
|
1236
1521
|
if ( isRadial ) {
|
|
1237
1522
|
if ( mirrorMode ) {
|
|
@@ -1242,10 +1527,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1242
1527
|
ctx.closePath();
|
|
1243
1528
|
}
|
|
1244
1529
|
|
|
1245
|
-
if ( lineWidth > 0 )
|
|
1246
|
-
ctx.lineWidth = lineWidth;
|
|
1530
|
+
if ( lineWidth > 0 )
|
|
1247
1531
|
ctx.stroke();
|
|
1248
|
-
}
|
|
1249
1532
|
|
|
1250
1533
|
if ( this.fillAlpha > 0 ) {
|
|
1251
1534
|
if ( isRadial ) {
|
|
@@ -1263,9 +1546,6 @@ export default class AudioMotionAnalyzer {
|
|
|
1263
1546
|
ctx.globalAlpha = 1;
|
|
1264
1547
|
}
|
|
1265
1548
|
}
|
|
1266
|
-
else if ( isRadial ) {
|
|
1267
|
-
ctx.fill();
|
|
1268
|
-
}
|
|
1269
1549
|
|
|
1270
1550
|
// Reflex effect
|
|
1271
1551
|
if ( this._reflexRatio > 0 && ! isLumiBars ) {
|
|
@@ -1365,6 +1645,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1365
1645
|
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1366
1646
|
}
|
|
1367
1647
|
|
|
1648
|
+
/**
|
|
1649
|
+
* Return the FFT data bin (array index) which represents a given frequency
|
|
1650
|
+
*/
|
|
1651
|
+
_freqToBin( freq, rounding = 'round' ) {
|
|
1652
|
+
const max = this._analyzer[0].frequencyBinCount - 1,
|
|
1653
|
+
bin = Math[ rounding ]( freq * this.fftSize / this.audioCtx.sampleRate );
|
|
1654
|
+
|
|
1655
|
+
return bin < max ? bin : max;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1368
1658
|
/**
|
|
1369
1659
|
* Generate currently selected gradient
|
|
1370
1660
|
*/
|
|
@@ -1454,247 +1744,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1454
1744
|
}
|
|
1455
1745
|
|
|
1456
1746
|
/**
|
|
1457
|
-
*
|
|
1458
|
-
*/
|
|
1459
|
-
_createScales() {
|
|
1460
|
-
const freqLabels = [ 16, 31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 ],
|
|
1461
|
-
canvas = this._canvasCtx.canvas,
|
|
1462
|
-
scaleX = this._scaleX,
|
|
1463
|
-
scaleR = this._scaleR,
|
|
1464
|
-
canvasX = scaleX.canvas,
|
|
1465
|
-
canvasR = scaleR.canvas,
|
|
1466
|
-
scaleHeight = Math.min( canvas.width, canvas.height ) * .03 | 0; // circular scale height (radial mode)
|
|
1467
|
-
|
|
1468
|
-
// in radial stereo mode, the scale is positioned exactly between both channels, by making the canvas a bit larger than the central diameter
|
|
1469
|
-
canvasR.width = canvasR.height = ( this._radius << 1 ) + ( this._stereo * scaleHeight );
|
|
1470
|
-
|
|
1471
|
-
const radius = canvasR.width >> 1, // this is also used as the center X and Y coordinates of the circular scale canvas
|
|
1472
|
-
radialY = radius - scaleHeight * .7; // vertical position of text labels in the circular scale
|
|
1473
|
-
|
|
1474
|
-
// helper function
|
|
1475
|
-
const radialLabel = ( x, label ) => {
|
|
1476
|
-
const angle = TAU * ( x / canvas.width ),
|
|
1477
|
-
adjAng = angle - HALF_PI, // rotate angles so 0 is at the top
|
|
1478
|
-
posX = radialY * Math.cos( adjAng ),
|
|
1479
|
-
posY = radialY * Math.sin( adjAng );
|
|
1480
|
-
|
|
1481
|
-
scaleR.save();
|
|
1482
|
-
scaleR.translate( radius + posX, radius + posY );
|
|
1483
|
-
scaleR.rotate( angle );
|
|
1484
|
-
scaleR.fillText( label, 0, 0 );
|
|
1485
|
-
scaleR.restore();
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
// clear scale canvas
|
|
1489
|
-
canvasX.width |= 0;
|
|
1490
|
-
|
|
1491
|
-
scaleX.fillStyle = scaleR.strokeStyle = '#000c';
|
|
1492
|
-
scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1493
|
-
|
|
1494
|
-
scaleR.arc( radius, radius, radius - scaleHeight / 2, 0, TAU );
|
|
1495
|
-
scaleR.lineWidth = scaleHeight;
|
|
1496
|
-
scaleR.stroke();
|
|
1497
|
-
|
|
1498
|
-
scaleX.fillStyle = scaleR.fillStyle = '#fff';
|
|
1499
|
-
scaleX.font = `${ canvasX.height >> 1 }px sans-serif`;
|
|
1500
|
-
scaleR.font = `${ scaleHeight >> 1 }px sans-serif`;
|
|
1501
|
-
scaleX.textAlign = scaleR.textAlign = 'center';
|
|
1502
|
-
|
|
1503
|
-
for ( const freq of freqLabels ) {
|
|
1504
|
-
const label = ( freq >= 1000 ) ? `${ freq / 1000 }k` : freq,
|
|
1505
|
-
x = this._logWidth * ( Math.log10( freq ) - this._minLog );
|
|
1506
|
-
|
|
1507
|
-
if ( x >= 0 && x <= this._analyzerWidth ) {
|
|
1508
|
-
scaleX.fillText( label, this._initialX + x, canvasX.height * .75 );
|
|
1509
|
-
if ( x < this._analyzerWidth ) // avoid wrapping-around the last label and overlapping the first one
|
|
1510
|
-
radialLabel( x, label );
|
|
1511
|
-
|
|
1512
|
-
if ( this._mirror ) {
|
|
1513
|
-
scaleX.fillText( label, ( this._initialX || canvas.width ) - x, canvasX.height * .75 );
|
|
1514
|
-
if ( x > 10 ) // avoid overlapping of first labels on mirror mode
|
|
1515
|
-
radialLabel( -x, label );
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/**
|
|
1523
|
-
* Precalculate the actual X-coordinate on screen for each analyzer bar
|
|
1524
|
-
*
|
|
1525
|
-
* Since the frequency scale is logarithmic, each position in the X-axis actually represents a power of 10.
|
|
1526
|
-
* To improve performace, the position of each frequency is calculated in advance and stored in an array.
|
|
1527
|
-
* Canvas space usage is optimized to accommodate exactly the frequency range the user needs.
|
|
1528
|
-
* Positions need to be recalculated whenever the frequency range, FFT size or canvas size change.
|
|
1529
|
-
*
|
|
1530
|
-
* +-------------------------- canvas --------------------------+
|
|
1531
|
-
* | |
|
|
1532
|
-
* |-------------------|-----|-------------|-------------------!-------------------|------|------------|
|
|
1533
|
-
* 1 10 | 100 1K 10K | 100K (Hz)
|
|
1534
|
-
* (10^0) (10^1) | (10^2) (10^3) (10^4) | (10^5)
|
|
1535
|
-
* |-------------|<--- logWidth ---->|--------------------------|
|
|
1536
|
-
* minFreq--> 20 (pixels) 22K <--maxFreq
|
|
1537
|
-
* (10^1.3) (10^4.34)
|
|
1538
|
-
* minLog
|
|
1539
|
-
*/
|
|
1540
|
-
_calcBars() {
|
|
1541
|
-
|
|
1542
|
-
const bars = this._bars = []; // initialize object property
|
|
1543
|
-
|
|
1544
|
-
if ( ! this._ready )
|
|
1545
|
-
return;
|
|
1546
|
-
|
|
1547
|
-
// helper functions
|
|
1548
|
-
const binToFreq = bin => bin * this.audioCtx.sampleRate / this.fftSize || 1; // returns 1 for bin 0
|
|
1549
|
-
const barsPush = ( posX, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi ) => bars.push( { posX, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi, peak: [0,0], hold: [0], value: [0] } );
|
|
1550
|
-
|
|
1551
|
-
const analyzerWidth = this._analyzerWidth,
|
|
1552
|
-
initialX = this._initialX,
|
|
1553
|
-
maxFreq = this._maxFreq,
|
|
1554
|
-
minFreq = this._minFreq;
|
|
1555
|
-
|
|
1556
|
-
let minLog, logWidth;
|
|
1557
|
-
|
|
1558
|
-
if ( this._isOctaveBands ) {
|
|
1559
|
-
|
|
1560
|
-
// generate a 11-octave 24-tone equal tempered scale (16Hz to 33kHz)
|
|
1561
|
-
|
|
1562
|
-
/*
|
|
1563
|
-
A simple linear interpolation is used to get an approximate amplitude value for the desired frequency from the available FFT data,
|
|
1564
|
-
like so:
|
|
1565
|
-
|
|
1566
|
-
h = hLo + ( hHi - hLo ) * ( f - fLo ) / ( fHi - fLo )
|
|
1567
|
-
\___________________________/
|
|
1568
|
-
|
|
|
1569
|
-
ratio
|
|
1570
|
-
where:
|
|
1571
|
-
|
|
1572
|
-
f - desired frequency
|
|
1573
|
-
h - amplitude of desired frequency
|
|
1574
|
-
fLo - frequency represented by the lower FFT bin
|
|
1575
|
-
fHi - frequency represented by the higher FFT bin
|
|
1576
|
-
hLo - amplitude of fLo
|
|
1577
|
-
hHi - amplitude of fHi
|
|
1578
|
-
|
|
1579
|
-
ratio is calculated in advance here, to reduce computational complexity during real-time rendering in the _draw() function
|
|
1580
|
-
*/
|
|
1581
|
-
|
|
1582
|
-
let temperedScale = [];
|
|
1583
|
-
|
|
1584
|
-
for ( let octave = 0; octave < 11; octave++ ) {
|
|
1585
|
-
for ( let note = 0; note < 24; note++ ) {
|
|
1586
|
-
|
|
1587
|
-
const freq = C0 * ROOT24 ** ( octave * 24 + note ),
|
|
1588
|
-
bin = this._freqToBin( freq, 'floor' ),
|
|
1589
|
-
binFreq = binToFreq( bin ),
|
|
1590
|
-
nextFreq = binToFreq( bin + 1 ),
|
|
1591
|
-
ratio = ( freq - binFreq ) / ( nextFreq - binFreq );
|
|
1592
|
-
|
|
1593
|
-
temperedScale.push( { freq, bin, ratio } );
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// generate the frequency bands according to current analyzer settings
|
|
1598
|
-
|
|
1599
|
-
const steps = [0,1,2,3,4,6,8,12,24][ this._mode ]; // number of notes grouped per band for each mode
|
|
1600
|
-
|
|
1601
|
-
for ( let index = 0; index < temperedScale.length; index += steps ) {
|
|
1602
|
-
let { freq: freqLo, bin: binLo, ratio: ratioLo } = temperedScale[ index ],
|
|
1603
|
-
{ freq: freqHi, bin: binHi, ratio: ratioHi } = temperedScale[ index + steps - 1 ];
|
|
1604
|
-
|
|
1605
|
-
const nBars = bars.length,
|
|
1606
|
-
prevBar = bars[ nBars - 1 ];
|
|
1607
|
-
|
|
1608
|
-
if ( freqHi > maxFreq || binHi >= this.fftSize / 2 ) {
|
|
1609
|
-
prevBar.binHi++;
|
|
1610
|
-
prevBar.ratioHi = 0;
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
if ( freqLo >= minFreq ) {
|
|
1615
|
-
if ( nBars > 0 ) {
|
|
1616
|
-
const diff = binLo - prevBar.binHi;
|
|
1617
|
-
|
|
1618
|
-
if ( diff > 1 ) {
|
|
1619
|
-
// allocate half of the unused bins to the previous bar
|
|
1620
|
-
prevBar.binHi = binLo - ( diff >> 1 );
|
|
1621
|
-
prevBar.ratioHi = 0;
|
|
1622
|
-
if ( nBars > 1 && prevBar.binHi > prevBar.binLo && prevBar.binLo > bars[ nBars - 2 ].binHi )
|
|
1623
|
-
prevBar.ratioLo = 0;
|
|
1624
|
-
binLo = prevBar.binHi + 1;
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
if ( binHi > binLo && binLo > prevBar.binHi )
|
|
1628
|
-
ratioLo = 0;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
barsPush( 0, binLo, binHi, freqLo, freqHi, ratioLo, ratioHi );
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
this._barWidth = analyzerWidth / bars.length;
|
|
1636
|
-
|
|
1637
|
-
bars.forEach( ( bar, index ) => bar.posX = initialX + index * this._barWidth );
|
|
1638
|
-
|
|
1639
|
-
minLog = Math.log10( bars[0].freqLo );
|
|
1640
|
-
logWidth = analyzerWidth / ( Math.log10( bars[ bars.length - 1 ].freqHi ) - minLog );
|
|
1641
|
-
}
|
|
1642
|
-
else {
|
|
1643
|
-
|
|
1644
|
-
// Discrete frequencies modes
|
|
1645
|
-
|
|
1646
|
-
this._barWidth = 1;
|
|
1647
|
-
|
|
1648
|
-
minLog = Math.log10( minFreq );
|
|
1649
|
-
logWidth = analyzerWidth / ( Math.log10( maxFreq ) - minLog );
|
|
1650
|
-
|
|
1651
|
-
const minIndex = this._freqToBin( minFreq, 'floor' ),
|
|
1652
|
-
maxIndex = this._freqToBin( maxFreq );
|
|
1653
|
-
|
|
1654
|
-
let lastPos = -999;
|
|
1655
|
-
|
|
1656
|
-
for ( let i = minIndex; i <= maxIndex; i++ ) {
|
|
1657
|
-
const freq = binToFreq( i ), // frequency represented by this index
|
|
1658
|
-
pos = initialX + Math.round( logWidth * ( Math.log10( freq ) - minLog ) ); // avoid fractionary pixel values
|
|
1659
|
-
|
|
1660
|
-
// if it's on a different X-coordinate, create a new bar for this frequency
|
|
1661
|
-
if ( pos > lastPos ) {
|
|
1662
|
-
barsPush( pos, i, i, freq, freq, 0, 0 );
|
|
1663
|
-
lastPos = pos;
|
|
1664
|
-
} // otherwise, add this frequency to the last bar's range
|
|
1665
|
-
else if ( bars.length ) {
|
|
1666
|
-
bars[ bars.length - 1 ].binHi = i;
|
|
1667
|
-
bars[ bars.length - 1 ].freqHi = freq;
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
// save these for scale generation
|
|
1673
|
-
this._minLog = minLog;
|
|
1674
|
-
this._logWidth = logWidth;
|
|
1675
|
-
|
|
1676
|
-
// update internal variables
|
|
1677
|
-
this._calcAux();
|
|
1678
|
-
|
|
1679
|
-
// generate the X-axis and radial scales
|
|
1680
|
-
this._createScales();
|
|
1681
|
-
|
|
1682
|
-
// update LED properties
|
|
1683
|
-
this._calcLeds();
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
/**
|
|
1687
|
-
* Return the FFT data bin (array index) which represents a given frequency
|
|
1688
|
-
*/
|
|
1689
|
-
_freqToBin( freq, rounding = 'round' ) {
|
|
1690
|
-
const max = this._analyzer[0].frequencyBinCount - 1,
|
|
1691
|
-
bin = Math[ rounding ]( freq * this.fftSize / this.audioCtx.sampleRate );
|
|
1692
|
-
|
|
1693
|
-
return bin < max ? bin : max;
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
/**
|
|
1697
|
-
* Internal function to change canvas dimensions on demand
|
|
1747
|
+
* Internal function to change canvas dimensions on demand
|
|
1698
1748
|
*/
|
|
1699
1749
|
_setCanvas( reason ) {
|
|
1700
1750
|
// if initialization is not finished, quit
|
|
@@ -1770,49 +1820,55 @@ export default class AudioMotionAnalyzer {
|
|
|
1770
1820
|
|
|
1771
1821
|
// settings defaults
|
|
1772
1822
|
const defaults = {
|
|
1773
|
-
|
|
1823
|
+
alphaBars : false,
|
|
1824
|
+
barSpace : 0.1,
|
|
1825
|
+
bgAlpha : 0.7,
|
|
1774
1826
|
fftSize : 8192,
|
|
1775
|
-
|
|
1776
|
-
maxFreq : 22000,
|
|
1777
|
-
smoothing : 0.5,
|
|
1827
|
+
fillAlpha : 1,
|
|
1778
1828
|
gradient : 'classic',
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
showBgColor : true,
|
|
1782
|
-
showLeds : false,
|
|
1783
|
-
showScaleX : true,
|
|
1784
|
-
showScaleY : false,
|
|
1785
|
-
showPeaks : true,
|
|
1786
|
-
showFPS : false,
|
|
1787
|
-
lumiBars : false,
|
|
1829
|
+
ledBars : false,
|
|
1830
|
+
lineWidth : 0,
|
|
1788
1831
|
loRes : false,
|
|
1789
|
-
|
|
1832
|
+
lumiBars : false,
|
|
1833
|
+
maxDecibels : -25,
|
|
1834
|
+
maxFreq : 22000,
|
|
1835
|
+
minDecibels : -85,
|
|
1836
|
+
minFreq : 20,
|
|
1837
|
+
mirror : 0,
|
|
1838
|
+
mode : 0,
|
|
1839
|
+
outlineBars : false,
|
|
1840
|
+
overlay : false,
|
|
1841
|
+
radial : false,
|
|
1790
1842
|
reflexAlpha : 0.15,
|
|
1791
1843
|
reflexBright : 1,
|
|
1792
1844
|
reflexFit : true,
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1845
|
+
reflexRatio : 0,
|
|
1846
|
+
showBgColor : true,
|
|
1847
|
+
showFPS : false,
|
|
1848
|
+
showPeaks : true,
|
|
1849
|
+
showScaleX : true,
|
|
1850
|
+
showScaleY : false,
|
|
1851
|
+
smoothing : 0.5,
|
|
1799
1852
|
spinSpeed : 0,
|
|
1800
|
-
stereo : false,
|
|
1801
1853
|
splitGradient: false,
|
|
1802
1854
|
start : true,
|
|
1855
|
+
stereo : false,
|
|
1856
|
+
useCanvas : true,
|
|
1803
1857
|
volume : 1,
|
|
1804
|
-
mirror : 0,
|
|
1805
|
-
useCanvas : true
|
|
1806
1858
|
};
|
|
1807
1859
|
|
|
1808
1860
|
// callback functions properties
|
|
1809
1861
|
const callbacks = [ 'onCanvasDraw', 'onCanvasResize' ];
|
|
1810
1862
|
|
|
1811
|
-
//
|
|
1812
|
-
const validProps = Object.keys( defaults ).concat( callbacks, ['height', 'width'] )
|
|
1863
|
+
// build an array of valid properties; `start` is not an actual property and is handled after setting everything else
|
|
1864
|
+
const validProps = Object.keys( defaults ).filter( e => e != 'start' ).concat( callbacks, ['height', 'width'] );
|
|
1865
|
+
|
|
1866
|
+
// handle deprecated `showLeds` property
|
|
1867
|
+
if ( options && options.showLeds !== undefined && options.ledBars === undefined )
|
|
1868
|
+
options.ledBars = options.showLeds;
|
|
1813
1869
|
|
|
1814
1870
|
if ( useDefaults || options === undefined )
|
|
1815
|
-
options =
|
|
1871
|
+
options = { ...defaults, ...options }; // merge options with defaults
|
|
1816
1872
|
|
|
1817
1873
|
for ( const prop of Object.keys( options ) ) {
|
|
1818
1874
|
if ( callbacks.includes( prop ) && typeof options[ prop ] !== 'function' ) // check invalid callback
|