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.
@@ -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.0-beta.0
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.5.0-beta.0';
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
- // Bar spacing (for octave bands modes)
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
- get width() {
280
- return this._width;
280
+
281
+ get ledBars() {
282
+ return this._showLeds;
281
283
  }
282
- set width( w ) {
283
- this._width = w;
284
- this._setCanvas('user');
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
- // Mirror
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
- // Low-resolution mode
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 lumiBars( value ) {
332
- this._lumiBars = !! value;
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
- // Current frequency range
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._showLeds;
407
+ return this.ledBars;
425
408
  }
426
409
  set showLeds( value ) {
427
- this._showLeds = !! value;
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
- // Split gradient (in stereo mode)
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 fsWidth() {
504
- return this._fsWidth;
494
+ get fps() {
495
+ return this._fps;
505
496
  }
506
497
  get fsHeight() {
507
498
  return this._fsHeight;
508
499
  }
509
- get fps() {
510
- return this._fps;
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 isOctaveBands() {
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 peakEnergy() {
528
- // DEPRECATED - to be removed in v4.0.0
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 != ( startFreq | 0 ) ) {
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
- if ( options.bgColor !== undefined )
699
- this._gradients[ name ].bgColor = options.bgColor;
700
- else
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
- this._gradients[ name ].colorStops = options.colorStops;
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 = ( this._mode % 10 != 0 );
843
- this._isLedDisplay = ( this._showLeds && this._isOctaveBands && ! isRadial );
844
- this._isLumiBars = ( this._lumiBars && this._isOctaveBands && ! isRadial );
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
- * Calculate attributes for the vintage LEDs effect, based on visualization mode and canvas resolution
858
+ * Precalculate the actual X-coordinate on screen for each analyzer bar
860
859
  */
861
- _calcLeds() {
862
- if ( ! this._isOctaveBands || ! this._ready )
863
- return;
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
- // adjustment for high pixel-ratio values on low-resolution screens (Android TV)
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
- const params = [ [],
869
- [ 128, 3, .45 ], // mode 1
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
- // use custom LED parameters if set, or the default parameters for the current mode
880
- const customParams = this._ledParams,
881
- [ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ this._mode ];
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
- let ledCount, spaceV,
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
- isOctaveBands = this._isOctaveBands,
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
- // draw bars / lines
1338
+ // store line graph points to create mirror effect in radial mode
1339
+ let points = [];
1078
1340
 
1079
- let points = []; // store line graph (mode 10) points to create mirror effect in radial mode
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 lumi bars before barHeight value is normalized
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
- ctx.fillRect( posX, isLumiBars ? channelTop : analyzerBottom, adjWidth, isLumiBars ? channelBottom : -barHeight );
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
- if ( bar.peak[ channel ] > 0 && this.showPeaks && ! isLumiBars && posX >= initialX && posX < finalX ) {
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( bar.peak[ channel ] ), width, ledHeight );
1503
+ ctx.fillRect( posX, analyzerBottom - ledPosY( peak ), width, ledHeight );
1219
1504
  else if ( ! isRadial )
1220
- ctx.fillRect( posX, analyzerBottom - bar.peak[ channel ] * maxBarHeight, adjWidth, 2 );
1505
+ ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
1221
1506
  else if ( mode != 10 ) // radial - no peaks for mode 10
1222
- radialPoly( posX, bar.peak[ channel ] * maxBarHeight * ( ! channel || -1 ), adjWidth, -2 );
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 and radial
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
- * Generate the X-axis and radial scales in auxiliary canvases
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
- mode : 0,
1823
+ alphaBars : false,
1824
+ barSpace : 0.1,
1825
+ bgAlpha : 0.7,
1774
1826
  fftSize : 8192,
1775
- minFreq : 20,
1776
- maxFreq : 22000,
1777
- smoothing : 0.5,
1827
+ fillAlpha : 1,
1778
1828
  gradient : 'classic',
1779
- minDecibels : -85,
1780
- maxDecibels : -25,
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
- reflexRatio : 0,
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
- lineWidth : 0,
1794
- fillAlpha : 1,
1795
- barSpace : 0.1,
1796
- overlay : false,
1797
- bgAlpha : 0.7,
1798
- radial : false,
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
- // compile valid properties; `start` is not an actual property and is handled after setting everything else
1812
- const validProps = Object.keys( defaults ).concat( callbacks, ['height', 'width'] ).filter( e => e != 'start' );
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 = Object.assign( defaults, options ); // NOTE: defaults is modified!
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