audiomotion-analyzer 4.1.0 → 4.2.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,17 +2,16 @@
2
2
  * audioMotion-analyzer
3
3
  * High-resolution real-time graphic audio spectrum analyzer JS module
4
4
  *
5
- * @version 4.1.0
5
+ * @version 4.2.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.1.0';
10
+ const VERSION = '4.2.0';
11
11
 
12
12
  // internal constants
13
13
  const TAU = 2 * Math.PI,
14
14
  HALF_PI = Math.PI / 2,
15
- RPM = TAU / 3600, // angle increment per frame for one revolution per minute @60fps
16
15
  C_1 = 8.17579892; // frequency for C -1
17
16
 
18
17
  const CANVAS_BACKGROUND_COLOR = '#000',
@@ -22,6 +21,10 @@ const CANVAS_BACKGROUND_COLOR = '#000',
22
21
  COLOR_BAR_INDEX = 'bar-index',
23
22
  COLOR_BAR_LEVEL = 'bar-level',
24
23
  COLOR_GRADIENT = 'gradient',
24
+ DEBOUNCE_TIMEOUT = 60,
25
+ EVENT_CLICK = 'click',
26
+ EVENT_FULLSCREENCHANGE = 'fullscreenchange',
27
+ EVENT_RESIZE = 'resize',
25
28
  GRADIENT_DEFAULT_BGCOLOR = '#111',
26
29
  FILTER_NONE = '',
27
30
  FILTER_A = 'A',
@@ -35,7 +38,7 @@ const CANVAS_BACKGROUND_COLOR = '#000',
35
38
  REASON_CREATE = 'create',
36
39
  REASON_FSCHANGE = 'fschange',
37
40
  REASON_LORES = 'lores',
38
- REASON_RESIZE = 'resize',
41
+ REASON_RESIZE = EVENT_RESIZE,
39
42
  REASON_USER = 'user',
40
43
  SCALEX_BACKGROUND_COLOR = '#000c',
41
44
  SCALEX_LABEL_COLOR = '#fff',
@@ -102,6 +105,21 @@ const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated.
102
105
  // returns the validated value, or the first element of `list` if `value` is not found in the array
103
106
  const validateFromList = ( value, list, modifier = 'toLowerCase' ) => list[ Math.max( 0, list.indexOf( ( '' + value )[ modifier ]() ) ) ];
104
107
 
108
+ // helper function - find the Y-coordinate of a point located between two other points, given its X-coordinate
109
+ const findY = ( x1, y1, x2, y2, x ) => y1 + ( y2 - y1 ) * ( x - x1 ) / ( x2 - x1 );
110
+
111
+ // Polyfill for Array.findLastIndex()
112
+ if ( ! Array.prototype.findLastIndex ) {
113
+ Array.prototype.findLastIndex = function( callback ) {
114
+ let index = this.length;
115
+ while ( index-- > 0 ) {
116
+ if ( callback( this[ index ] ) )
117
+ return index;
118
+ }
119
+ return -1;
120
+ }
121
+ }
122
+
105
123
  // AudioMotionAnalyzer class
106
124
 
