audiomotion-analyzer 4.0.0 → 4.1.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 4.0.0
5
+ * @version 4.1.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 = '4.0.0';
10
+ const VERSION = '4.1.0';
11
11
 
12
12
  // internal constants
13
13
  const TAU = 2 * Math.PI,
@@ -19,6 +19,9 @@ const CANVAS_BACKGROUND_COLOR = '#000',
19
19
  CHANNEL_COMBINED = 'dual-combined',
20
20
  CHANNEL_SINGLE = 'single',
21
21
  CHANNEL_VERTICAL = 'dual-vertical',
22
+ COLOR_BAR_INDEX = 'bar-index',
23
+ COLOR_BAR_LEVEL = 'bar-level',
24
+ COLOR_GRADIENT = 'gradient',
22
25
  GRADIENT_DEFAULT_BGCOLOR = '#111',
23
26
  FILTER_NONE = '',
24
27
  FILTER_A = 'A',
@@ -49,9 +52,9 @@ const PRISM = [ '#a35', '#c66', '#e94', '#ed0', '#9d5', '#4d8', '#2cb', '#0bc',
49
52
  GRADIENTS = [
50
53
  [ 'classic', {
51
54
  colorStops: [
52
- 'hsl( 0, 100%, 50% )',
53
- { pos: .6, color: 'hsl( 60, 100%, 50% )' },
54
- 'hsl( 120, 100%, 50% )'
55
+ 'red',
56
+ { color: 'yellow', level: .85, pos: .6 },
57
+ { color: 'lime', level: .475 }
55
58
  ]
56
59
  }],
57
60
  [ 'prism', {
@@ -92,9 +95,13 @@ class AudioMotionError extends Error {
92
95
  }
93
96
  }
94
97
 
95
- // helper function
98
+ // helper function - output deprecation warning message on console
96
99
  const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated. Use ${alternative} instead.` );
97
100
 
101
+ // helper function - validate a given value with an array of strings (by default, all lowercase)
102
+ // returns the validated value, or the first element of `list` if `value` is not found in the array
103
+ const validateFromList = ( value, list, modifier = 'toLowerCase' ) => list[ Math.max( 0, list.indexOf( ( '' + value )[ modifier ]() ) ) ];
104
+
98
105
  // AudioMotionAnalyzer class
99
106
 
100
107
  export default class AudioMotionAnalyzer {
@@ -110,10 +117,12 @@ export default class AudioMotionAnalyzer {
110
117
 
111
118
  this._ready = false;
112
119
 
113
- // Initialize internal gradient objects
120
+ // Initialize internal objects
121
+ this._aux = {};
122
+ this._flg = {};
114
123
  this._gradients = {}; // registered gradients
115
124
  this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
116
- this._canvasGradients = []; // actual CanvasGradient objects for channels 0 and 1
125
+ this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
117
126
 
118
127
  // Register built-in gradients
119
128
  for ( const [ name, options ] of GRADIENTS )
@@ -262,9 +271,6 @@ export default class AudioMotionAnalyzer {
262
271
  }
263
272
  window.addEventListener( 'click', unlockContext );
264
273
 
265
- // initialize internal variables
266
- this._calcAux();
267
-
268
274
  // Set configuration options and use defaults for any missing properties
269
275
  this._setProps( options, true );
270
276
 
@@ -290,7 +296,7 @@ export default class AudioMotionAnalyzer {
290
296
  }
291
297
  set alphaBars( value ) {
292
298
  this._alphaBars = !! value;
293
- this._calcAux();
299
+ this._calcBars();
294
300
  }
295
301
 
296
302
  get ansiBands() {
@@ -306,15 +312,14 @@ export default class AudioMotionAnalyzer {
306
312
  }
307
313
  set barSpace( value ) {
308
314
  this._barSpace = +value || 0;
309
- this._calcAux();
315
+ this._calcBars();
310
316
  }
311
317
 
312
318
  get channelLayout() {
313
319
  return this._chLayout;
314
320
  }
315
321
  set channelLayout( value ) {
316
- const LAYOUTS = [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ];
317
- this._chLayout = LAYOUTS[ Math.max( 0, LAYOUTS.indexOf( ( '' + value ).toLowerCase() ) ) ];
322
+ this._chLayout = validateFromList( value, [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ] );
318
323
 
319
324
  // update node connections
320
325
  this._input.disconnect();
@@ -323,13 +328,17 @@ export default class AudioMotionAnalyzer {
323
328
  if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
324
329
  this._analyzer[0].connect( this._chLayout != CHANNEL_SINGLE ? this._merger : this._output );
325
330
 
326
- // update properties affected by channel layout
327
- this._calcAux();
328
- this._createScales();
329
- this._calcLeds();
331
+ this._calcBars();
330
332
  this._makeGrad();
331
333
  }
332
334
 
335
+ get colorMode() {
336
+ return this._colorMode;
337
+ }
338
+ set colorMode( value ) {
339
+ this._colorMode = validateFromList( value, [ COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL ] );
340
+ }
341
+
333
342
  get fftSize() {
334
343
  return this._analyzer[0].fftSize;
335
344
  }
@@ -345,9 +354,7 @@ export default class AudioMotionAnalyzer {
345
354
  return this._frequencyScale;
346
355
  }
347
356
  set frequencyScale( value ) {
348
- const FREQUENCY_SCALES = [ SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR ];
349
- this._frequencyScale = FREQUENCY_SCALES[ Math.max( 0, FREQUENCY_SCALES.indexOf( ( '' + value ).toLowerCase() ) ) ];
350
- this._calcAux();
357
+ this._frequencyScale = validateFromList( value, [ SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR ] );
351
358
  this._calcBars();
352
359
  }
353
360
 
@@ -385,7 +392,7 @@ export default class AudioMotionAnalyzer {
385
392
  }
386
393
  set ledBars( value ) {
387
394
  this._showLeds = !! value;
388
- this._calcAux();
395
+ this._calcBars();
389
396
  }
390
397
 
391
398
  get linearAmplitude() {
@@ -402,6 +409,13 @@ export default class AudioMotionAnalyzer {
402
409
  this._linearBoost = value >= 1 ? +value : 1;
403
410
  }
404
411
 
412
+ get lineWidth() {
413
+ return this._lineWidth;
414
+ }
415
+ set lineWidth( value ) {
416
+ this._lineWidth = +value || 0;
417
+ }
418
+
405
419
  get loRes() {
406
420
  return this._loRes;
407
421
  }
@@ -415,8 +429,7 @@ export default class AudioMotionAnalyzer {
415
429
  }
416
430
  set lumiBars( value ) {
417
431
  this._lumiBars = !! value;
418
- this._calcAux();
419
- this._calcLeds();
432
+ this._calcBars();
420
433
  this._makeGrad();
421
434
  }
422
435
 
@@ -465,7 +478,6 @@ export default class AudioMotionAnalyzer {
465
478
  }
466
479
  set mirror( value ) {
467
480
  this._mirror = Math.sign( value ) | 0; // ensure only -1, 0 or 1
468
- this._calcAux();
469
481
  this._calcBars();
470
482
  this._makeGrad();
471
483
  }
@@ -477,7 +489,6 @@ export default class AudioMotionAnalyzer {
477
489
  const mode = value | 0;
478
490
  if ( mode >= 0 && mode <= 10 && mode != 9 ) {
479
491
  this._mode = mode;
480
- this._calcAux();
481
492
  this._calcBars();
482
493
  this._makeGrad();
483
494
  }
@@ -498,7 +509,7 @@ export default class AudioMotionAnalyzer {
498
509
  }
499
510
  set outlineBars( value ) {
500
511
  this._outlineBars = !! value;
501
- this._calcAux();
512
+ this._calcBars();
502
513
  }
503
514
 
504
515
  get radial() {
@@ -506,7 +517,6 @@ export default class AudioMotionAnalyzer {
506
517
  }
507
518
  set radial( value ) {
508
519
  this._radial = !! value;
509
- this._calcAux();
510
520
  this._calcBars();
511
521
  this._makeGrad();
512
522
  }
@@ -520,12 +530,19 @@ export default class AudioMotionAnalyzer {
520
530
  throw new AudioMotionError( ERR_REFLEX_OUT_OF_RANGE );
521
531
  else {
522
532
  this._reflexRatio = value;
523
- this._calcAux();
533
+ this._calcBars();
524
534
  this._makeGrad();
525
- this._calcLeds();
526
535
  }
527
536
  }
528
537
 
538
+ get roundBars() {
539
+ return this._roundBars;
540
+ }
541
+ set roundBars( value ) {
542
+ this._roundBars = !! value;
543
+ this._calcBars();
544
+ }
545
+
529
546
  get smoothing() {
530
547
  return this._analyzer[0].smoothingTimeConstant;
531
548
  }
@@ -561,6 +578,13 @@ export default class AudioMotionAnalyzer {
561
578
  this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
562
579
  }
563
580
 
581
+ get trueLeds() {
582
+ return this._trueLeds;
583
+ }
584
+ set trueLeds( value ) {
585
+ this._trueLeds = !! value;
586
+ }
587
+
564
588
  get volume() {
565
589
  return this._output.gain.value;
566
590
  }
@@ -572,8 +596,7 @@ export default class AudioMotionAnalyzer {
572
596
  return this._weightingFilter;
573
597
  }
574
598
  set weightingFilter( value ) {
575
- const WEIGHTING_FILTERS = [ FILTER_NONE, FILTER_A, FILTER_B, FILTER_C, FILTER_D, FILTER_468 ];
576
- this._weightingFilter = WEIGHTING_FILTERS[ Math.max( 0, WEIGHTING_FILTERS.indexOf( ( '' + value ).toUpperCase() ) ) ];
599
+ this._weightingFilter = validateFromList( value, [ FILTER_NONE, FILTER_A, FILTER_B, FILTER_C, FILTER_D, FILTER_468 ], 'toUpperCase' );
577
600
  }
578
601
 
579
602
  get width() {
@@ -611,32 +634,35 @@ export default class AudioMotionAnalyzer {
611
634
  return this._fsWidth;
612
635
  }
613
636
  get isAlphaBars() {
614
- return this._isAlphaBars;
637
+ return this._flg.isAlpha;
615
638
  }
616
639
  get isBandsMode() {
617
- return this._isBandsMode;
640
+ return this._flg.isBands;
618
641
  }
619
642
  get isFullscreen() {
620
643
  return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
621
644
  }
622
645
  get isLedBars() {
623
- return this._isLedDisplay;
646
+ return this._flg.isLeds;
624
647
  }
625
648
  get isLumiBars() {
626
- return this._isLumiBars;
649
+ return this._flg.isLumi;
627
650
  }
628
651
  get isOctaveBands() {
629
- return this._isOctaveBands;
652
+ return this._flg.isOctaves;
630
653
  }
631
654
  get isOn() {
632
655
  return this._runId !== undefined;
633
656
  }
634
657
  get isOutlineBars() {
635
- return this._isOutline;
658
+ return this._flg.isOutline;
636
659
  }
637
660
  get pixelRatio() {
638
661
  return this._pixelRatio;
639
662
  }
663
+ get isRoundBars() {
664
+ return this._flg.isRound;
665
+ }
640
666
  static get version() {
641
667
  return VERSION;
642
668
  }
@@ -790,19 +816,41 @@ export default class AudioMotionAnalyzer {
790
816
  * @param {object} options
791
817
  */
792
818
  registerGradient( name, options ) {
793
- if ( typeof name !== 'string' || name.trim().length == 0 )
819
+ if ( typeof name != 'string' || name.trim().length == 0 )
794
820
  throw new AudioMotionError( ERR_GRADIENT_INVALID_NAME );
795
821
 
796
- if ( typeof options !== 'object' )
822
+ if ( typeof options != 'object' )
797
823
  throw new AudioMotionError( ERR_GRADIENT_NOT_AN_OBJECT );
798
824
 
799
- if ( ! Array.isArray( options.colorStops ) || ! options.colorStops.length )
825
+ const { colorStops } = options;
826
+
827
+ if ( ! Array.isArray( colorStops ) || ! colorStops.length )
800
828
  throw new AudioMotionError( ERR_GRADIENT_MISSING_COLOR );
801
829
 
830
+ const count = colorStops.length,
831
+ isInvalid = val => +val != val || val < 0 || val > 1;
832
+
833
+ // normalize all colorStops as objects with `pos`, `color` and `level` properties
834
+ colorStops.forEach( ( colorStop, index ) => {
835
+ const pos = index / Math.max( 1, count - 1 );
836
+ if ( typeof colorStop != 'object' ) // only color string was defined
837
+ colorStops[ index ] = { pos, color: colorStop };
838
+ else if ( isInvalid( colorStop.pos ) )
839
+ colorStop.pos = pos;
840
+
841
+ if ( isInvalid( colorStop.level ) )
842
+ colorStops[ index ].level = 1 - index / count;
843
+ });
844
+
845
+ // make sure colorStops is in descending `level` order and that the first one has `level == 1`
846
+ // this is crucial for proper operation of 'bar-level' colorMode!
847
+ colorStops.sort( ( a, b ) => a.level < b.level ? 1 : a.level > b.level ? -1 : 0 );
848
+ colorStops[0].level = 1;
849
+
802
850
  this._gradients[ name ] = {
803
851
  bgColor: options.bgColor || GRADIENT_DEFAULT_BGCOLOR,
804
852
  dir: options.dir,
805
- colorStops: options.colorStops
853
+ colorStops: colorStops
806
854
  };
807
855
 
808
856
  // if the registered gradient is one of the currently selected gradients, regenerate them
@@ -854,7 +902,7 @@ export default class AudioMotionAnalyzer {
854
902
  }
855
903
 
856
904
  this._ledParams = maxLeds > 0 && spaceV > 0 && spaceH >= 0 ? [ maxLeds, spaceV, spaceH ] : undefined;
857
- this._calcLeds();
905
+ this._calcBars();
858
906
  }
859
907
 
860
908
  /**
@@ -939,37 +987,7 @@ export default class AudioMotionAnalyzer {
939
987
  }
940
988
 
941
989
  /**
942
- * Calculate auxiliary values and flags
943
- */
944
- _calcAux() {
945
- const canvas = this.canvas,
946
- isRadial = this._radial,
947
- isDual = this._chLayout == CHANNEL_VERTICAL && ! isRadial,
948
- centerX = canvas.width >> 1;
949
-
950
- this._radius = Math.min( canvas.width, canvas.height ) * ( this._chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
951
- this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
952
- this._isBandsMode = this._mode % 10 != 0;
953
- this._isOctaveBands = this._isBandsMode && this._frequencyScale == SCALE_LOG;
954
- this._isLedDisplay = this._showLeds && this._isBandsMode && ! isRadial;
955
- this._isLumiBars = this._lumiBars && this._isBandsMode && ! isRadial;
956
- this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
957
- this._isOutline = this._outlineBars && this._isBandsMode && ! this._isLumiBars && ! this._isLedDisplay;
958
- this._maximizeLeds = this._chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! this._isLumiBars;
959
-
960
- this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
961
- this._analyzerHeight = this._channelHeight * ( this._isLumiBars || isRadial ? 1 : 1 - this._reflexRatio ) | 0;
962
-
963
- // channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
964
- // TODO: improve this, make it configurable?
965
- this._channelGap = isDual ? canvas.height - this._channelHeight * 2 : 0;
966
-
967
- this._analyzerWidth = canvas.width - centerX * ( this._mirror != 0 );
968
- this._initialX = centerX * ( this._mirror == -1 && ! isRadial );
969
- }
970
-
971
- /**
972
- * Calculate the X-coordinate on canvas for each analyzer bar
990
+ * Compute all internal data required for the analyzer, based on its current settings
973
991
  */
974
992
  _calcBars() {
975
993
  const bars = this._bars = []; // initialize object property
@@ -977,9 +995,63 @@ export default class AudioMotionAnalyzer {
977
995
  if ( ! this._ready )
978
996
  return;
979
997
 
998
+ const barSpace = this._barSpace,
999
+ canvas = this.canvas,
1000
+ centerX = canvas.width >> 1,
1001
+ chLayout = this._chLayout,
1002
+ isAnsiBands = this._ansiBands,
1003
+ isRadial = this._radial,
1004
+ isDual = chLayout == CHANNEL_VERTICAL && ! isRadial,
1005
+ maxFreq = this._maxFreq,
1006
+ minFreq = this._minFreq,
1007
+ mode = this._mode,
1008
+
1009
+ // COMPUTE FLAGS
1010
+
1011
+ isBands = mode % 10 != 0,
1012
+ isOctaves = isBands && this._frequencyScale == SCALE_LOG,
1013
+ isLeds = this._showLeds && isBands && ! isRadial,
1014
+ isLumi = this._lumiBars && isBands && ! isRadial,
1015
+ isAlpha = this._alphaBars && ! isLumi && mode != 10,
1016
+ isOutline = this._outlineBars && isBands && ! isLumi && ! isLeds,
1017
+ isRound = this._roundBars && isBands && ! isLumi && ! isLeds,
1018
+ noLedGap = chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! isLumi,
1019
+
1020
+ // COMPUTE AUXILIARY VALUES
1021
+
1022
+ // channelHeight is the total canvas height dedicated to each channel, including the reflex area, if any)
1023
+ channelHeight = canvas.height - ( isDual && ! isLeds ? .5 : 0 ) >> isDual,
1024
+ // analyzerHeight is the effective height used to render the analyzer, excluding the reflex area
1025
+ analyzerHeight = channelHeight * ( isLumi || isRadial ? 1 : 1 - this._reflexRatio ) | 0,
1026
+
1027
+ analyzerWidth = canvas.width - centerX * ( this._mirror != 0 ),
1028
+
1029
+ // channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
1030
+ // TODO: improve this, make it configurable?
1031
+ channelGap = isDual ? canvas.height - channelHeight * 2 : 0,
1032
+
1033
+ initialX = centerX * ( this._mirror == -1 && ! isRadial ),
1034
+ radius = Math.min( canvas.width, canvas.height ) * ( chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
1035
+
1036
+ /**
1037
+ * CREATE ANALYZER BANDS
1038
+ *
1039
+ * USES:
1040
+ * analyzerWidth
1041
+ * initialX
1042
+ * isBands
1043
+ * isOctaves
1044
+ *
1045
+ * GENERATES:
1046
+ * bars (populates this._bars)
1047
+ * bardWidth
1048
+ * scaleMin
1049
+ * unitWidth
1050
+ */
1051
+
980
1052
  // helper function
981
1053
  // bar object: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
982
- const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
1054
+ const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
983
1055
 
984
1056
  /*
985
1057
  A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
@@ -1012,15 +1084,9 @@ export default class AudioMotionAnalyzer {
1012
1084
  return [ bin, ratio ];
1013
1085
  }
1014
1086
 
1015
- const analyzerWidth = this._analyzerWidth,
1016
- initialX = this._initialX,
1017
- isAnsiBands = this._ansiBands,
1018
- maxFreq = this._maxFreq,
1019
- minFreq = this._minFreq;
1020
-
1021
- let scaleMin, unitWidth;
1087
+ let barWidth, scaleMin, unitWidth;
1022
1088
 
1023
- if ( this._isOctaveBands ) {
1089
+ if ( isOctaves ) {
1024
1090
  // helper function to round a value to a given number of significant digits
1025
1091
  // `atLeast` set to true prevents reducing the number of integer significant digits
1026
1092
  const roundSD = ( value, digits, atLeast ) => +value.toPrecision( atLeast ? Math.max( digits, 1 + Math.log10( value ) | 0 ) : digits );
@@ -1075,9 +1141,9 @@ export default class AudioMotionAnalyzer {
1075
1141
  currFreq *= bandWidth;
1076
1142
  } while ( currFreq <= maxFreq );
1077
1143
 
1078
- this._barWidth = analyzerWidth / bars.length;
1144
+ barWidth = analyzerWidth / bars.length;
1079
1145
 
1080
- bars.forEach( ( bar, index ) => bar.posX = initialX + index * this._barWidth );
1146
+ bars.forEach( ( bar, index ) => bar.posX = initialX + index * barWidth );
1081
1147
 
1082
1148
  const firstBar = bars[0],
1083
1149
  lastBar = bars[ bars.length - 1 ];
@@ -1097,7 +1163,7 @@ export default class AudioMotionAnalyzer {
1097
1163
  [ lastBar.binHi, lastBar.ratioHi ] = calcRatio( maxFreq );
1098
1164
  }
1099
1165
  }
1100
- else if ( this._isBandsMode ) { // a bands mode is selected, but frequency scale is not logarithmic
1166
+ else if ( isBands ) { // a bands mode is selected, but frequency scale is not logarithmic
1101
1167
 
1102
1168
  const bands = [0,24,12,8,6,4,3,2,1][ this._mode ] * 10;
1103
1169
 
@@ -1112,15 +1178,15 @@ export default class AudioMotionAnalyzer {
1112
1178
  }
1113
1179
  }
1114
1180
 
1115
- this._barWidth = analyzerWidth / bands;
1181
+ barWidth = analyzerWidth / bands;
1116
1182
 
1117
1183
  scaleMin = this._freqScaling( minFreq );
1118
1184
  unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
1119
1185
 
1120
- for ( let i = 0, posX = 0; i < bands; i++, posX += this._barWidth ) {
1186
+ for ( let i = 0, posX = 0; i < bands; i++, posX += barWidth ) {
1121
1187
  const freqLo = invFreqScaling( scaleMin + posX / unitWidth ),
1122
- freq = invFreqScaling( scaleMin + ( posX + this._barWidth / 2 ) / unitWidth ),
1123
- freqHi = invFreqScaling( scaleMin + ( posX + this._barWidth ) / unitWidth ),
1188
+ freq = invFreqScaling( scaleMin + ( posX + barWidth / 2 ) / unitWidth ),
1189
+ freqHi = invFreqScaling( scaleMin + ( posX + barWidth ) / unitWidth ),
1124
1190
  [ binLo, ratioLo ] = calcRatio( freqLo ),
1125
1191
  [ binHi, ratioHi ] = calcRatio( freqHi );
1126
1192
 
@@ -1129,7 +1195,7 @@ export default class AudioMotionAnalyzer {
1129
1195
 
1130
1196
  }
1131
1197
  else { // Discrete frequencies modes
1132
- this._barWidth = 1;
1198
+ barWidth = 1;
1133
1199
 
1134
1200
  scaleMin = this._freqScaling( minFreq );
1135
1201
  unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
@@ -1157,78 +1223,130 @@ export default class AudioMotionAnalyzer {
1157
1223
  }
1158
1224
  }
1159
1225
 
1160
- // save these for scale generation
1161
- this._scaleMin = scaleMin;
1162
- this._unitWidth = unitWidth;
1163
-
1164
- // update internal variables
1165
- this._calcAux();
1226
+ /**
1227
+ * COMPUTE ATTRIBUTES FOR THE LED BARS
1228
+ *
1229
+ * USES:
1230
+ * analyzerHeight
1231
+ * barWidth
1232
+ * noLedGap
1233
+ *
1234
+ * GENERATES:
1235
+ * spaceH
1236
+ * spaceV
1237
+ * this._leds
1238
+ */
1239
+
1240
+ let spaceH = 0,
1241
+ spaceV = 0;
1242
+
1243
+ if ( isLeds ) {
1244
+ // adjustment for high pixel-ratio values on low-resolution screens (Android TV)
1245
+ const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
1246
+
1247
+ const params = [ [],
1248
+ [ 128, 3, .45 ], // mode 1
1249
+ [ 128, 4, .225 ], // mode 2
1250
+ [ 96, 6, .225 ], // mode 3
1251
+ [ 80, 6, .225 ], // mode 4
1252
+ [ 80, 6, .125 ], // mode 5
1253
+ [ 64, 6, .125 ], // mode 6
1254
+ [ 48, 8, .125 ], // mode 7
1255
+ [ 24, 16, .125 ], // mode 8
1256
+ ];
1257
+
1258
+ // use custom LED parameters if set, or the default parameters for the current mode
1259
+ const customParams = this._ledParams,
1260
+ [ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ mode ];
1261
+
1262
+ let ledCount, maxHeight = analyzerHeight;
1263
+
1264
+ if ( customParams ) {
1265
+ const minHeight = 2 * dPR;
1266
+ let blockHeight;
1267
+ ledCount = maxLeds + 1;
1268
+ do {
1269
+ ledCount--;
1270
+ blockHeight = maxHeight / ledCount / ( 1 + spaceVRatio );
1271
+ spaceV = blockHeight * spaceVRatio;
1272
+ } while ( ( blockHeight < minHeight || spaceV < minHeight ) && ledCount > 1 );
1273
+ }
1274
+ else {
1275
+ // calculate vertical spacing - aim for the reference ratio, but make sure it's at least 2px
1276
+ const refRatio = 540 / spaceVRatio;
1277
+ spaceV = Math.min( spaceVRatio * dPR, Math.max( 2, maxHeight / refRatio + .1 | 0 ) );
1278
+ }
1166
1279
 
1167
- // generate the X-axis and radial scales
1168
- this._createScales();
1280
+ // remove the extra spacing below the last line of LEDs
1281
+ if ( noLedGap )
1282
+ maxHeight += spaceV;
1169
1283
 
1170
- // update LED properties
1171
- this._calcLeds();
1172
- }
1284
+ // recalculate the number of leds, considering the effective spaceV
1285
+ if ( ! customParams )
1286
+ ledCount = Math.min( maxLeds, maxHeight / ( spaceV * 2 ) | 0 );
1173
1287
 
1174
- /**
1175
- * Calculate attributes for the vintage LEDs effect, based on visualization mode and canvas resolution
1176
- */
1177
- _calcLeds() {
1178
- if ( ! this._isBandsMode || ! this._ready )
1179
- return;
1288
+ spaceH = spaceHRatio >= 1 ? spaceHRatio : barWidth * spaceHRatio;
1180
1289
 
1181
- // adjustment for high pixel-ratio values on low-resolution screens (Android TV)
1182
- const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
1183
-
1184
- const params = [ [],
1185
- [ 128, 3, .45 ], // mode 1
1186
- [ 128, 4, .225 ], // mode 2
1187
- [ 96, 6, .225 ], // mode 3
1188
- [ 80, 6, .225 ], // mode 4
1189
- [ 80, 6, .125 ], // mode 5
1190
- [ 64, 6, .125 ], // mode 6
1191
- [ 48, 8, .125 ], // mode 7
1192
- [ 24, 16, .125 ], // mode 8
1193
- ];
1194
-
1195
- // use custom LED parameters if set, or the default parameters for the current mode
1196
- const customParams = this._ledParams,
1197
- [ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ this._mode ];
1198
-
1199
- let ledCount, spaceV,
1200
- analyzerHeight = this._analyzerHeight;
1201
-
1202
- if ( customParams ) {
1203
- const minHeight = 2 * dPR;
1204
- let blockHeight;
1205
- ledCount = maxLeds + 1;
1206
- do {
1207
- ledCount--;
1208
- blockHeight = analyzerHeight / ledCount / ( 1 + spaceVRatio );
1209
- spaceV = blockHeight * spaceVRatio;
1210
- } while ( ( blockHeight < minHeight || spaceV < minHeight ) && ledCount > 1 );
1290
+ this._leds = [
1291
+ ledCount,
1292
+ spaceH,
1293
+ spaceV,
1294
+ maxHeight / ledCount - spaceV // ledHeight
1295
+ ];
1211
1296
  }
1212
- else {
1213
- // calculate vertical spacing - aim for the reference ratio, but make sure it's at least 2px
1214
- const refRatio = 540 / spaceVRatio;
1215
- spaceV = Math.min( spaceVRatio * dPR, Math.max( 2, analyzerHeight / refRatio + .1 | 0 ) );
1297
+
1298
+ // COMPUTE ADDITIONAL BAR POSITIONING, ACCORDING TO THE CURRENT SETTINGS
1299
+ // uses: barSpace, barWidth, spaceH
1300
+
1301
+ const barSpacePx = Math.min( barWidth - 1, barSpace * ( barSpace > 0 && barSpace < 1 ? barWidth : 1 ) );
1302
+
1303
+ if ( isBands )
1304
+ barWidth -= Math.max( isLeds ? spaceH : 0, barSpacePx );
1305
+
1306
+ bars.forEach( ( bar, index ) => {
1307
+ let posX = bar.posX,
1308
+ width = barWidth;
1309
+
1310
+ // in bands modes we need to update bar.posX to account for bar/led spacing
1311
+
1312
+ if ( isBands ) {
1313
+ if ( barSpace == 0 && ! isLeds ) {
1314
+ // when barSpace == 0 use integer values for perfect gapless positioning
1315
+ posX |= 0;
1316
+ width |= 0;
1317
+ if ( index > 0 && posX > bars[ index - 1 ].posX + bars[ index - 1 ].width ) {
1318
+ posX--;
1319
+ width++;
1320
+ }
1321
+ }
1322
+ else
1323
+ posX += Math.max( ( isLeds ? spaceH : 0 ), barSpacePx ) / 2;
1324
+
1325
+ bar.posX = posX; // update
1326
+ }
1327
+
1328
+ bar.barCenter = posX + ( barWidth == 1 ? 0 : width / 2 );
1329
+ bar.width = width;
1330
+ });
1331
+
1332
+ // COMPUTE CHANNEL COORDINATES (uses spaceV)
1333
+
1334
+ const channelCoords = [];
1335
+ for ( const channel of [0,1] ) {
1336
+ const channelTop = chLayout == CHANNEL_VERTICAL ? ( channelHeight + channelGap ) * channel : 0,
1337
+ channelBottom = channelTop + channelHeight,
1338
+ analyzerBottom = channelTop + analyzerHeight - ( ! isLeds || noLedGap ? 0 : spaceV );
1339
+
1340
+ channelCoords.push( { channelTop, channelBottom, analyzerBottom } );
1216
1341
  }
1217
1342
 
1218
- // remove the extra spacing below the last line of LEDs
1219
- if ( this._maximizeLeds )
1220
- analyzerHeight += spaceV;
1343
+ // SAVE INTERNAL PROPERTIES
1221
1344
 
1222
- // recalculate the number of leds, considering the effective spaceV
1223
- if ( ! customParams )
1224
- ledCount = Math.min( maxLeds, analyzerHeight / ( spaceV * 2 ) | 0 );
1345
+ this._aux = { analyzerHeight, analyzerWidth, channelCoords, channelHeight, channelGap, initialX, radius, scaleMin, unitWidth };
1346
+ this._flg = { isAlpha, isBands, isLeds, isLumi, isOctaves, isOutline, isRound, noLedGap };
1225
1347
 
1226
- this._leds = [
1227
- ledCount,
1228
- spaceHRatio >= 1 ? spaceHRatio : this._barWidth * spaceHRatio, // spaceH
1229
- spaceV,
1230
- analyzerHeight / ledCount - spaceV // ledHeight
1231
- ];
1348
+ // generate the X-axis and radial scales
1349
+ this._createScales();
1232
1350
  }
1233
1351
 
1234
1352
  /**
@@ -1238,7 +1356,7 @@ export default class AudioMotionAnalyzer {
1238
1356
  if ( ! this._ready )
1239
1357
  return;
1240
1358
 
1241
- const analyzerWidth = this._analyzerWidth,
1359
+ const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
1242
1360
  canvas = this._canvasCtx.canvas,
1243
1361
  scaleX = this._scaleX,
1244
1362
  scaleR = this._scaleR,
@@ -1246,20 +1364,22 @@ export default class AudioMotionAnalyzer {
1246
1364
  canvasR = scaleR.canvas,
1247
1365
  freqLabels = [],
1248
1366
  frequencyScale= this._frequencyScale,
1249
- initialX = this._initialX,
1250
- isDual = this._chLayout == CHANNEL_VERTICAL,
1251
- isMirror = this._mirror,
1252
1367
  isNoteLabels = this._noteLabels,
1368
+ isRadial = this._radial,
1369
+ isVertical = this._chLayout == CHANNEL_VERTICAL,
1370
+ mirror = this._mirror,
1253
1371
  scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
1254
- scaleHeight = Math.min( canvas.width, canvas.height ) * .03 | 0, // circular scale height (radial mode)
1372
+ scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
1255
1373
  fontSizeX = canvasX.height >> 1,
1256
1374
  fontSizeR = scaleHeight >> 1,
1375
+ labelWidthX = fontSizeX * ( isNoteLabels ? .7 : 1.5 ),
1376
+ labelWidthR = fontSizeR * ( isNoteLabels ? 1 : 2 ),
1257
1377
  root12 = 2 ** ( 1 / 12 );
1258
1378
 
1259
1379
  if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
1260
1380
  freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
1261
1381
  if ( frequencyScale == SCALE_LINEAR )
1262
- freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3 );
1382
+ freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
1263
1383
  else
1264
1384
  freqLabels.push( 8e3, 16e3 );
1265
1385
  }
@@ -1270,7 +1390,7 @@ export default class AudioMotionAnalyzer {
1270
1390
  if ( freq >= this._minFreq && freq <= this._maxFreq ) {
1271
1391
  const pitch = scale[ note ],
1272
1392
  isC = pitch == 'C';
1273
- if ( ( pitch && isNoteLabels && ! isMirror ) || isC )
1393
+ if ( ( pitch && isNoteLabels && ! mirror ) || isC )
1274
1394
  freqLabels.push( isNoteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
1275
1395
  }
1276
1396
  freq *= root12;
@@ -1278,24 +1398,21 @@ export default class AudioMotionAnalyzer {
1278
1398
  }
1279
1399
  }
1280
1400
 
1281
- // in radial stereo mode, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
1282
- canvasR.width = canvasR.height = ( this._radius << 1 ) + ( isDual * scaleHeight );
1401
+ // in radial dual-vertical layout, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
1402
+ canvasR.width = canvasR.height = ( radius << 1 ) + ( isVertical * scaleHeight );
1283
1403
 
1284
- const radius = canvasR.width >> 1, // this is also used as the center X and Y coordinates of the circular scale canvas
1285
- radialY = radius - scaleHeight * .7; // vertical position of text labels in the circular scale
1404
+ const centerR = canvasR.width >> 1,
1405
+ radialY = centerR - scaleHeight * .7; // vertical position of text labels in the circular scale
1286
1406
 
1287
1407
  // helper function
1288
1408
  const radialLabel = ( x, label ) => {
1289
- if ( isNoteLabels && ! isDual && ! ['C','E','G'].includes( label[0] ) )
1290
- return;
1291
-
1292
1409
  const angle = TAU * ( x / canvas.width ),
1293
1410
  adjAng = angle - HALF_PI, // rotate angles so 0 is at the top
1294
1411
  posX = radialY * Math.cos( adjAng ),
1295
1412
  posY = radialY * Math.sin( adjAng );
1296
1413
 
1297
1414
  scaleR.save();
1298
- scaleR.translate( radius + posX, radius + posY );
1415
+ scaleR.translate( centerR + posX, centerR + posY );
1299
1416
  scaleR.rotate( angle );
1300
1417
  scaleR.fillText( label, 0, 0 );
1301
1418
  scaleR.restore();
@@ -1307,7 +1424,7 @@ export default class AudioMotionAnalyzer {
1307
1424
  scaleX.fillStyle = scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
1308
1425
  scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
1309
1426
 
1310
- scaleR.arc( radius, radius, radius - scaleHeight / 2, 0, TAU );
1427
+ scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
1311
1428
  scaleR.lineWidth = scaleHeight;
1312
1429
  scaleR.stroke();
1313
1430
 
@@ -1316,32 +1433,49 @@ export default class AudioMotionAnalyzer {
1316
1433
  scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
1317
1434
  scaleX.textAlign = scaleR.textAlign = 'center';
1318
1435
 
1319
- let prevX = 0, prevR = 0;
1436
+ let prevX = -labelWidthX / 4,
1437
+ prevR = -labelWidthR;
1320
1438
 
1321
1439
  for ( const item of freqLabels ) {
1322
1440
  const [ freq, label ] = Array.isArray( item ) ? item : [ item, item < 1e3 ? item | 0 : `${ ( item / 100 | 0 ) / 10 }k` ],
1323
- x = this._unitWidth * ( this._freqScaling( freq ) - this._scaleMin ),
1441
+ x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
1324
1442
  y = canvasX.height * .75,
1325
1443
  isC = label[0] == 'C',
1326
- maxW = fontSizeX * ( isNoteLabels && ! isMirror ? ( isC ? 1.2 : .6 ) : 3 );
1327
-
1328
- if ( x >= 0 && x <= analyzerWidth ) {
1329
-
1330
- scaleX.fillStyle = scaleR.fillStyle = isC && ! isMirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
1444
+ maxW = fontSizeX * ( isNoteLabels && ! mirror ? ( isC ? 1.2 : .6 ) : 3 );
1445
+
1446
+ // set label color - no highlight when mirror effect is active (only Cs displayed)
1447
+ scaleX.fillStyle = scaleR.fillStyle = isC && ! mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
1448
+
1449
+ // prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
1450
+ if ( isNoteLabels ) {
1451
+ let allowedLabels = ['C'];
1452
+ if ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 250 ) ||
1453
+ ( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 125 || freq > 1e3 ) ) )
1454
+ allowedLabels.push('G');
1455
+ if ( frequencyScale == SCALE_LOG || freq > 4e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ||
1456
+ ( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 250 || freq > 2e3 ) ) )
1457
+ allowedLabels.push('E');
1458
+ if ( frequencyScale == SCALE_LINEAR && freq > 4e3 ||
1459
+ ( ( ! isRadial || isVertical ) && ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ) ) )
1460
+ allowedLabels.push('D','F','A','B');
1461
+ if ( ! allowedLabels.includes( label[0] ) )
1462
+ continue; // skip this label
1463
+ }
1331
1464
 
1332
- if ( x > prevX + fontSizeX / 2 ) {
1333
- scaleX.fillText( label, initialX + x, y, maxW );
1334
- if ( isMirror )
1335
- scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
1336
- prevX = x + Math.min( maxW, scaleX.measureText( label ).width ) / 2;
1337
- }
1465
+ // linear scale
1466
+ if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
1467
+ scaleX.fillText( label, initialX + x, y, maxW );
1468
+ if ( mirror && ( x > labelWidthX || mirror == 1 ) )
1469
+ scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
1470
+ prevX = x + Math.min( maxW, scaleX.measureText( label ).width ) / 2;
1471
+ }
1338
1472
 
1339
- if ( x < analyzerWidth && ( x > prevR + fontSizeR || isC ) ) { // avoid wrapping-around the last label and overlapping the first one
1340
- radialLabel( x, label );
1341
- if ( isMirror && x > fontSizeR ) // avoid overlapping of first labels on mirror mode
1342
- radialLabel( -x, label );
1343
- prevR = x;
1344
- }
1473
+ // radial scale
1474
+ if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
1475
+ radialLabel( x, label );
1476
+ if ( mirror && ( x > labelWidthR || mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
1477
+ radialLabel( -x, label );
1478
+ prevR = x;
1345
1479
  }
1346
1480
  }
1347
1481
  }
@@ -1351,48 +1485,132 @@ export default class AudioMotionAnalyzer {
1351
1485
  * this is called 60 times per second by requestAnimationFrame()
1352
1486
  */
1353
1487
  _draw( timestamp ) {
1354
- const barSpace = this._barSpace,
1355
- barSpacePx = this._barSpacePx,
1488
+ const { isAlpha, isBands, isLeds, isLumi,
1489
+ isOctaves, isOutline, isRound, noLedGap } = this._flg,
1356
1490
  ctx = this._canvasCtx,
1357
1491
  canvas = ctx.canvas,
1358
1492
  canvasX = this._scaleX.canvas,
1359
1493
  canvasR = this._scaleR.canvas,
1360
1494
  canvasGradients= this._canvasGradients,
1495
+ centerX = canvas.width >> 1,
1496
+ centerY = canvas.height >> 1,
1497
+ colorMode = this._colorMode,
1361
1498
  energy = this._energy,
1362
1499
  fillAlpha = this.fillAlpha,
1363
1500
  mode = this._mode,
1364
- isAlphaBars = this._isAlphaBars,
1365
- isLedDisplay = this._isLedDisplay,
1366
1501
  isLinear = this._linearAmplitude,
1367
- isLumiBars = this._isLumiBars,
1368
- isBandsMode = this._isBandsMode,
1369
- isOutline = this._isOutline,
1370
1502
  isOverlay = this.overlay,
1371
1503
  isRadial = this._radial,
1504
+ isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
1372
1505
  channelLayout = this._chLayout,
1373
- lineWidth = +this.lineWidth, // make sure the damn thing is a number!
1506
+ lineWidth = this._lineWidth,
1374
1507
  mirrorMode = this._mirror,
1375
- channelHeight = this._channelHeight,
1376
- channelGap = this._channelGap,
1377
- analyzerHeight = this._analyzerHeight,
1378
- analyzerWidth = isRadial ? canvas.width : this._analyzerWidth,
1379
- initialX = this._initialX,
1508
+ { analyzerHeight, channelCoords,
1509
+ channelHeight, channelGap, initialX, radius } = this._aux,
1510
+ analyzerWidth = isRadial ? canvas.width : this._aux.analyzerWidth,
1380
1511
  finalX = initialX + analyzerWidth,
1381
- centerX = canvas.width >> 1,
1382
- centerY = canvas.height >> 1,
1383
- radius = this._radius,
1384
1512
  showBgColor = this.showBgColor,
1385
1513
  maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1386
1514
  maxdB = this.maxDecibels,
1387
1515
  mindB = this.minDecibels,
1388
1516
  dbRange = maxdB - mindB,
1389
1517
  useCanvas = this.useCanvas,
1390
- weightingFilter= this._weightingFilter;
1518
+ weightingFilter= this._weightingFilter,
1519
+ [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1391
1520
 
1392
1521
  if ( energy.val > 0 )
1393
1522
  this._spinAngle += this._spinSpeed * RPM;
1394
1523
 
1395
- // helper function - apply the selected weighting filter and return dB gain for a given frequency
1524
+ /* HELPER FUNCTIONS */
1525
+
1526
+ // create Reflex effect
1527
+ const doReflex = channel => {
1528
+ if ( this._reflexRatio > 0 && ! isLumi ) {
1529
+ let posY, height;
1530
+ if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
1531
+ posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
1532
+ height = channelHeight - analyzerHeight;
1533
+ }
1534
+ else {
1535
+ posY = canvas.height - analyzerHeight * 2;
1536
+ height = analyzerHeight;
1537
+ }
1538
+
1539
+ ctx.save();
1540
+
1541
+ // set alpha and brightness for the reflection
1542
+ ctx.globalAlpha = this.reflexAlpha;
1543
+ if ( this.reflexBright != 1 )
1544
+ ctx.filter = `brightness(${this.reflexBright})`;
1545
+
1546
+ // create the reflection
1547
+ ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
1548
+ ctx.drawImage( canvas, 0, channelCoords[ channel ].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
1549
+
1550
+ ctx.restore();
1551
+ }
1552
+ }
1553
+
1554
+ // draw scale on X-axis
1555
+ const drawScaleX = () => {
1556
+ if ( this.showScaleX ) {
1557
+ if ( isRadial ) {
1558
+ ctx.save();
1559
+ ctx.translate( centerX, centerY );
1560
+ if ( this._spinSpeed )
1561
+ ctx.rotate( this._spinAngle + HALF_PI );
1562
+ ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1563
+ ctx.restore();
1564
+ }
1565
+ else
1566
+ ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1567
+ }
1568
+ }
1569
+
1570
+ // draw scale on Y-axis
1571
+ const drawScaleY = channelTop => {
1572
+ const scaleWidth = canvasX.height,
1573
+ fontSize = scaleWidth >> 1,
1574
+ max = isLinear ? 100 : maxdB,
1575
+ min = isLinear ? 0 : mindB,
1576
+ incr = isLinear ? 20 : 5,
1577
+ interval = analyzerHeight / ( max - min );
1578
+
1579
+ ctx.save();
1580
+ ctx.fillStyle = SCALEY_LABEL_COLOR;
1581
+ ctx.font = `${fontSize}px ${FONT_FAMILY}`;
1582
+ ctx.textAlign = 'right';
1583
+ ctx.lineWidth = 1;
1584
+
1585
+ for ( let val = max; val > min; val -= incr ) {
1586
+ const posY = channelTop + ( max - val ) * interval,
1587
+ even = ( val % 2 == 0 ) | 0;
1588
+
1589
+ if ( even ) {
1590
+ const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
1591
+ if ( mirrorMode != -1 )
1592
+ ctx.fillText( val, scaleWidth * .85, labelY );
1593
+ if ( mirrorMode != 1 )
1594
+ ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
1595
+ ctx.strokeStyle = SCALEY_LABEL_COLOR;
1596
+ ctx.setLineDash([2,4]);
1597
+ ctx.lineDashOffset = 0;
1598
+ }
1599
+ else {
1600
+ ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1601
+ ctx.setLineDash([2,8]);
1602
+ ctx.lineDashOffset = 1;
1603
+ }
1604
+
1605
+ ctx.beginPath();
1606
+ ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
1607
+ ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
1608
+ ctx.stroke();
1609
+ }
1610
+ ctx.restore();
1611
+ }
1612
+
1613
+ // returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
1396
1614
  const weightingdB = freq => {
1397
1615
  const f2 = freq ** 2,
1398
1616
  SQ20_6 = 424.36,
@@ -1430,6 +1648,15 @@ export default class AudioMotionAnalyzer {
1430
1648
  return 0; // unknown filter
1431
1649
  }
1432
1650
 
1651
+ // draws (stroke) a bar from x,y1 to x,y2
1652
+ const strokeBar = ( x, y1, y2 ) => {
1653
+ ctx.beginPath();
1654
+ ctx.moveTo( x, y1 );
1655
+ ctx.lineTo( x, y2 );
1656
+ ctx.stroke();
1657
+ }
1658
+
1659
+ // conditionally strokes current path on canvas
1433
1660
  const strokeIf = flag => {
1434
1661
  if ( flag && lineWidth ) {
1435
1662
  const alpha = ctx.globalAlpha;
@@ -1439,52 +1666,107 @@ export default class AudioMotionAnalyzer {
1439
1666
  }
1440
1667
  }
1441
1668
 
1442
- // helper function - convert planar X,Y coordinates to radial coordinates
1669
+ // converts a given X-coordinate to its corresponding angle in radial mode
1670
+ const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
1671
+
1672
+ // converts planar X,Y coordinates to radial coordinates
1443
1673
  const radialXY = ( x, y, dir ) => {
1444
1674
  const height = radius + y,
1445
- angle = dir * TAU * ( x / canvas.width ) + this._spinAngle;
1446
-
1675
+ angle = getAngle( x, dir );
1447
1676
  return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
1448
1677
  }
1449
1678
 
1450
- // helper function - draw a polygon of width `w` and height `h` at (x,y) in radial mode
1679
+ // draws a polygon of width `w` and height `h` at (x,y) in radial mode
1451
1680
  const radialPoly = ( x, y, w, h, stroke ) => {
1452
1681
  ctx.beginPath();
1453
1682
  for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
1683
+ const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
1454
1684
  ctx.moveTo( ...radialXY( x, y, dir ) );
1455
1685
  ctx.lineTo( ...radialXY( x, y + h, dir ) );
1456
- ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1686
+ if ( isRound )
1687
+ ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle );
1688
+ else
1689
+ ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1457
1690
  ctx.lineTo( ...radialXY( x + w, y, dir ) );
1691
+ if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
1692
+ ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, true );
1458
1693
  }
1459
-
1460
1694
  strokeIf( stroke );
1461
1695
  ctx.fill();
1462
1696
  }
1463
1697
 
1464
- // LED attributes and helper function for bar height calculation
1465
- const [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1466
- const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
1698
+ // converts a value in [0;1] range to a height in pixels that fits into the current LED elements
1699
+ const ledPosY = value => Math.max( 0, ( value * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
1467
1700
 
1468
- // compute the effective bar width, considering the selected bar spacing
1469
- // if led effect is active, ensure at least the spacing from led definitions
1470
- let width = this._barWidth - ( ! isBandsMode ? 0 : Math.max( isLedDisplay ? ledSpaceH : 0, barSpacePx ) );
1701
+ // update energy information
1702
+ const updateEnergy = newVal => {
1703
+ energy.val = newVal;
1704
+ if ( newVal >= energy.peak ) {
1705
+ energy.peak = newVal;
1706
+ energy.hold = 30;
1707
+ }
1708
+ else {
1709
+ if ( energy.hold > 0 )
1710
+ energy.hold--;
1711
+ else if ( energy.peak > 0 )
1712
+ energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
1713
+ }
1714
+ }
1471
1715
 
1472
- // make sure width is integer for pixel accurate calculation, when no bar spacing is required
1473
- if ( barSpace == 0 && ! isLedDisplay )
1474
- width |= 0;
1716
+ // calculate and display (if enabled) the current frame rate
1717
+ const updateFPS = () => {
1718
+ this._frame++;
1719
+ const elapsed = timestamp - this._time;
1720
+
1721
+ if ( elapsed >= 1000 ) {
1722
+ this._fps = this._frame / ( elapsed / 1000 );
1723
+ this._frame = 0;
1724
+ this._time = timestamp;
1725
+ }
1726
+ if ( this.showFPS ) {
1727
+ const size = canvasX.height;
1728
+ ctx.font = `bold ${size}px ${FONT_FAMILY}`;
1729
+ ctx.fillStyle = FPS_COLOR;
1730
+ ctx.textAlign = 'right';
1731
+ ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
1732
+ }
1733
+ }
1734
+
1735
+ /* MAIN FUNCTION */
1475
1736
 
1476
1737
  let currentEnergy = 0;
1477
1738
 
1478
- const nBars = this._bars.length,
1739
+ const bars = this._bars,
1740
+ nBars = bars.length,
1479
1741
  nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
1480
1742
 
1481
1743
  for ( let channel = 0; channel < nChannels; channel++ ) {
1482
1744
 
1483
- const channelTop = channelLayout == CHANNEL_VERTICAL ? channelHeight * channel + channelGap * channel : 0,
1484
- channelBottom = channelTop + channelHeight,
1485
- analyzerBottom = channelTop + analyzerHeight - ( isLedDisplay && ! this._maximizeLeds ? ledSpaceV : 0 ),
1486
- bgColor = ( ! showBgColor || isLedDisplay && ! isOverlay ) ? '#000' : this._gradients[ this._selectedGrads[ channel ] ].bgColor,
1487
- mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
1745
+ const { channelTop, channelBottom, analyzerBottom } = channelCoords[ channel ],
1746
+ channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
1747
+ colorStops = channelGradient.colorStops,
1748
+ colorCount = colorStops.length,
1749
+ bgColor = ( ! showBgColor || isLeds && ! isOverlay ) ? '#000' : channelGradient.bgColor,
1750
+ mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
1751
+
1752
+ // helper function for FFT data interpolation (uses fftData)
1753
+ const interpolate = ( bin, ratio ) => {
1754
+ const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
1755
+ return isNaN( value ) ? -Infinity : value;
1756
+ }
1757
+
1758
+ // set fillStyle and strokeStyle according to current colorMode (uses: channel, colorStops, colorCount)
1759
+ const setBarColor = ( value = 0, barIndex = 0 ) => {
1760
+ let color;
1761
+ // for mode 10, always use the channel gradient (ignore colorMode)
1762
+ if ( ( colorMode == COLOR_GRADIENT && ! isTrueLeds ) || mode == 10 )
1763
+ color = canvasGradients[ channel ];
1764
+ else {
1765
+ const selectedIndex = colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
1766
+ color = colorStops[ selectedIndex ].color;
1767
+ }
1768
+ ctx.fillStyle = ctx.strokeStyle = color;
1769
+ }
1488
1770
 
1489
1771
  if ( useCanvas ) {
1490
1772
  // clear the channel area, if in overlay mode
@@ -1506,60 +1788,25 @@ export default class AudioMotionAnalyzer {
1506
1788
  ctx.globalAlpha = 1;
1507
1789
  }
1508
1790
 
1509
- // draw dB scale (Y-axis)
1510
- if ( this.showScaleY && ! isLumiBars && ! isRadial ) {
1511
- const scaleWidth = canvasX.height,
1512
- fontSize = scaleWidth >> 1,
1513
- max = isLinear ? 100 : maxdB,
1514
- min = isLinear ? 0 : mindB,
1515
- incr = isLinear ? 20 : 5,
1516
- interval = analyzerHeight / ( max - min );
1517
-
1518
- ctx.fillStyle = SCALEY_LABEL_COLOR;
1519
- ctx.font = `${fontSize}px ${FONT_FAMILY}`;
1520
- ctx.textAlign = 'right';
1521
- ctx.lineWidth = 1;
1522
-
1523
- for ( let val = max; val > min; val -= incr ) {
1524
- const posY = channelTop + ( max - val ) * interval,
1525
- even = ( val % 2 == 0 ) | 0;
1526
-
1527
- if ( even ) {
1528
- const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
1529
- if ( mirrorMode != -1 )
1530
- ctx.fillText( val, scaleWidth * .85, labelY );
1531
- if ( mirrorMode != 1 )
1532
- ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
1533
- ctx.strokeStyle = SCALEY_LABEL_COLOR;
1534
- ctx.setLineDash([2,4]);
1535
- ctx.lineDashOffset = 0;
1536
- }
1537
- else {
1538
- ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1539
- ctx.setLineDash([2,8]);
1540
- ctx.lineDashOffset = 1;
1541
- }
1542
-
1543
- ctx.beginPath();
1544
- ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
1545
- ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
1546
- ctx.stroke();
1547
- }
1548
- // restore line properties
1549
- ctx.setLineDash([]);
1550
- ctx.lineDashOffset = 0;
1551
- }
1791
+ // draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
1792
+ if ( this.showScaleY && ! isLumi && ! isRadial && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) )
1793
+ drawScaleY( channelTop );
1552
1794
 
1553
1795
  // set line width and dash for LEDs effect
1554
- if ( isLedDisplay ) {
1796
+ if ( isLeds ) {
1555
1797
  ctx.setLineDash( [ ledHeight, ledSpaceV ] );
1556
- ctx.lineWidth = width;
1798
+ ctx.lineWidth = bars[0].width;
1557
1799
  }
1558
1800
  else // for outline effect ensure linewidth is not greater than half the bar width
1559
- ctx.lineWidth = isOutline ? Math.min( lineWidth, width / 2 ) : lineWidth;
1560
-
1561
- // set selected gradient for fill and stroke
1562
- ctx.fillStyle = ctx.strokeStyle = canvasGradients[ channel ];
1801
+ ctx.lineWidth = isOutline ? Math.min( lineWidth, bars[0].width / 2 ) : lineWidth;
1802
+
1803
+ // set clip region
1804
+ ctx.save();
1805
+ if ( ! isRadial ) {
1806
+ const channelRegion = new Path2D();
1807
+ channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
1808
+ ctx.clip( channelRegion );
1809
+ }
1563
1810
  } // if ( useCanvas )
1564
1811
 
1565
1812
  // get a new array of data from the FFT
@@ -1570,12 +1817,6 @@ export default class AudioMotionAnalyzer {
1570
1817
  if ( weightingFilter )
1571
1818
  fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
1572
1819
 
1573
- // helper function for FFT data interpolation
1574
- const interpolate = ( bin, ratio ) => {
1575
- const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
1576
- return isNaN( value ) ? -Infinity : value;
1577
- }
1578
-
1579
1820
  // start drawing path (for mode 10)
1580
1821
  ctx.beginPath();
1581
1822
 
@@ -1584,24 +1825,24 @@ export default class AudioMotionAnalyzer {
1584
1825
 
1585
1826
  // draw bars / lines
1586
1827
 
1587
- for ( let i = 0; i < nBars; i++ ) {
1828
+ for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
1588
1829
 
1589
- const bar = this._bars[ i ],
1590
- { freq, binLo, binHi, ratioLo, ratioHi } = bar;
1830
+ const bar = bars[ barIndex ],
1831
+ { posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
1591
1832
 
1592
- let barHeight = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
1833
+ let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
1593
1834
 
1594
1835
  // check additional bins (if any) for this bar and keep the highest value
1595
1836
  for ( let j = binLo + 1; j < binHi; j++ ) {
1596
- if ( fftData[ j ] > barHeight )
1597
- barHeight = fftData[ j ];
1837
+ if ( fftData[ j ] > barValue )
1838
+ barValue = fftData[ j ];
1598
1839
  }
1599
1840
 
1600
1841
  // normalize bar amplitude in [0;1] range
1601
- barHeight = this._normalizedB( barHeight );
1842
+ barValue = this._normalizedB( barValue );
1602
1843
 
1603
- bar.value[ channel ] = barHeight;
1604
- currentEnergy += barHeight;
1844
+ bar.value[ channel ] = barValue;
1845
+ currentEnergy += barValue;
1605
1846
 
1606
1847
  // update bar peak
1607
1848
  if ( bar.peak[ channel ] > 0 ) {
@@ -1612,8 +1853,8 @@ export default class AudioMotionAnalyzer {
1612
1853
  }
1613
1854
 
1614
1855
  // check if it's a new peak for this bar
1615
- if ( barHeight >= bar.peak[ channel ] ) {
1616
- bar.peak[ channel ] = barHeight;
1856
+ if ( barValue >= bar.peak[ channel ] ) {
1857
+ bar.peak[ channel ] = barValue;
1617
1858
  bar.hold[ channel ] = 30; // set peak hold time to 30 frames (0.5s)
1618
1859
  }
1619
1860
 
@@ -1622,31 +1863,30 @@ export default class AudioMotionAnalyzer {
1622
1863
  continue;
1623
1864
 
1624
1865
  // set opacity for bar effects
1625
- if ( isLumiBars || isAlphaBars )
1626
- ctx.globalAlpha = barHeight;
1866
+ if ( isLumi || isAlpha )
1867
+ ctx.globalAlpha = barValue;
1627
1868
  else if ( isOutline )
1628
1869
  ctx.globalAlpha = fillAlpha;
1629
1870
 
1871
+ // set fillStyle and strokeStyle for the current bar
1872
+ setBarColor( barValue, barIndex );
1873
+
1630
1874
  // compute actual bar height on screen
1631
- barHeight = isLedDisplay ? ledPosY( barHeight ) : barHeight * maxBarHeight | 0;
1875
+ let barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
1632
1876
 
1633
1877
  // invert bar for radial channel 1
1634
1878
  if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
1635
1879
  barHeight *= -1;
1636
1880
 
1637
- // bar width may need small adjustments for some bars, when barSpace == 0
1638
- let adjWidth = width,
1639
- posX = bar.posX;
1640
-
1641
1881
  // Draw current bar or line segment
1642
1882
 
1643
1883
  if ( mode == 10 ) {
1644
- // compute the average between the initial bar (i==0) and the next one
1884
+ // compute the average between the initial bar (barIndex==0) and the next one
1645
1885
  // used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
1646
- const nextBarAvg = i ? 0 : ( this._normalizedB( fftData[ this._bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
1886
+ const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
1647
1887
 
1648
1888
  if ( isRadial ) {
1649
- if ( i == 0 )
1889
+ if ( barIndex == 0 )
1650
1890
  ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ), 1 ) );
1651
1891
  // draw line to the current point, avoiding overlapping wrap-around frequencies
1652
1892
  if ( posX >= 0 ) {
@@ -1656,7 +1896,7 @@ export default class AudioMotionAnalyzer {
1656
1896
  }
1657
1897
  }
1658
1898
  else { // Linear
1659
- if ( i == 0 ) {
1899
+ if ( barIndex == 0 ) {
1660
1900
  // start the line off-screen using the previous FFT bin value as the initial amplitude
1661
1901
  if ( mirrorMode != -1 ) {
1662
1902
  const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
@@ -1672,57 +1912,50 @@ export default class AudioMotionAnalyzer {
1672
1912
  }
1673
1913
  }
1674
1914
  else {
1675
- if ( mode > 0 ) {
1676
- if ( isLedDisplay )
1677
- posX += Math.max( ledSpaceH / 2, barSpacePx / 2 );
1678
- else {
1679
- if ( barSpace == 0 ) {
1680
- posX |= 0;
1681
- if ( i > 0 && posX > this._bars[ i - 1 ].posX + width ) {
1682
- posX--;
1683
- adjWidth++;
1684
- }
1685
- }
1686
- else
1687
- posX += barSpacePx / 2;
1688
- }
1689
- }
1690
-
1691
- if ( isLedDisplay ) {
1692
- const x = posX + width / 2;
1693
- // draw "unlit" leds
1694
- if ( showBgColor && ! isOverlay ) {
1915
+ if ( isLeds ) {
1916
+ // draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
1917
+ if ( showBgColor && ! isOverlay && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) ) {
1695
1918
  const alpha = ctx.globalAlpha;
1696
- ctx.beginPath();
1697
- ctx.moveTo( x, channelTop );
1698
- ctx.lineTo( x, analyzerBottom );
1699
1919
  ctx.strokeStyle = LEDS_UNLIT_COLOR;
1700
1920
  ctx.globalAlpha = 1;
1701
- ctx.stroke();
1921
+ strokeBar( barCenter, channelTop, analyzerBottom );
1702
1922
  // restore properties
1703
1923
  ctx.strokeStyle = ctx.fillStyle;
1704
1924
  ctx.globalAlpha = alpha;
1705
1925
  }
1706
- ctx.beginPath();
1707
- ctx.moveTo( x, isLumiBars ? channelTop : analyzerBottom );
1708
- ctx.lineTo( x, isLumiBars ? channelBottom : analyzerBottom - barHeight );
1709
- ctx.stroke();
1926
+ if ( isTrueLeds ) {
1927
+ // ledPosY() is used below to fit one entire led height into the selected range
1928
+ const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
1929
+ let last = analyzerBottom;
1930
+ for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
1931
+ ctx.strokeStyle = colorStops[ i ].color;
1932
+ let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
1933
+ strokeBar( barCenter, last, y );
1934
+ last = y - ledSpaceV;
1935
+ }
1936
+ }
1937
+ else
1938
+ strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
1710
1939
  }
1711
1940
  else if ( posX >= initialX ) {
1712
1941
  if ( isRadial )
1713
- radialPoly( posX, 0, adjWidth, barHeight, isOutline );
1714
- else {
1715
- const x = posX,
1716
- y = isLumiBars ? channelTop : analyzerBottom,
1717
- w = adjWidth,
1718
- h = isLumiBars ? channelBottom : -barHeight;
1942
+ radialPoly( posX, 0, width, barHeight, isOutline );
1943
+ else if ( isRound ) {
1944
+ const halfWidth = width / 2,
1945
+ y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
1719
1946
 
1720
1947
  ctx.beginPath();
1721
- ctx.moveTo( x, y );
1722
- ctx.lineTo( x, y + h );
1723
- ctx.lineTo( x + w, y + h );
1724
- ctx.lineTo( x + w, y );
1725
-
1948
+ ctx.moveTo( posX, y );
1949
+ ctx.lineTo( posX, y - barHeight );
1950
+ ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
1951
+ ctx.lineTo( posX + width, y );
1952
+ strokeIf( isOutline );
1953
+ ctx.fill();
1954
+ }
1955
+ else {
1956
+ const offset = isOutline ? ctx.lineWidth : 0;
1957
+ ctx.beginPath();
1958
+ ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
1726
1959
  strokeIf( isOutline );
1727
1960
  ctx.fill();
1728
1961
  }
@@ -1731,36 +1964,44 @@ export default class AudioMotionAnalyzer {
1731
1964
 
1732
1965
  // Draw peak
1733
1966
  const peak = bar.peak[ channel ];
1734
- if ( peak > 0 && this.showPeaks && ! isLumiBars && posX >= initialX && posX < finalX ) {
1735
- // choose the best opacity for the peaks
1967
+ if ( peak > 0 && this.showPeaks && ! isLumi && posX >= initialX && posX < finalX ) {
1968
+ // set opacity
1736
1969
  if ( isOutline && lineWidth > 0 )
1737
1970
  ctx.globalAlpha = 1;
1738
- else if ( isAlphaBars )
1971
+ else if ( isAlpha )
1739
1972
  ctx.globalAlpha = peak;
1740
1973
 
1974
+ // select the peak color for 'bar-level' colorMode or 'trueLeds'
1975
+ if ( colorMode == COLOR_BAR_LEVEL || isTrueLeds )
1976
+ setBarColor( peak );
1977
+
1741
1978
  // render peak according to current mode / effect
1742
- if ( isLedDisplay ) {
1979
+ if ( isLeds ) {
1743
1980
  const ledPeak = ledPosY( peak );
1744
- if ( ledPeak >= ledHeight ) // avoid peak below zero level
1981
+ if ( ledPeak >= ledSpaceV ) // avoid peak below first led
1745
1982
  ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
1746
1983
  }
1747
1984
  else if ( ! isRadial )
1748
- ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
1985
+ ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
1749
1986
  else if ( mode != 10 ) // radial - no peaks for mode 10
1750
- radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), adjWidth, -2 );
1987
+ radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), width, -2 );
1751
1988
  }
1752
1989
 
1753
- } // for ( let i = 0; i < nBars; i++ )
1990
+ } // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
1754
1991
 
1755
1992
  // if not using the canvas, move earlier to the next channel
1756
1993
  if ( ! useCanvas )
1757
1994
  continue;
1758
1995
 
1996
+ ctx.restore(); // restore clip region
1997
+
1759
1998
  // restore global alpha
1760
1999
  ctx.globalAlpha = 1;
1761
2000
 
1762
2001
  // Fill/stroke drawing path for mode 10
1763
2002
  if ( mode == 10 ) {
2003
+ setBarColor(); // select channel gradient
2004
+
1764
2005
  if ( isRadial ) {
1765
2006
  if ( mirrorMode ) {
1766
2007
  let p;
@@ -1790,47 +2031,12 @@ export default class AudioMotionAnalyzer {
1790
2031
  }
1791
2032
  }
1792
2033
 
1793
- // Reflex effect
1794
- if ( this._reflexRatio > 0 && ! isLumiBars ) {
1795
- let posY, height;
1796
- if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
1797
- posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
1798
- height = channelHeight - analyzerHeight;
1799
- }
1800
- else {
1801
- posY = canvas.height - analyzerHeight * 2;
1802
- height = analyzerHeight;
1803
- }
1804
-
1805
- // set alpha and brightness for the reflection
1806
- ctx.globalAlpha = this.reflexAlpha;
1807
- if ( this.reflexBright != 1 )
1808
- ctx.filter = `brightness(${this.reflexBright})`;
1809
-
1810
- // create the reflection
1811
- ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
1812
- ctx.drawImage( canvas, 0, channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
1813
-
1814
- // reset changed properties
1815
- ctx.setTransform( 1, 0, 0, 1, 0, 0 );
1816
- ctx.filter = 'none';
1817
- ctx.globalAlpha = 1;
1818
- }
2034
+ // create Reflex effect
2035
+ doReflex( channel );
1819
2036
 
1820
2037
  } // for ( let channel = 0; channel < nChannels; channel++ ) {
1821
2038
 
1822
- // Update energy
1823
- energy.val = currentEnergy / ( nBars << ( nChannels - 1 ) );
1824
- if ( energy.val >= energy.peak ) {
1825
- energy.peak = energy.val;
1826
- energy.hold = 30;
1827
- }
1828
- else {
1829
- if ( energy.hold > 0 )
1830
- energy.hold--;
1831
- else if ( energy.peak > 0 )
1832
- energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
1833
- }
2039
+ updateEnergy( currentEnergy / ( nBars << ( nChannels - 1 ) ) );
1834
2040
 
1835
2041
  if ( useCanvas ) {
1836
2042
  // Mirror effect
@@ -1844,37 +2050,11 @@ export default class AudioMotionAnalyzer {
1844
2050
  ctx.setLineDash([]);
1845
2051
 
1846
2052
  // draw frequency scale (X-axis)
1847
- if ( this.showScaleX ) {
1848
- if ( isRadial ) {
1849
- ctx.save();
1850
- ctx.translate( centerX, centerY );
1851
- if ( this._spinSpeed )
1852
- ctx.rotate( this._spinAngle + HALF_PI );
1853
- ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1854
- ctx.restore();
1855
- }
1856
- else
1857
- ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1858
- }
2053
+ drawScaleX();
1859
2054
  }
1860
2055
 
1861
- // calculate and update current frame rate
1862
-
1863
- this._frame++;
1864
- const elapsed = timestamp - this._time;
1865
-
1866
- if ( elapsed >= 1000 ) {
1867
- this._fps = this._frame / ( elapsed / 1000 );
1868
- this._frame = 0;
1869
- this._time = timestamp;
1870
- }
1871
- if ( this.showFPS ) {
1872
- const size = canvasX.height;
1873
- ctx.font = `bold ${size}px ${FONT_FAMILY}`;
1874
- ctx.fillStyle = FPS_COLOR;
1875
- ctx.textAlign = 'right';
1876
- ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
1877
- }
2056
+ // calculate and display (if enabled) the current frame rate
2057
+ updateFPS();
1878
2058
 
1879
2059
  // call callback function, if defined
1880
2060
  if ( this.onCanvasDraw ) {
@@ -1925,18 +2105,17 @@ export default class AudioMotionAnalyzer {
1925
2105
  const ctx = this._canvasCtx,
1926
2106
  canvas = ctx.canvas,
1927
2107
  channelLayout = this._chLayout,
1928
- isLumiBars = this._isLumiBars,
2108
+ { isLumi } = this._flg,
1929
2109
  isRadial = this._radial,
1930
- gradientHeight = isLumiBars ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
2110
+ gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
1931
2111
  // for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
1932
2112
  analyzerRatio = 1 - this._reflexRatio,
1933
- initialX = this._initialX;
2113
+ { analyzerWidth, initialX, radius } = this._aux;
1934
2114
 
1935
2115
  // for radial mode
1936
2116
  const centerX = canvas.width >> 1,
1937
2117
  centerY = canvas.height >> 1,
1938
- maxRadius = Math.min( centerX, centerY ),
1939
- radius = this._radius;
2118
+ maxRadius = Math.min( centerX, centerY );
1940
2119
 
1941
2120
  for ( const channel of [0,1] ) {
1942
2121
  const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
@@ -1948,26 +2127,23 @@ export default class AudioMotionAnalyzer {
1948
2127
  if ( isRadial )
1949
2128
  grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( channelLayout == CHANNEL_VERTICAL ) );
1950
2129
  else
1951
- grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + this._analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
2130
+ grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
1952
2131
 
1953
2132
  if ( colorStops ) {
1954
2133
  const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
1955
2134
 
1956
- // helper function
1957
- const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
1958
-
1959
2135
  for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
2136
+ const maxIndex = colorStops.length - 1;
1960
2137
 
1961
- colorStops.forEach( ( colorInfo, index ) => {
1962
- const maxIndex = colorStops.length - 1;
1963
- let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / Math.max( 1, maxIndex );
2138
+ colorStops.forEach( ( colorStop, index ) => {
2139
+ let offset = colorStop.pos;
1964
2140
 
1965
2141
  // in dual mode (not split), use half the original offset for each channel
1966
2142
  if ( dual )
1967
2143
  offset /= 2;
1968
2144
 
1969
2145
  // constrain the offset within the useful analyzer areas (avoid reflex areas)
1970
- if ( channelLayout == CHANNEL_VERTICAL && ! isLumiBars && ! isRadial && ! isHorizontal ) {
2146
+ if ( channelLayout == CHANNEL_VERTICAL && ! isLumi && ! isRadial && ! isHorizontal ) {
1971
2147
  offset *= analyzerRatio;
1972
2148
  // skip the first reflex area in split mode
1973
2149
  if ( ! dual && offset > .5 * analyzerRatio )
@@ -1977,26 +2153,26 @@ export default class AudioMotionAnalyzer {
1977
2153
  // only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
1978
2154
  if ( channelArea == 1 ) {
1979
2155
  // add colors in reverse order if radial or lumi are active
1980
- if ( isRadial || isLumiBars ) {
2156
+ if ( isRadial || isLumi ) {
1981
2157
  const revIndex = maxIndex - index;
1982
- colorInfo = colorStops[ revIndex ];
1983
- offset = 1 - ( colorInfo.pos !== undefined ? colorInfo.pos : revIndex / Math.max( 1, maxIndex ) ) / 2;
2158
+ colorStop = colorStops[ revIndex ];
2159
+ offset = 1 - colorStop.pos / 2;
1984
2160
  }
1985
2161
  else {
1986
2162
  // if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
1987
2163
  if ( index == 0 && offset > 0 )
1988
- addColorStop( .5, colorInfo );
2164
+ grad.addColorStop( .5, colorStop.color );
1989
2165
  // bump the offset to the second half of the gradient
1990
2166
  offset += .5;
1991
2167
  }
1992
2168
  }
1993
2169
 
1994
2170
  // add gradient color stop
1995
- addColorStop( offset, colorInfo );
2171
+ grad.addColorStop( offset, colorStop.color );
1996
2172
 
1997
2173
  // create additional color stop at the end of first channel to prevent bleeding
1998
2174
  if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
1999
- addColorStop( .5, colorInfo );
2175
+ grad.addColorStop( .5, colorStop.color );
2000
2176
  });
2001
2177
  } // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
2002
2178
  }
@@ -2064,9 +2240,6 @@ export default class AudioMotionAnalyzer {
2064
2240
  canvas.width = newWidth;
2065
2241
  canvas.height = newHeight;
2066
2242
 
2067
- // update internal variables
2068
- this._calcAux();
2069
-
2070
2243
  // if not in overlay mode, paint the canvas black
2071
2244
  if ( ! this.overlay ) {
2072
2245
  ctx.fillStyle = '#000';
@@ -2078,14 +2251,14 @@ export default class AudioMotionAnalyzer {
2078
2251
 
2079
2252
  // update dimensions of the scale canvas
2080
2253
  canvasX.width = newWidth;
2081
- canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) / 27 | 0 );
2082
-
2083
- // (re)generate gradient
2084
- this._makeGrad();
2254
+ canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) / 32 | 0 );
2085
2255
 
2086
2256
  // calculate bar positions and led options
2087
2257
  this._calcBars();
2088
2258
 
2259
+ // (re)generate gradient
2260
+ this._makeGrad();
2261
+
2089
2262
  // detect fullscreen changes (for Safari)
2090
2263
  if ( this._fsStatus !== undefined && this._fsStatus !== isFullscreen )
2091
2264
  reason = REASON_FSCHANGE;
@@ -2127,6 +2300,7 @@ export default class AudioMotionAnalyzer {
2127
2300
  barSpace : 0.1,
2128
2301
  bgAlpha : 0.7,
2129
2302
  channelLayout : CHANNEL_SINGLE,
2303
+ colorMode : COLOR_GRADIENT,
2130
2304
  fftSize : 8192,
2131
2305
  fillAlpha : 1,
2132
2306
  frequencyScale : SCALE_LOG,
@@ -2151,6 +2325,7 @@ export default class AudioMotionAnalyzer {
2151
2325
  reflexBright : 1,
2152
2326
  reflexFit : true,
2153
2327
  reflexRatio : 0,
2328
+ roundBars : false,
2154
2329
  showBgColor : true,
2155
2330
  showFPS : false,
2156
2331
  showPeaks : true,
@@ -2160,6 +2335,7 @@ export default class AudioMotionAnalyzer {
2160
2335
  spinSpeed : 0,
2161
2336
  splitGradient : false,
2162
2337
  start : true,
2338
+ trueLeds : false,
2163
2339
  useCanvas : true,
2164
2340
  volume : 1,
2165
2341
  weightingFilter: FILTER_NONE