107
125
  export default class AudioMotionAnalyzer {
@@ -118,11 +136,18 @@ export default class AudioMotionAnalyzer {
118
136
  this._ready = false;
119
137
 
120
138
  // Initialize internal objects
121
- this._aux = {};
122
- this._flg = {};
139
+ this._aux = {}; // auxiliary variables
140
+ this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
141
+ this._destroyed = false;
142
+ this._energy = { val: 0, peak: 0, hold: 0 };
143
+ this._flg = {}; // flags
144
+ this._fps = 0;
123
145
  this._gradients = {}; // registered gradients
146
+ this._last = 0; // timestamp of last rendered frame
147
+ this._outNodes = []; // output nodes
148
+ this._ownContext = false;
124
149
  this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
125
- this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
150
+ this._sources = []; // input nodes
126
151
 
127
152
  // Register built-in gradients
128
153
  for ( const [ name, options ] of GRADIENTS )
@@ -148,6 +173,7 @@ export default class AudioMotionAnalyzer {
148
173
  else {
149
174
  try {
150
175
  audioCtx = new ( window.AudioContext || window.webkitAudioContext )();
176
+ this._ownContext = true;
151
177
  }
152
178
  catch( err ) {
153
179
  throw new AudioMotionError( ERR_AUDIO_CONTEXT_FAIL );
@@ -162,13 +188,13 @@ export default class AudioMotionAnalyzer {
162
188
  Connection routing:
163
189
  ===================
164
190
 
165
- for dual channel modes: +---> analyzer[0] ---+
191
+ for dual channel layouts: +---> analyzer[0] ---+
166
192
  | |
167
193
  (source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
168
194
  | |
169
195
  +---> analyzer[1] ---+
170
196
 
171
- for single channel mode:
197
+ for single channel layout:
172
198
 
173
199
  (source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
174
200
 
@@ -181,8 +207,7 @@ export default class AudioMotionAnalyzer {
181
207
  this._input = audioCtx.createGain();
182
208
  this._output = audioCtx.createGain();
183
209
 
184
- // initialize sources array and connect audio source if provided in the options
185
- this._sources = [];
210
+ // connect audio source if provided in the options
186
211
  if ( options.source )
187
212
  this.connectInput( options.source );
188
213
 
@@ -194,17 +219,13 @@ export default class AudioMotionAnalyzer {
194
219
  merger.connect( this._output );
195
220
 
196
221
  // connect output -> destination (speakers)
197
- this._outNodes = [];
198
222
  if ( options.connectSpeakers !== false )
199
223
  this.connectOutput();
200
224
 
201
- // initialize object to save energy
202
- this._energy = { val: 0, peak: 0, hold: 0 };
203
-
204
225
  // create analyzer canvas
205
226
  const canvas = document.createElement('canvas');
206
227
  canvas.style = 'max-width: 100%;';
207
- this._canvasCtx = canvas.getContext('2d');
228
+ this._ctx = canvas.getContext('2d');
208
229
 
209
230
  // create auxiliary canvases for the X-axis and radial scale labels
210
231
  for ( const ctx of [ '_scaleX', '_scaleR' ] )
@@ -231,21 +252,25 @@ export default class AudioMotionAnalyzer {
231
252
  this._setCanvas( REASON_RESIZE );
232
253
  this._fsTimeout = 0;
233
254
  }
234
- }, 60 );
255
+ }, DEBOUNCE_TIMEOUT );
235
256
  }
236
257
  }
237
258
 
238
259
  // if browser supports ResizeObserver, listen for resize on the container
239
260
  if ( window.ResizeObserver ) {
240
- const resizeObserver = new ResizeObserver( onResize );
241
- resizeObserver.observe( this._container );
261
+ this._observer = new ResizeObserver( onResize );
262
+ this._observer.observe( this._container );
242
263
  }
243
264
 
265
+ // create an AbortController to remove event listeners on destroy()
266
+ this._controller = new AbortController();
267
+ const signal = this._controller.signal;
268
+
244
269
  // listen for resize events on the window - required for fullscreen on iPadOS
245
- window.addEventListener( 'resize', onResize );
270
+ window.addEventListener( EVENT_RESIZE, onResize, { signal } );
246
271
 
247
272
  // listen for fullscreenchange events on the canvas - not available on Safari
248
- canvas.addEventListener( 'fullscreenchange', () => {
273
+ canvas.addEventListener( EVENT_FULLSCREENCHANGE, () => {
249
274
  // set flag to indicate a fullscreen change in progress
250
275
  this._fsChanging = true;
251
276
 
@@ -260,16 +285,24 @@ export default class AudioMotionAnalyzer {
260
285
  this._fsTimeout = window.setTimeout( () => {
261
286
  this._fsChanging = false;
262
287
  this._fsTimeout = 0;
263
- }, 60 );
264
- });
288
+ }, DEBOUNCE_TIMEOUT );
289
+ }, { signal } );
265
290
 
266
291
  // Resume audio context if in suspended state (browsers' autoplay policy)
267
292
  const unlockContext = () => {
268
293
  if ( audioCtx.state == 'suspended' )
269
294
  audioCtx.resume();
270
- window.removeEventListener( 'click', unlockContext );
295
+ window.removeEventListener( EVENT_CLICK, unlockContext );
271
296
  }
272
- window.addEventListener( 'click', unlockContext );
297
+ window.addEventListener( EVENT_CLICK, unlockContext );
298
+
299
+ // reset FPS-related variables when window becomes visible (avoid FPS drop due to frames not rendered while hidden)
300
+ document.addEventListener( 'visibilitychange', () => {
301
+ if ( document.visibilityState != 'hidden' ) {
302
+ this._frames = 0;
303
+ this._time = performance.now();
304
+ }
305
+ }, { signal } );
273
306
 
274
307
  // Set configuration options and use defaults for any missing properties
275
308
  this._setProps( options, true );
@@ -441,6 +474,13 @@ export default class AudioMotionAnalyzer {
441
474
  this._analyzer[ i ].maxDecibels = value;
442
475
  }
443
476
 
477
+ get maxFPS() {
478
+ return this._maxFPS;
479
+ }
480
+ set maxFPS( value ) {
481
+ this._maxFPS = value < 0 ? 0 : +value || 0;
482
+ }
483
+
444
484
  get maxFreq() {
445
485
  return this._maxFreq;
446
486
  }
@@ -512,6 +552,13 @@ export default class AudioMotionAnalyzer {
512
552
  this._calcBars();
513
553
  }
514
554
 
555
+ get peakLine() {
556
+ return this._peakLine;
557
+ }
558
+ set peakLine( value ) {
559
+ this._peakLine = !! value;
560
+ }
561
+
515
562
  get radial() {
516
563
  return this._radial;
517
564
  }
@@ -613,10 +660,10 @@ export default class AudioMotionAnalyzer {
613
660
  return this._input.context;
614
661
  }
615
662
  get canvas() {
616
- return this._canvasCtx.canvas;
663
+ return this._ctx.canvas;
617
664
  }
618
665
  get canvasCtx() {
619
- return this._canvasCtx;
666
+ return this._ctx;
620
667
  }
621
668
  get connectedSources() {
622
669
  return this._sources;
@@ -639,8 +686,11 @@ export default class AudioMotionAnalyzer {
639
686
  get isBandsMode() {
640
687
  return this._flg.isBands;
641
688
  }
689
+ get isDestroyed() {
690
+ return this._destroyed;
691
+ }
642
692
  get isFullscreen() {
643
- return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
693
+ return this._fsEl && ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
644
694
  }
645
695
  get isLedBars() {
646
696
  return this._flg.isLeds;
@@ -652,7 +702,7 @@ export default class AudioMotionAnalyzer {
652
702
  return this._flg.isOctaves;
653
703
  }
654
704
  get isOn() {
655
- return this._runId !== undefined;
705
+ return !! this._runId;
656
706
  }
657
707
  get isOutlineBars() {
658
708
  return this._flg.isOutline;
@@ -717,12 +767,54 @@ export default class AudioMotionAnalyzer {
717
767
  }
718
768
  }
719
769
 
770
+ /**
771
+ * Destroys instance
772
+ */
773
+ destroy() {
774
+ if ( ! this._ready )
775
+ return;
776
+
777
+ const { audioCtx, canvas, _controller, _input, _merger, _observer, _ownContext, _splitter } = this;
778
+
779
+ this._destroyed = true;
780
+ this._ready = false;
781
+ this.stop();
782
+
783
+ // remove event listeners
784
+ _controller.abort();
785
+ if ( _observer )
786
+ _observer.disconnect();
787
+
788
+ // clear callbacks and fullscreen element
789
+ this.onCanvasResize = null;
790
+ this.onCanvasDraw = null;
791
+ this._fsEl = null;
792
+
793
+ // disconnect audio nodes
794
+ this.disconnectInput();
795
+ this.disconnectOutput(); // also disconnects analyzer nodes
796
+ _input.disconnect();
797
+ _splitter.disconnect();
798
+ _merger.disconnect();
799
+
800
+ // if audio context is our own (not provided by the user), close it
801
+ if ( _ownContext )
802
+ audioCtx.close();
803
+
804
+ // remove canvas from the DOM
805
+ canvas.remove();
806
+
807
+ // reset flags
808
+ this._calcBars();
809
+ }
810
+
720
811
  /**
721
812
  * Disconnects audio sources from the analyzer
722
813
  *
723
- * @param [{object|array}] a connected AudioNode object or an array of such objects; if undefined, all connected nodes are disconnected
814
+ * @param [{object|array}] a connected AudioNode object or an array of such objects; if falsy, all connected nodes are disconnected
815
+ * @param [{boolean}] if true, stops/releases audio tracks from disconnected media streams (e.g. microphone)
724
816
  */
725
- disconnectInput( sources ) {
817
+ disconnectInput( sources, stopTracks ) {
726
818
  if ( ! sources )
727
819
  sources = Array.from( this._sources );
728
820
  else if ( ! Array.isArray( sources ) )
@@ -730,6 +822,11 @@ export default class AudioMotionAnalyzer {
730
822
 
731
823
  for ( const node of sources ) {
732
824
  const idx = this._sources.indexOf( node );
825
+ if ( stopTracks && node.mediaStream ) {
826
+ for ( const track of node.mediaStream.getAudioTracks() ) {
827
+ track.stop();
828
+ }
829
+ }
733
830
  if ( idx >= 0 ) {
734
831
  node.disconnect( this._input );
735
832
  this._sources.splice( idx, 1 );
@@ -927,26 +1024,42 @@ export default class AudioMotionAnalyzer {
927
1024
  }
928
1025
  }
929
1026
 
1027
+ /**
1028
+ * Start the analyzer
1029
+ */
1030
+ start() {
1031
+ this.toggleAnalyzer( true );
1032
+ }
1033
+
1034
+ /**
1035
+ * Stop the analyzer
1036
+ */
1037
+ stop() {
1038
+ this.toggleAnalyzer( false );
1039
+ }
1040
+
930
1041
  /**
931
1042
  * Start / stop canvas animation
932
1043
  *
933
- * @param {boolean} [value] if undefined, inverts the current status
934
- * @returns {boolean} resulting status after the change
1044
+ * @param {boolean} [force] if undefined, inverts the current state
1045
+ * @returns {boolean} resulting state after the change
935
1046
  */
936
- toggleAnalyzer( value ) {
937
- const started = this.isOn;
1047
+ toggleAnalyzer( force ) {
1048
+ const hasStarted = this.isOn;
938
1049
 
939
- if ( value === undefined )
940
- value = ! started;
1050
+ if ( force === undefined )
1051
+ force = ! hasStarted;
941
1052
 
942
- if ( started && ! value ) {
1053
+ // Stop the analyzer if it was already running and must be disabled
1054
+ if ( hasStarted && ! force ) {
943
1055
  cancelAnimationFrame( this._runId );
944
- this._runId = undefined;
1056
+ this._runId = 0;
945
1057
  }
946
- else if ( ! started && value ) {
947
- this._frame = this._fps = 0;
1058
+ // Start the analyzer if it was stopped and must be enabled
1059
+ else if ( ! hasStarted && force && ! this._destroyed ) {
1060
+ this._frames = 0;
948
1061
  this._time = performance.now();
949
- this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
1062
+ this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) ); // arrow function preserves the scope of *this*
950
1063
  }
951
1064
 
952
1065
  return this.isOn;
@@ -964,6 +1077,8 @@ export default class AudioMotionAnalyzer {
964
1077
  }
965
1078
  else {
966
1079
  const fsEl = this._fsEl;
1080
+ if ( ! fsEl )
1081
+ return;
967
1082
  if ( fsEl.requestFullscreen )
968
1083
  fsEl.requestFullscreen();
969
1084
  else if ( fsEl.webkitRequestFullscreen )
@@ -992,8 +1107,10 @@ export default class AudioMotionAnalyzer {
992
1107
  _calcBars() {
993
1108
  const bars = this._bars = []; // initialize object property
994
1109
 
995
- if ( ! this._ready )
1110
+ if ( ! this._ready ) {
1111
+ this._flg = { isAlpha: false, isBands: false, isLeds: false, isLumi: false, isOctaves: false, isOutline: false, isRound: false, noLedGap: false };
996
1112
  return;
1113
+ }
997
1114
 
998
1115
  const barSpace = this._barSpace,
999
1116
  canvas = this.canvas,
@@ -1357,28 +1474,22 @@ export default class AudioMotionAnalyzer {
1357
1474
  return;
1358
1475
 
1359
1476
  const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
1360
- canvas = this._canvasCtx.canvas,
1361
- scaleX = this._scaleX,
1362
- scaleR = this._scaleR,
1363
- canvasX = scaleX.canvas,
1364
- canvasR = scaleR.canvas,
1477
+ { canvas, _frequencyScale, _mirror, _noteLabels, _radial, _scaleX, _scaleR } = this,
1478
+ canvasX = _scaleX.canvas,
1479
+ canvasR = _scaleR.canvas,
1365
1480
  freqLabels = [],
1366
- frequencyScale= this._frequencyScale,
1367
- isNoteLabels = this._noteLabels,
1368
- isRadial = this._radial,
1369
1481
  isVertical = this._chLayout == CHANNEL_VERTICAL,
1370
- mirror = this._mirror,
1371
1482
  scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
1372
1483
  scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
1373
1484
  fontSizeX = canvasX.height >> 1,
1374
1485
  fontSizeR = scaleHeight >> 1,
1375
- labelWidthX = fontSizeX * ( isNoteLabels ? .7 : 1.5 ),
1376
- labelWidthR = fontSizeR * ( isNoteLabels ? 1 : 2 ),
1486
+ labelWidthX = fontSizeX * ( _noteLabels ? .7 : 1.5 ),
1487
+ labelWidthR = fontSizeR * ( _noteLabels ? 1 : 2 ),
1377
1488
  root12 = 2 ** ( 1 / 12 );
1378
1489
 
1379
- if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
1490
+ if ( ! _noteLabels && ( this._ansiBands || _frequencyScale != SCALE_LOG ) ) {
1380
1491
  freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
1381
- if ( frequencyScale == SCALE_LINEAR )
1492
+ if ( _frequencyScale == SCALE_LINEAR )
1382
1493
  freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
1383
1494
  else
1384
1495
  freqLabels.push( 8e3, 16e3 );
@@ -1390,8 +1501,8 @@ export default class AudioMotionAnalyzer {
1390
1501
  if ( freq >= this._minFreq && freq <= this._maxFreq ) {
1391
1502
  const pitch = scale[ note ],
1392
1503
  isC = pitch == 'C';
1393
- if ( ( pitch && isNoteLabels && ! mirror ) || isC )
1394
- freqLabels.push( isNoteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
1504
+ if ( ( pitch && _noteLabels && ! _mirror ) || isC )
1505
+ freqLabels.push( _noteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
1395
1506
  }
1396
1507
  freq *= root12;
1397
1508
  }
@@ -1411,27 +1522,27 @@ export default class AudioMotionAnalyzer {
1411
1522
  posX = radialY * Math.cos( adjAng ),
1412
1523
  posY = radialY * Math.sin( adjAng );
1413
1524
 
1414
- scaleR.save();
1415
- scaleR.translate( centerR + posX, centerR + posY );
1416
- scaleR.rotate( angle );
1417
- scaleR.fillText( label, 0, 0 );
1418
- scaleR.restore();
1525
+ _scaleR.save();
1526
+ _scaleR.translate( centerR + posX, centerR + posY );
1527
+ _scaleR.rotate( angle );
1528
+ _scaleR.fillText( label, 0, 0 );
1529
+ _scaleR.restore();
1419
1530
  }
1420
1531
 
1421
1532
  // clear scale canvas
1422
1533
  canvasX.width |= 0;
1423
1534
 
1424
- scaleX.fillStyle = scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
1425
- scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
1535
+ _scaleX.fillStyle = _scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
1536
+ _scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
1426
1537
 
1427
- scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
1428
- scaleR.lineWidth = scaleHeight;
1429
- scaleR.stroke();
1538
+ _scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
1539
+ _scaleR.lineWidth = scaleHeight;
1540
+ _scaleR.stroke();
1430
1541
 
1431
- scaleX.fillStyle = scaleR.fillStyle = SCALEX_LABEL_COLOR;
1432
- scaleX.font = `${ fontSizeX }px ${FONT_FAMILY}`;
1433
- scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
1434
- scaleX.textAlign = scaleR.textAlign = 'center';
1542
+ _scaleX.fillStyle = _scaleR.fillStyle = SCALEX_LABEL_COLOR;
1543
+ _scaleX.font = `${ fontSizeX }px ${FONT_FAMILY}`;
1544
+ _scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
1545
+ _scaleX.textAlign = _scaleR.textAlign = 'center';
1435
1546
 
1436
1547
  let prevX = -labelWidthX / 4,
1437
1548
  prevR = -labelWidthR;
@@ -1441,22 +1552,26 @@ export default class AudioMotionAnalyzer {
1441
1552
  x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
1442
1553
  y = canvasX.height * .75,
1443
1554
  isC = label[0] == 'C',
1444
- maxW = fontSizeX * ( isNoteLabels && ! mirror ? ( isC ? 1.2 : .6 ) : 3 );
1555
+ maxW = fontSizeX * ( _noteLabels && ! _mirror ? ( isC ? 1.2 : .6 ) : 3 );
1445
1556
 
1446
1557
  // 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;
1558
+ _scaleX.fillStyle = _scaleR.fillStyle = isC && ! _mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
1448
1559
 
1449
1560
  // prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
1450
- if ( isNoteLabels ) {
1561
+ if ( _noteLabels ) {
1562
+ const isLog = _frequencyScale == SCALE_LOG,
1563
+ isLinear = _frequencyScale == SCALE_LINEAR;
1564
+
1451
1565
  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 ) ) )
1566
+
1567
+ if ( isLog || freq > 2e3 || ( ! isLinear && freq > 250 ) ||
1568
+ ( ( ! _radial || isVertical ) && ( ! isLinear && freq > 125 || freq > 1e3 ) ) )
1454
1569
  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 ) ) )
1570
+ if ( isLog || freq > 4e3 || ( ! isLinear && freq > 500 ) ||
1571
+ ( ( ! _radial || isVertical ) && ( ! isLinear && freq > 250 || freq > 2e3 ) ) )
1457
1572
  allowedLabels.push('E');
1458
- if ( frequencyScale == SCALE_LINEAR && freq > 4e3 ||
1459
- ( ( ! isRadial || isVertical ) && ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ) ) )
1573
+ if ( isLinear && freq > 4e3 ||
1574
+ ( ( ! _radial || isVertical ) && ( isLog || freq > 2e3 || ( ! isLinear && freq > 500 ) ) ) )
1460
1575
  allowedLabels.push('D','F','A','B');
1461
1576
  if ( ! allowedLabels.includes( label[0] ) )
1462
1577
  continue; // skip this label
@@ -1464,16 +1579,16 @@ export default class AudioMotionAnalyzer {
1464
1579
 
1465
1580
  // linear scale
1466
1581
  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;
1582
+ _scaleX.fillText( label, initialX + x, y, maxW );
1583
+ if ( _mirror && ( x > labelWidthX || _mirror == 1 ) )
1584
+ _scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
1585
+ prevX = x + Math.min( maxW, _scaleX.measureText( label ).width ) / 2;
1471
1586
  }
1472
1587
 
1473
1588
  // radial scale
1474
1589
  if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
1475
1590
  radialLabel( x, label );
1476
- if ( mirror && ( x > labelWidthR || mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
1591
+ if ( _mirror && ( x > labelWidthR || _mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
1477
1592
  radialLabel( -x, label );
1478
1593
  prevR = x;
1479
1594
  }
@@ -1485,41 +1600,68 @@ export default class AudioMotionAnalyzer {
1485
1600
  * this is called 60 times per second by requestAnimationFrame()
1486
1601
  */
1487
1602
  _draw( timestamp ) {
1488
- const { isAlpha, isBands, isLeds, isLumi,
1489
- isOctaves, isOutline, isRound, noLedGap } = this._flg,
1490
- ctx = this._canvasCtx,
1491
- canvas = ctx.canvas,
1603
+ // schedule next canvas update
1604
+ this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
1605
+
1606
+ if ( this._maxFPS && ( timestamp - this._last < 1000 / this._maxFPS ) )
1607
+ return;
1608
+
1609
+ const { isAlpha,
1610
+ isBands,
1611
+ isLeds,
1612
+ isLumi,
1613
+ isOctaves,
1614
+ isOutline,
1615
+ isRound,
1616
+ noLedGap } = this._flg,
1617
+
1618
+ { analyzerHeight,
1619
+ channelCoords,
1620
+ channelHeight,
1621
+ channelGap,
1622
+ initialX,
1623
+ radius } = this._aux,
1624
+
1625
+ { _bars,
1626
+ canvas,
1627
+ _canvasGradients,
1628
+ _chLayout,
1629
+ _colorMode,
1630
+ _ctx,
1631
+ _energy,
1632
+ fillAlpha,
1633
+ _fps,
1634
+ _linearAmplitude,
1635
+ _lineWidth,
1636
+ maxDecibels,
1637
+ minDecibels,
1638
+ _mirror,
1639
+ _mode,
1640
+ overlay,
1641
+ _radial,
1642
+ showBgColor,
1643
+ showPeaks,
1644
+ useCanvas,
1645
+ _weightingFilter } = this,
1646
+
1492
1647
  canvasX = this._scaleX.canvas,
1493
1648
  canvasR = this._scaleR.canvas,
1494
- canvasGradients= this._canvasGradients,
1495
1649
  centerX = canvas.width >> 1,
1496
1650
  centerY = canvas.height >> 1,
1497
- colorMode = this._colorMode,
1498
- energy = this._energy,
1499
- fillAlpha = this.fillAlpha,
1500
- mode = this._mode,
1501
- isLinear = this._linearAmplitude,
1502
- isOverlay = this.overlay,
1503
- isRadial = this._radial,
1504
- isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
1505
- channelLayout = this._chLayout,
1506
- lineWidth = this._lineWidth,
1507
- mirrorMode = this._mirror,
1508
- { analyzerHeight, channelCoords,
1509
- channelHeight, channelGap, initialX, radius } = this._aux,
1510
- analyzerWidth = isRadial ? canvas.width : this._aux.analyzerWidth,
1651
+ holdFrames = _fps >> 1, // number of frames in half a second
1652
+ isDualVertical = _chLayout == CHANNEL_VERTICAL,
1653
+ isDualCombined = _chLayout == CHANNEL_COMBINED,
1654
+ isSingle = _chLayout == CHANNEL_SINGLE,
1655
+ isTrueLeds = isLeds && this._trueLeds && _colorMode == COLOR_GRADIENT,
1656
+ analyzerWidth = _radial ? canvas.width : this._aux.analyzerWidth,
1511
1657
  finalX = initialX + analyzerWidth,
1512
- showBgColor = this.showBgColor,
1513
- maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1514
- maxdB = this.maxDecibels,
1515
- mindB = this.minDecibels,
1516
- dbRange = maxdB - mindB,
1517
- useCanvas = this.useCanvas,
1518
- weightingFilter= this._weightingFilter,
1658
+ showPeakLine = showPeaks && this._peakLine && _mode == 10,
1659
+ maxBarHeight = _radial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1660
+ dbRange = maxDecibels - minDecibels,
1519
1661
  [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1520
1662
 
1521
- if ( energy.val > 0 )
1522
- this._spinAngle += this._spinSpeed * RPM;
1663
+ if ( _energy.val > 0 )
1664
+ this._spinAngle += this._spinSpeed * TAU / ( 60 * _fps ); // spinSpeed * angle increment per frame for 1 RPM
1523
1665
 
1524
1666
  /* HELPER FUNCTIONS */
1525
1667
 
@@ -1527,8 +1669,8 @@ export default class AudioMotionAnalyzer {
1527
1669
  const doReflex = channel => {
1528
1670
  if ( this._reflexRatio > 0 && ! isLumi ) {
1529
1671
  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;
1672
+ if ( this.reflexFit || isDualVertical ) { // always fit reflex in vertical stereo mode
1673
+ posY = isDualVertical && channel == 0 ? channelHeight + channelGap : 0;
1532
1674
  height = channelHeight - analyzerHeight;
1533
1675
  }
1534
1676
  else {
@@ -1536,34 +1678,34 @@ export default class AudioMotionAnalyzer {
1536
1678
  height = analyzerHeight;
1537
1679
  }
1538
1680
 
1539
- ctx.save();
1681
+ _ctx.save();
1540
1682
 
1541
1683
  // set alpha and brightness for the reflection
1542
- ctx.globalAlpha = this.reflexAlpha;
1684
+ _ctx.globalAlpha = this.reflexAlpha;
1543
1685
  if ( this.reflexBright != 1 )
1544
- ctx.filter = `brightness(${this.reflexBright})`;
1686
+ _ctx.filter = `brightness(${this.reflexBright})`;
1545
1687
 
1546
1688
  // 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 );
1689
+ _ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
1690
+ _ctx.drawImage( canvas, 0, channelCoords[ channel ].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
1549
1691
 
1550
- ctx.restore();
1692
+ _ctx.restore();
1551
1693
  }
1552
1694
  }
1553
1695
 
1554
1696
  // draw scale on X-axis
1555
1697
  const drawScaleX = () => {
1556
1698
  if ( this.showScaleX ) {
1557
- if ( isRadial ) {
1558
- ctx.save();
1559
- ctx.translate( centerX, centerY );
1699
+ if ( _radial ) {
1700
+ _ctx.save();
1701
+ _ctx.translate( centerX, centerY );
1560
1702
  if ( this._spinSpeed )
1561
- ctx.rotate( this._spinAngle + HALF_PI );
1562
- ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1563
- ctx.restore();
1703
+ _ctx.rotate( this._spinAngle + HALF_PI );
1704
+ _ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1705
+ _ctx.restore();
1564
1706
  }
1565
1707
  else
1566
- ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1708
+ _ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1567
1709
  }
1568
1710
  }
1569
1711
 
@@ -1571,16 +1713,16 @@ export default class AudioMotionAnalyzer {
1571
1713
  const drawScaleY = channelTop => {
1572
1714
  const scaleWidth = canvasX.height,
1573
1715
  fontSize = scaleWidth >> 1,
1574
- max = isLinear ? 100 : maxdB,
1575
- min = isLinear ? 0 : mindB,
1576
- incr = isLinear ? 20 : 5,
1716
+ max = _linearAmplitude ? 100 : maxDecibels,
1717
+ min = _linearAmplitude ? 0 : minDecibels,
1718
+ incr = _linearAmplitude ? 20 : 5,
1577
1719
  interval = analyzerHeight / ( max - min );
1578
1720
 
1579
- ctx.save();
1580
- ctx.fillStyle = SCALEY_LABEL_COLOR;
1581
- ctx.font = `${fontSize}px ${FONT_FAMILY}`;
1582
- ctx.textAlign = 'right';
1583
- ctx.lineWidth = 1;
1721
+ _ctx.save();
1722
+ _ctx.fillStyle = SCALEY_LABEL_COLOR;
1723
+ _ctx.font = `${fontSize}px ${FONT_FAMILY}`;
1724
+ _ctx.textAlign = 'right';
1725
+ _ctx.lineWidth = 1;
1584
1726
 
1585
1727
  for ( let val = max; val > min; val -= incr ) {
1586
1728
  const posY = channelTop + ( max - val ) * interval,
@@ -1588,26 +1730,26 @@ export default class AudioMotionAnalyzer {
1588
1730
 
1589
1731
  if ( even ) {
1590
1732
  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;
1733
+ if ( _mirror != -1 )
1734
+ _ctx.fillText( val, scaleWidth * .85, labelY );
1735
+ if ( _mirror != 1 )
1736
+ _ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
1737
+ _ctx.strokeStyle = SCALEY_LABEL_COLOR;
1738
+ _ctx.setLineDash([2,4]);
1739
+ _ctx.lineDashOffset = 0;
1598
1740
  }
1599
1741
  else {
1600
- ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1601
- ctx.setLineDash([2,8]);
1602
- ctx.lineDashOffset = 1;
1742
+ _ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1743
+ _ctx.setLineDash([2,8]);
1744
+ _ctx.lineDashOffset = 1;
1603
1745
  }
1604
1746
 
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();
1747
+ _ctx.beginPath();
1748
+ _ctx.moveTo( initialX + scaleWidth * even * ( _mirror != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
1749
+ _ctx.lineTo( finalX - scaleWidth * even * ( _mirror != 1 ), ~~posY + .5 );
1750
+ _ctx.stroke();
1609
1751
  }
1610
- ctx.restore();
1752
+ _ctx.restore();
1611
1753
  }
1612
1754
 
1613
1755
  // returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
@@ -1620,7 +1762,7 @@ export default class AudioMotionAnalyzer {
1620
1762
  SQ12194 = 148693636,
1621
1763
  linearTodB = value => 20 * Math.log10( value );
1622
1764
 
1623
- switch ( weightingFilter ) {
1765
+ switch ( _weightingFilter ) {
1624
1766
  case FILTER_A : // A-weighting https://en.wikipedia.org/wiki/A-weighting
1625
1767
  const rA = ( SQ12194 * f2 ** 2 ) / ( ( f2 + SQ20_6 ) * Math.sqrt( ( f2 + SQ107_7 ) * ( f2 + SQ737_9 ) ) * ( f2 + SQ12194 ) );
1626
1768
  return 2 + linearTodB( rA );
@@ -1650,19 +1792,19 @@ export default class AudioMotionAnalyzer {
1650
1792
 
1651
1793
  // draws (stroke) a bar from x,y1 to x,y2
1652
1794
  const strokeBar = ( x, y1, y2 ) => {
1653
- ctx.beginPath();
1654
- ctx.moveTo( x, y1 );
1655
- ctx.lineTo( x, y2 );
1656
- ctx.stroke();
1795
+ _ctx.beginPath();
1796
+ _ctx.moveTo( x, y1 );
1797
+ _ctx.lineTo( x, y2 );
1798
+ _ctx.stroke();
1657
1799
  }
1658
1800
 
1659
1801
  // conditionally strokes current path on canvas
1660
1802
  const strokeIf = flag => {
1661
- if ( flag && lineWidth ) {
1662
- const alpha = ctx.globalAlpha;
1663
- ctx.globalAlpha = 1;
1664
- ctx.stroke();
1665
- ctx.globalAlpha = alpha;
1803
+ if ( flag && _lineWidth ) {
1804
+ const alpha = _ctx.globalAlpha;
1805
+ _ctx.globalAlpha = 1;
1806
+ _ctx.stroke();
1807
+ _ctx.globalAlpha = alpha;
1666
1808
  }
1667
1809
  }
1668
1810
 
@@ -1670,7 +1812,7 @@ export default class AudioMotionAnalyzer {
1670
1812
  const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
1671
1813
 
1672
1814
  // converts planar X,Y coordinates to radial coordinates
1673
- const radialXY = ( x, y, dir ) => {
1815
+ const radialXY = ( x, y, dir = 1 ) => {
1674
1816
  const height = radius + y,
1675
1817
  angle = getAngle( x, dir );
1676
1818
  return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
@@ -1678,21 +1820,21 @@ export default class AudioMotionAnalyzer {
1678
1820
 
1679
1821
  // draws a polygon of width `w` and height `h` at (x,y) in radial mode
1680
1822
  const radialPoly = ( x, y, w, h, stroke ) => {
1681
- ctx.beginPath();
1682
- for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
1823
+ _ctx.beginPath();
1824
+ for ( const dir of ( _mirror ? [1,-1] : [1] ) ) {
1683
1825
  const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
1684
- ctx.moveTo( ...radialXY( x, y, dir ) );
1685
- ctx.lineTo( ...radialXY( x, y + h, dir ) );
1826
+ _ctx.moveTo( ...radialXY( x, y, dir ) );
1827
+ _ctx.lineTo( ...radialXY( x, y + h, dir ) );
1686
1828
  if ( isRound )
1687
- ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle );
1829
+ _ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
1688
1830
  else
1689
- ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1690
- ctx.lineTo( ...radialXY( x + w, y, dir ) );
1831
+ _ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1832
+ _ctx.lineTo( ...radialXY( x + w, y, dir ) );
1691
1833
  if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
1692
- ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, true );
1834
+ _ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
1693
1835
  }
1694
1836
  strokeIf( stroke );
1695
- ctx.fill();
1837
+ _ctx.fill();
1696
1838
  }
1697
1839
 
1698
1840
  // converts a value in [0;1] range to a height in pixels that fits into the current LED elements
@@ -1700,35 +1842,36 @@ export default class AudioMotionAnalyzer {
1700
1842
 
1701
1843
  // update energy information
1702
1844
  const updateEnergy = newVal => {
1703
- energy.val = newVal;
1704
- if ( newVal >= energy.peak ) {
1705
- energy.peak = newVal;
1706
- energy.hold = 30;
1845
+ _energy.val = newVal;
1846
+ if ( _energy.peak > 0 ) {
1847
+ _energy.hold--;
1848
+ if ( _energy.hold < 0 )
1849
+ _energy.peak += _energy.hold / ( holdFrames * holdFrames / 2 );
1707
1850
  }
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)
1851
+ if ( newVal >= _energy.peak ) {
1852
+ _energy.peak = newVal;
1853
+ _energy.hold = holdFrames;
1713
1854
  }
1714
1855
  }
1715
1856
 
1716
1857
  // calculate and display (if enabled) the current frame rate
1717
1858
  const updateFPS = () => {
1718
- this._frame++;
1719
- const elapsed = timestamp - this._time;
1859
+ const elapsed = timestamp - this._time; // elapsed time since the last FPS computation
1860
+
1861
+ this._last = timestamp - ( this._maxFPS ? elapsed % ( 1000 / this._maxFPS ) : 0 ); // thanks https://stackoverflow.com/a/19772220/2370385
1862
+ this._frames++;
1720
1863
 
1721
1864
  if ( elapsed >= 1000 ) {
1722
- this._fps = this._frame / ( elapsed / 1000 );
1723
- this._frame = 0;
1865
+ this._fps = this._frames / ( elapsed / 1000 );
1866
+ this._frames = 0;
1724
1867
  this._time = timestamp;
1725
1868
  }
1726
1869
  if ( this.showFPS ) {
1727
1870
  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 );
1871
+ _ctx.font = `bold ${size}px ${FONT_FAMILY}`;
1872
+ _ctx.fillStyle = FPS_COLOR;
1873
+ _ctx.textAlign = 'right';
1874
+ _ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
1732
1875
  }
1733
1876
  }
1734
1877
 
@@ -1736,9 +1879,8 @@ export default class AudioMotionAnalyzer {
1736
1879
 
1737
1880
  let currentEnergy = 0;
1738
1881
 
1739
- const bars = this._bars,
1740
- nBars = bars.length,
1741
- nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
1882
+ const nBars = _bars.length,
1883
+ nChannels = isSingle ? 1 : 2;
1742
1884
 
1743
1885
  for ( let channel = 0; channel < nChannels; channel++ ) {
1744
1886
 
@@ -1746,8 +1888,9 @@ export default class AudioMotionAnalyzer {
1746
1888
  channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
1747
1889
  colorStops = channelGradient.colorStops,
1748
1890
  colorCount = colorStops.length,
1749
- bgColor = ( ! showBgColor || isLeds && ! isOverlay ) ? '#000' : channelGradient.bgColor,
1750
- mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
1891
+ bgColor = ( ! showBgColor || isLeds && ! overlay ) ? '#000' : channelGradient.bgColor,
1892
+ mustClear = channel == 0 || ! _radial && ! isDualCombined,
1893
+ direction = channel && _radial && isDualVertical ? -1 : 1; // for radial dual vertical layout
1751
1894
 
1752
1895
  // helper function for FFT data interpolation (uses fftData)
1753
1896
  const interpolate = ( bin, ratio ) => {
@@ -1759,53 +1902,53 @@ export default class AudioMotionAnalyzer {
1759
1902
  const setBarColor = ( value = 0, barIndex = 0 ) => {
1760
1903
  let color;
1761
1904
  // for mode 10, always use the channel gradient (ignore colorMode)
1762
- if ( ( colorMode == COLOR_GRADIENT && ! isTrueLeds ) || mode == 10 )
1763
- color = canvasGradients[ channel ];
1905
+ if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode == 10 )
1906
+ color = _canvasGradients[ channel ];
1764
1907
  else {
1765
- const selectedIndex = colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
1908
+ const selectedIndex = _colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
1766
1909
  color = colorStops[ selectedIndex ].color;
1767
1910
  }
1768
- ctx.fillStyle = ctx.strokeStyle = color;
1911
+ _ctx.fillStyle = _ctx.strokeStyle = color;
1769
1912
  }
1770
1913
 
1771
1914
  if ( useCanvas ) {
1772
1915
  // clear the channel area, if in overlay mode
1773
1916
  // this is done per channel to clear any residue below 0 off the top channel (especially in line graph mode with lineWidth > 1)
1774
- if ( isOverlay && mustClear )
1775
- ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1917
+ if ( overlay && mustClear )
1918
+ _ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1776
1919
 
1777
1920
  // fill the analyzer background if needed (not overlay or overlay + showBgColor)
1778
- if ( ! isOverlay || showBgColor ) {
1779
- if ( isOverlay )
1780
- ctx.globalAlpha = this.bgAlpha;
1921
+ if ( ! overlay || showBgColor ) {
1922
+ if ( overlay )
1923
+ _ctx.globalAlpha = this.bgAlpha;
1781
1924
 
1782
- ctx.fillStyle = bgColor;
1925
+ _ctx.fillStyle = bgColor;
1783
1926
 
1784
1927
  // exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
1785
1928
  if ( mustClear )
1786
- ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( isOverlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
1929
+ _ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
1787
1930
 
1788
- ctx.globalAlpha = 1;
1931
+ _ctx.globalAlpha = 1;
1789
1932
  }
1790
1933
 
1791
1934
  // 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 ) )
1935
+ if ( this.showScaleY && ! isLumi && ! _radial && ( channel == 0 || ! isDualCombined ) )
1793
1936
  drawScaleY( channelTop );
1794
1937
 
1795
1938
  // set line width and dash for LEDs effect
1796
1939
  if ( isLeds ) {
1797
- ctx.setLineDash( [ ledHeight, ledSpaceV ] );
1798
- ctx.lineWidth = bars[0].width;
1940
+ _ctx.setLineDash( [ ledHeight, ledSpaceV ] );
1941
+ _ctx.lineWidth = _bars[0].width;
1799
1942
  }
1800
1943
  else // for outline effect ensure linewidth is not greater than half the bar width
1801
- ctx.lineWidth = isOutline ? Math.min( lineWidth, bars[0].width / 2 ) : lineWidth;
1944
+ _ctx.lineWidth = isOutline ? Math.min( _lineWidth, _bars[0].width / 2 ) : _lineWidth;
1802
1945
 
1803
1946
  // set clip region
1804
- ctx.save();
1805
- if ( ! isRadial ) {
1947
+ _ctx.save();
1948
+ if ( ! _radial ) {
1806
1949
  const channelRegion = new Path2D();
1807
1950
  channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
1808
- ctx.clip( channelRegion );
1951
+ _ctx.clip( channelRegion );
1809
1952
  }
1810
1953
  } // if ( useCanvas )
1811
1954
 
@@ -1814,11 +1957,11 @@ export default class AudioMotionAnalyzer {
1814
1957
  this._analyzer[ channel ].getFloatFrequencyData( fftData );
1815
1958
 
1816
1959
  // apply weighting
1817
- if ( weightingFilter )
1960
+ if ( _weightingFilter )
1818
1961
  fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
1819
1962
 
1820
1963
  // start drawing path (for mode 10)
1821
- ctx.beginPath();
1964
+ _ctx.beginPath();
1822
1965
 
1823
1966
  // store line graph points to create mirror effect in radial mode
1824
1967
  let points = [];
@@ -1827,7 +1970,7 @@ export default class AudioMotionAnalyzer {
1827
1970
 
1828
1971
  for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
1829
1972
 
1830
- const bar = bars[ barIndex ],
1973
+ const bar = _bars[ barIndex ],
1831
1974
  { posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
1832
1975
 
1833
1976
  let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
@@ -1849,13 +1992,13 @@ export default class AudioMotionAnalyzer {
1849
1992
  bar.hold[ channel ]--;
1850
1993
  // if hold is negative, it becomes the "acceleration" for peak drop
1851
1994
  if ( bar.hold[ channel ] < 0 )
1852
- bar.peak[ channel ] += bar.hold[ channel ] / maxBarHeight;
1995
+ bar.peak[ channel ] += bar.hold[ channel ] / ( holdFrames * holdFrames / 2 );
1853
1996
  }
1854
1997
 
1855
1998
  // check if it's a new peak for this bar
1856
1999
  if ( barValue >= bar.peak[ channel ] ) {
1857
2000
  bar.peak[ channel ] = barValue;
1858
- bar.hold[ channel ] = 30; // set peak hold time to 30 frames (0.5s)
2001
+ bar.hold[ channel ] = holdFrames;
1859
2002
  }
1860
2003
 
1861
2004
  // if not using the canvas, move earlier to the next bar
@@ -1864,71 +2007,67 @@ export default class AudioMotionAnalyzer {
1864
2007
 
1865
2008
  // set opacity for bar effects
1866
2009
  if ( isLumi || isAlpha )
1867
- ctx.globalAlpha = barValue;
2010
+ _ctx.globalAlpha = barValue;
1868
2011
  else if ( isOutline )
1869
- ctx.globalAlpha = fillAlpha;
2012
+ _ctx.globalAlpha = fillAlpha;
1870
2013
 
1871
2014
  // set fillStyle and strokeStyle for the current bar
1872
2015
  setBarColor( barValue, barIndex );
1873
2016
 
1874
2017
  // compute actual bar height on screen
1875
- let barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
1876
-
1877
- // invert bar for radial channel 1
1878
- if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
1879
- barHeight *= -1;
2018
+ const barHeight = ( isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0 ) * direction;
1880
2019
 
1881
2020
  // Draw current bar or line segment
1882
2021
 
1883
- if ( mode == 10 ) {
2022
+ if ( _mode == 10 ) {
1884
2023
  // compute the average between the initial bar (barIndex==0) and the next one
1885
2024
  // used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
1886
- const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
2025
+ const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ _bars[1].binLo ] ) * maxBarHeight * direction + barHeight ) / 2;
1887
2026
 
1888
- if ( isRadial ) {
2027
+ if ( _radial ) {
1889
2028
  if ( barIndex == 0 )
1890
- ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ), 1 ) );
2029
+ _ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ) ) );
1891
2030
  // draw line to the current point, avoiding overlapping wrap-around frequencies
1892
2031
  if ( posX >= 0 ) {
1893
2032
  const point = [ posX, barHeight ];
1894
- ctx.lineTo( ...radialXY( ...point, 1 ) );
2033
+ _ctx.lineTo( ...radialXY( ...point ) );
1895
2034
  points.push( point );
1896
2035
  }
1897
2036
  }
1898
2037
  else { // Linear
1899
2038
  if ( barIndex == 0 ) {
1900
2039
  // start the line off-screen using the previous FFT bin value as the initial amplitude
1901
- if ( mirrorMode != -1 ) {
2040
+ if ( _mirror != -1 ) {
1902
2041
  const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
1903
- ctx.moveTo( initialX - lineWidth, analyzerBottom - prevFFTData );
2042
+ _ctx.moveTo( initialX - _lineWidth, analyzerBottom - prevFFTData );
1904
2043
  }
1905
2044
  else
1906
- ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
2045
+ _ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
1907
2046
  }
1908
2047
  // draw line to the current point
1909
2048
  // avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
1910
- if ( mirrorMode != -1 || posX >= initialX )
1911
- ctx.lineTo( posX, analyzerBottom - barHeight );
2049
+ if ( _mirror != -1 || posX >= initialX )
2050
+ _ctx.lineTo( posX, analyzerBottom - barHeight );
1912
2051
  }
1913
2052
  }
1914
2053
  else {
1915
2054
  if ( isLeds ) {
1916
2055
  // draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
1917
- if ( showBgColor && ! isOverlay && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) ) {
1918
- const alpha = ctx.globalAlpha;
1919
- ctx.strokeStyle = LEDS_UNLIT_COLOR;
1920
- ctx.globalAlpha = 1;
2056
+ if ( showBgColor && ! overlay && ( channel == 0 || ! isDualCombined ) ) {
2057
+ const alpha = _ctx.globalAlpha;
2058
+ _ctx.strokeStyle = LEDS_UNLIT_COLOR;
2059
+ _ctx.globalAlpha = 1;
1921
2060
  strokeBar( barCenter, channelTop, analyzerBottom );
1922
2061
  // restore properties
1923
- ctx.strokeStyle = ctx.fillStyle;
1924
- ctx.globalAlpha = alpha;
2062
+ _ctx.strokeStyle = _ctx.fillStyle;
2063
+ _ctx.globalAlpha = alpha;
1925
2064
  }
1926
2065
  if ( isTrueLeds ) {
1927
2066
  // ledPosY() is used below to fit one entire led height into the selected range
1928
2067
  const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
1929
2068
  let last = analyzerBottom;
1930
2069
  for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
1931
- ctx.strokeStyle = colorStops[ i ].color;
2070
+ _ctx.strokeStyle = colorStops[ i ].color;
1932
2071
  let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
1933
2072
  strokeBar( barCenter, last, y );
1934
2073
  last = y - ledSpaceV;
@@ -1938,53 +2077,53 @@ export default class AudioMotionAnalyzer {
1938
2077
  strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
1939
2078
  }
1940
2079
  else if ( posX >= initialX ) {
1941
- if ( isRadial )
2080
+ if ( _radial )
1942
2081
  radialPoly( posX, 0, width, barHeight, isOutline );
1943
2082
  else if ( isRound ) {
1944
2083
  const halfWidth = width / 2,
1945
2084
  y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
1946
2085
 
1947
- ctx.beginPath();
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 );
2086
+ _ctx.beginPath();
2087
+ _ctx.moveTo( posX, y );
2088
+ _ctx.lineTo( posX, y - barHeight );
2089
+ _ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
2090
+ _ctx.lineTo( posX + width, y );
1952
2091
  strokeIf( isOutline );
1953
- ctx.fill();
2092
+ _ctx.fill();
1954
2093
  }
1955
2094
  else {
1956
- const offset = isOutline ? ctx.lineWidth : 0;
1957
- ctx.beginPath();
1958
- ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
2095
+ const offset = isOutline ? _ctx.lineWidth : 0;
2096
+ _ctx.beginPath();
2097
+ _ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
1959
2098
  strokeIf( isOutline );
1960
- ctx.fill();
2099
+ _ctx.fill();
1961
2100
  }
1962
2101
  }
1963
2102
  }
1964
2103
 
1965
2104
  // Draw peak
1966
2105
  const peak = bar.peak[ channel ];
1967
- if ( peak > 0 && this.showPeaks && ! isLumi && posX >= initialX && posX < finalX ) {
2106
+ if ( peak > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
1968
2107
  // set opacity
1969
- if ( isOutline && lineWidth > 0 )
1970
- ctx.globalAlpha = 1;
2108
+ if ( isOutline && _lineWidth > 0 )
2109
+ _ctx.globalAlpha = 1;
1971
2110
  else if ( isAlpha )
1972
- ctx.globalAlpha = peak;
2111
+ _ctx.globalAlpha = peak;
1973
2112
 
1974
2113
  // select the peak color for 'bar-level' colorMode or 'trueLeds'
1975
- if ( colorMode == COLOR_BAR_LEVEL || isTrueLeds )
2114
+ if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
1976
2115
  setBarColor( peak );
1977
2116
 
1978
2117
  // render peak according to current mode / effect
1979
2118
  if ( isLeds ) {
1980
2119
  const ledPeak = ledPosY( peak );
1981
2120
  if ( ledPeak >= ledSpaceV ) // avoid peak below first led
1982
- ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
2121
+ _ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
1983
2122
  }
1984
- else if ( ! isRadial )
1985
- ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
1986
- else if ( mode != 10 ) // radial - no peaks for mode 10
1987
- radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), width, -2 );
2123
+ else if ( ! _radial )
2124
+ _ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
2125
+ else if ( _mode != 10 ) // radial - peaks for mode 10 are done by the peak line code
2126
+ radialPoly( posX, peak * maxBarHeight * direction, width, -2 );
1988
2127
  }
1989
2128
 
1990
2129
  } // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
@@ -1993,41 +2132,72 @@ export default class AudioMotionAnalyzer {
1993
2132
  if ( ! useCanvas )
1994
2133
  continue;
1995
2134
 
1996
- ctx.restore(); // restore clip region
2135
+ _ctx.restore(); // restore clip region
1997
2136
 
1998
2137
  // restore global alpha
1999
- ctx.globalAlpha = 1;
2138
+ _ctx.globalAlpha = 1;
2000
2139
 
2001
2140
  // Fill/stroke drawing path for mode 10
2002
- if ( mode == 10 ) {
2141
+ if ( _mode == 10 ) {
2003
2142
  setBarColor(); // select channel gradient
2004
2143
 
2005
- if ( isRadial ) {
2006
- if ( mirrorMode ) {
2144
+ if ( _radial ) {
2145
+ if ( _mirror ) {
2007
2146
  let p;
2008
2147
  while ( p = points.pop() )
2009
- ctx.lineTo( ...radialXY( ...p, -1 ) );
2148
+ _ctx.lineTo( ...radialXY( ...p, -1 ) );
2010
2149
  }
2011
- ctx.closePath();
2150
+ _ctx.closePath();
2012
2151
  }
2013
2152
 
2014
- if ( lineWidth > 0 )
2015
- ctx.stroke();
2153
+ if ( _lineWidth > 0 )
2154
+ _ctx.stroke();
2016
2155
 
2017
2156
  if ( fillAlpha > 0 ) {
2018
- if ( isRadial ) {
2157
+ if ( _radial ) {
2019
2158
  // exclude the center circle from the fill area
2020
- ctx.moveTo( centerX + radius, centerY );
2021
- ctx.arc( centerX, centerY, radius, 0, TAU, true );
2159
+ _ctx.moveTo( centerX + radius, centerY );
2160
+ _ctx.arc( centerX, centerY, radius, 0, TAU, true );
2022
2161
  }
2023
2162
  else { // close the fill area
2024
- ctx.lineTo( finalX, analyzerBottom );
2025
- ctx.lineTo( initialX, analyzerBottom );
2163
+ _ctx.lineTo( finalX, analyzerBottom );
2164
+ _ctx.lineTo( initialX, analyzerBottom );
2026
2165
  }
2027
2166
 
2028
- ctx.globalAlpha = fillAlpha;
2029
- ctx.fill();
2030
- ctx.globalAlpha = 1;
2167
+ _ctx.globalAlpha = fillAlpha;
2168
+ _ctx.fill();
2169
+ _ctx.globalAlpha = 1;
2170
+ }
2171
+
2172
+ // draw peak line (and standard peaks on radial)
2173
+ if ( showPeakLine || ( _radial && showPeaks ) ) {
2174
+ points = []; // for mirror line on radial
2175
+ _ctx.beginPath();
2176
+ _bars.forEach( ( b, i ) => {
2177
+ let x = b.posX,
2178
+ h = b.peak[ channel ],
2179
+ m = i ? 'lineTo' : 'moveTo';
2180
+ if ( _radial && x < 0 ) {
2181
+ const nextBar = _bars[ i + 1 ];
2182
+ h = findY( x, h, nextBar.posX, nextBar.peak[ channel ], 0 );
2183
+ x = 0;
2184
+ }
2185
+ h *= maxBarHeight * direction;
2186
+ if ( showPeakLine ) {
2187
+ _ctx[ m ]( ...( _radial ? radialXY( x, h ) : [ x, analyzerBottom - h ] ) );
2188
+ if ( _radial && _mirror )
2189
+ points.push( [ x, h ] );
2190
+ }
2191
+ else if ( h )
2192
+ radialPoly( x, h, 1, -2 * direction ); // standard peaks (also does mirror)
2193
+ });
2194
+ if ( showPeakLine ) {
2195
+ let p;
2196
+ while ( p = points.pop() )
2197
+ _ctx.lineTo( ...radialXY( ...p, -1 ) ); // mirror line points
2198
+ _ctx.lineWidth = 1;
2199
+ _ctx.stroke(); // stroke peak line
2200
+ }
2031
2201
  }
2032
2202
  }
2033
2203
 
@@ -2040,14 +2210,14 @@ export default class AudioMotionAnalyzer {
2040
2210
 
2041
2211
  if ( useCanvas ) {
2042
2212
  // Mirror effect
2043
- if ( mirrorMode && ! isRadial ) {
2044
- ctx.setTransform( -1, 0, 0, 1, canvas.width - initialX, 0 );
2045
- ctx.drawImage( canvas, initialX, 0, centerX, canvas.height, 0, 0, centerX, canvas.height );
2046
- ctx.setTransform( 1, 0, 0, 1, 0, 0 );
2213
+ if ( _mirror && ! _radial ) {
2214
+ _ctx.setTransform( -1, 0, 0, 1, canvas.width - initialX, 0 );
2215
+ _ctx.drawImage( canvas, initialX, 0, centerX, canvas.height, 0, 0, centerX, canvas.height );
2216
+ _ctx.setTransform( 1, 0, 0, 1, 0, 0 );
2047
2217
  }
2048
2218
 
2049
2219
  // restore solid lines
2050
- ctx.setLineDash([]);
2220
+ _ctx.setLineDash([]);
2051
2221
 
2052
2222
  // draw frequency scale (X-axis)
2053
2223
  drawScaleX();
@@ -2058,14 +2228,11 @@ export default class AudioMotionAnalyzer {
2058
2228
 
2059
2229
  // call callback function, if defined
2060
2230
  if ( this.onCanvasDraw ) {
2061
- ctx.save();
2062
- ctx.fillStyle = ctx.strokeStyle = canvasGradients[0];
2063
- this.onCanvasDraw( this, { timestamp, canvasGradients } );
2064
- ctx.restore();
2231
+ _ctx.save();
2232
+ _ctx.fillStyle = _ctx.strokeStyle = _canvasGradients[0];
2233
+ this.onCanvasDraw( this, { timestamp, _canvasGradients } );
2234
+ _ctx.restore();
2065
2235
  }
2066
-
2067
- // schedule next canvas update
2068
- this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
2069
2236
  }
2070
2237
 
2071
2238
  /**
@@ -2098,24 +2265,19 @@ export default class AudioMotionAnalyzer {
2098
2265
  * Generate currently selected gradient
2099
2266
  */
2100
2267
  _makeGrad() {
2101
-
2102
2268
  if ( ! this._ready )
2103
2269
  return;
2104
2270
 
2105
- const ctx = this._canvasCtx,
2106
- canvas = ctx.canvas,
2107
- channelLayout = this._chLayout,
2271
+ const { canvas, _ctx, _radial, _reflexRatio } = this,
2272
+ { analyzerWidth, initialX, radius } = this._aux,
2108
2273
  { isLumi } = this._flg,
2109
- isRadial = this._radial,
2110
- gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
2274
+ isDualVertical = this._chLayout == CHANNEL_VERTICAL,
2275
+ analyzerRatio = 1 - _reflexRatio,
2276
+ centerX = canvas.width >> 1,
2277
+ centerY = canvas.height >> 1,
2278
+ maxRadius = Math.min( centerX, centerY ),
2279
+ gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - _reflexRatio * ( ! isDualVertical ) ) | 0;
2111
2280
  // for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
2112
- analyzerRatio = 1 - this._reflexRatio,
2113
- { analyzerWidth, initialX, radius } = this._aux;
2114
-
2115
- // for radial mode
2116
- const centerX = canvas.width >> 1,
2117
- centerY = canvas.height >> 1,
2118
- maxRadius = Math.min( centerX, centerY );
2119
2281
 
2120
2282
  for ( const channel of [0,1] ) {
2121
2283
  const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
@@ -2124,13 +2286,13 @@ export default class AudioMotionAnalyzer {
2124
2286
 
2125
2287
  let grad;
2126
2288
 
2127
- if ( isRadial )
2128
- grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( channelLayout == CHANNEL_VERTICAL ) );
2289
+ if ( _radial )
2290
+ grad = _ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * isDualVertical );
2129
2291
  else
2130
- grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
2292
+ grad = _ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
2131
2293
 
2132
2294
  if ( colorStops ) {
2133
- const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
2295
+ const dual = isDualVertical && ! this._splitGradient && ( ! isHorizontal || _radial );
2134
2296
 
2135
2297
  for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
2136
2298
  const maxIndex = colorStops.length - 1;
@@ -2143,17 +2305,17 @@ export default class AudioMotionAnalyzer {
2143
2305
  offset /= 2;
2144
2306
 
2145
2307
  // constrain the offset within the useful analyzer areas (avoid reflex areas)
2146
- if ( channelLayout == CHANNEL_VERTICAL && ! isLumi && ! isRadial && ! isHorizontal ) {
2308
+ if ( isDualVertical && ! isLumi && ! _radial && ! isHorizontal ) {
2147
2309
  offset *= analyzerRatio;
2148
2310
  // skip the first reflex area in split mode
2149
2311
  if ( ! dual && offset > .5 * analyzerRatio )
2150
- offset += .5 * this._reflexRatio;
2312
+ offset += .5 * _reflexRatio;
2151
2313
  }
2152
2314
 
2153
2315
  // only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
2154
2316
  if ( channelArea == 1 ) {
2155
2317
  // add colors in reverse order if radial or lumi are active
2156
- if ( isRadial || isLumi ) {
2318
+ if ( _radial || isLumi ) {
2157
2319
  const revIndex = maxIndex - index;
2158
2320
  colorStop = colorStops[ revIndex ];
2159
2321
  offset = 1 - colorStop.pos / 2;
@@ -2171,7 +2333,7 @@ export default class AudioMotionAnalyzer {
2171
2333
  grad.addColorStop( offset, colorStop.color );
2172
2334
 
2173
2335
  // create additional color stop at the end of first channel to prevent bleeding
2174
- if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
2336
+ if ( isDualVertical && index == maxIndex && offset < .5 )
2175
2337
  grad.addColorStop( .5, colorStop.color );
2176
2338
  });
2177
2339
  } // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
@@ -2206,12 +2368,10 @@ export default class AudioMotionAnalyzer {
2206
2368
  * Internal function to change canvas dimensions on demand
2207
2369
  */
2208
2370
  _setCanvas( reason ) {
2209
- // if initialization is not finished, quit
2210
2371
  if ( ! this._ready )
2211
2372
  return;
2212
2373
 
2213
- const ctx = this._canvasCtx,
2214
- canvas = ctx.canvas,
2374
+ const { canvas, _ctx } = this,
2215
2375
  canvasX = this._scaleX.canvas,
2216
2376
  pixelRatio = window.devicePixelRatio / ( this._loRes + 1 );
2217
2377
 
@@ -2242,12 +2402,12 @@ export default class AudioMotionAnalyzer {
2242
2402
 
2243
2403
  // if not in overlay mode, paint the canvas black
2244
2404
  if ( ! this.overlay ) {
2245
- ctx.fillStyle = '#000';
2246
- ctx.fillRect( 0, 0, newWidth, newHeight );
2405
+ _ctx.fillStyle = '#000';
2406
+ _ctx.fillRect( 0, 0, newWidth, newHeight );
2247
2407
  }
2248
2408
 
2249
2409
  // set lineJoin property for area fill mode (this is reset whenever the canvas size changes)
2250
- ctx.lineJoin = 'bevel';
2410
+ _ctx.lineJoin = 'bevel';
2251
2411
 
2252
2412
  // update dimensions of the scale canvas
2253
2413
  canvasX.width = newWidth;
@@ -2312,6 +2472,7 @@ export default class AudioMotionAnalyzer {
2312
2472
  loRes : false,
2313
2473
  lumiBars : false,
2314
2474
  maxDecibels : -25,
2475
+ maxFPS : 0,
2315
2476
  maxFreq : 22000,
2316
2477
  minDecibels : -85,
2317
2478
  minFreq : 20,
@@ -2320,6 +2481,7 @@ export default class AudioMotionAnalyzer {
2320
2481
  noteLabels : false,
2321
2482
  outlineBars : false,
2322
2483
  overlay : false,
2484
+ peakLine : false,
2323
2485
  radial : false,
2324
2486
  reflexAlpha : 0.15,
2325
2487
  reflexBright : 1,