audiomotion-analyzer 4.1.1 → 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.1
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.1';
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,9 @@ 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
+
105
111
  // Polyfill for Array.findLastIndex()
106
112
  if ( ! Array.prototype.findLastIndex ) {
107
113
  Array.prototype.findLastIndex = function( callback ) {
@@ -130,11 +136,18 @@ export default class AudioMotionAnalyzer {
130
136
  this._ready = false;
131
137
 
132
138
  // Initialize internal objects
133
- this._aux = {};
134
- 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;
135
145
  this._gradients = {}; // registered gradients
146
+ this._last = 0; // timestamp of last rendered frame
147
+ this._outNodes = []; // output nodes
148
+ this._ownContext = false;
136
149
  this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
137
- this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
150
+ this._sources = []; // input nodes
138
151
 
139
152
  // Register built-in gradients
140
153
  for ( const [ name, options ] of GRADIENTS )
@@ -160,6 +173,7 @@ export default class AudioMotionAnalyzer {
160
173
  else {
161
174
  try {
162
175
  audioCtx = new ( window.AudioContext || window.webkitAudioContext )();
176
+ this._ownContext = true;
163
177
  }
164
178
  catch( err ) {
165
179
  throw new AudioMotionError( ERR_AUDIO_CONTEXT_FAIL );
@@ -174,13 +188,13 @@ export default class AudioMotionAnalyzer {
174
188
  Connection routing:
175
189
  ===================
176
190
 
177
- for dual channel modes: +---> analyzer[0] ---+
191
+ for dual channel layouts: +---> analyzer[0] ---+
178
192
  | |
179
193
  (source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
180
194
  | |
181
195
  +---> analyzer[1] ---+
182
196
 
183
- for single channel mode:
197
+ for single channel layout:
184
198
 
185
199
  (source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
186
200
 
@@ -193,8 +207,7 @@ export default class AudioMotionAnalyzer {
193
207
  this._input = audioCtx.createGain();
194
208
  this._output = audioCtx.createGain();
195
209
 
196
- // initialize sources array and connect audio source if provided in the options
197
- this._sources = [];
210
+ // connect audio source if provided in the options
198
211
  if ( options.source )
199
212
  this.connectInput( options.source );
200
213
 
@@ -206,17 +219,13 @@ export default class AudioMotionAnalyzer {
206
219
  merger.connect( this._output );
207
220
 
208
221
  // connect output -> destination (speakers)
209
- this._outNodes = [];
210
222
  if ( options.connectSpeakers !== false )
211
223
  this.connectOutput();
212
224
 
213
- // initialize object to save energy
214
- this._energy = { val: 0, peak: 0, hold: 0 };
215
-
216
225
  // create analyzer canvas
217
226
  const canvas = document.createElement('canvas');
218
227
  canvas.style = 'max-width: 100%;';
219
- this._canvasCtx = canvas.getContext('2d');
228
+ this._ctx = canvas.getContext('2d');
220
229
 
221
230
  // create auxiliary canvases for the X-axis and radial scale labels
222
231
  for ( const ctx of [ '_scaleX', '_scaleR' ] )
@@ -243,21 +252,25 @@ export default class AudioMotionAnalyzer {
243
252
  this._setCanvas( REASON_RESIZE );
244
253
  this._fsTimeout = 0;
245
254
  }
246
- }, 60 );
255
+ }, DEBOUNCE_TIMEOUT );
247
256
  }
248
257
  }
249
258
 
250
259
  // if browser supports ResizeObserver, listen for resize on the container
251
260
  if ( window.ResizeObserver ) {
252
- const resizeObserver = new ResizeObserver( onResize );
253
- resizeObserver.observe( this._container );
261
+ this._observer = new ResizeObserver( onResize );
262
+ this._observer.observe( this._container );
254
263
  }
255
264
 
265
+ // create an AbortController to remove event listeners on destroy()
266
+ this._controller = new AbortController();
267
+ const signal = this._controller.signal;
268
+
256
269
  // listen for resize events on the window - required for fullscreen on iPadOS
257
- window.addEventListener( 'resize', onResize );
270
+ window.addEventListener( EVENT_RESIZE, onResize, { signal } );
258
271
 
259
272
  // listen for fullscreenchange events on the canvas - not available on Safari
260
- canvas.addEventListener( 'fullscreenchange', () => {
273
+ canvas.addEventListener( EVENT_FULLSCREENCHANGE, () => {
261
274
  // set flag to indicate a fullscreen change in progress
262
275
  this._fsChanging = true;
263
276
 
@@ -272,16 +285,24 @@ export default class AudioMotionAnalyzer {
272
285
  this._fsTimeout = window.setTimeout( () => {
273
286
  this._fsChanging = false;
274
287
  this._fsTimeout = 0;
275
- }, 60 );
276
- });
288
+ }, DEBOUNCE_TIMEOUT );
289
+ }, { signal } );
277
290
 
278
291
  // Resume audio context if in suspended state (browsers' autoplay policy)
279
292
  const unlockContext = () => {
280
293
  if ( audioCtx.state == 'suspended' )
281
294
  audioCtx.resume();
282
- window.removeEventListener( 'click', unlockContext );
295
+ window.removeEventListener( EVENT_CLICK, unlockContext );
283
296
  }
284
- 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 } );
285
306
 
286
307
  // Set configuration options and use defaults for any missing properties
287
308
  this._setProps( options, true );
@@ -453,6 +474,13 @@ export default class AudioMotionAnalyzer {
453
474
  this._analyzer[ i ].maxDecibels = value;
454
475
  }
455
476
 
477
+ get maxFPS() {
478
+ return this._maxFPS;
479
+ }
480
+ set maxFPS( value ) {
481
+ this._maxFPS = value < 0 ? 0 : +value || 0;
482
+ }
483
+
456
484
  get maxFreq() {
457
485
  return this._maxFreq;
458
486
  }
@@ -524,6 +552,13 @@ export default class AudioMotionAnalyzer {
524
552
  this._calcBars();
525
553
  }
526
554
 
555
+ get peakLine() {
556
+ return this._peakLine;
557
+ }
558
+ set peakLine( value ) {
559
+ this._peakLine = !! value;
560
+ }
561
+
527
562
  get radial() {
528
563
  return this._radial;
529
564
  }
@@ -625,10 +660,10 @@ export default class AudioMotionAnalyzer {
625
660
  return this._input.context;
626
661
  }
627
662
  get canvas() {
628
- return this._canvasCtx.canvas;
663
+ return this._ctx.canvas;
629
664
  }
630
665
  get canvasCtx() {
631
- return this._canvasCtx;
666
+ return this._ctx;
632
667
  }
633
668
  get connectedSources() {
634
669
  return this._sources;
@@ -651,8 +686,11 @@ export default class AudioMotionAnalyzer {
651
686
  get isBandsMode() {
652
687
  return this._flg.isBands;
653
688
  }
689
+ get isDestroyed() {
690
+ return this._destroyed;
691
+ }
654
692
  get isFullscreen() {
655
- return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
693
+ return this._fsEl && ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
656
694
  }
657
695
  get isLedBars() {
658
696
  return this._flg.isLeds;
@@ -664,7 +702,7 @@ export default class AudioMotionAnalyzer {
664
702
  return this._flg.isOctaves;
665
703
  }
666
704
  get isOn() {
667
- return this._runId !== undefined;
705
+ return !! this._runId;
668
706
  }
669
707
  get isOutlineBars() {
670
708
  return this._flg.isOutline;
@@ -729,12 +767,54 @@ export default class AudioMotionAnalyzer {
729
767
  }
730
768
  }
731
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
+
732
811
  /**
733
812
  * Disconnects audio sources from the analyzer
734
813
  *
735
- * @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)
736
816
  */
737
- disconnectInput( sources ) {
817
+ disconnectInput( sources, stopTracks ) {
738
818
  if ( ! sources )
739
819
  sources = Array.from( this._sources );
740
820
  else if ( ! Array.isArray( sources ) )
@@ -742,6 +822,11 @@ export default class AudioMotionAnalyzer {
742
822
 
743
823
  for ( const node of sources ) {
744
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
+ }
745
830
  if ( idx >= 0 ) {
746
831
  node.disconnect( this._input );
747
832
  this._sources.splice( idx, 1 );
@@ -939,26 +1024,42 @@ export default class AudioMotionAnalyzer {
939
1024
  }
940
1025
  }
941
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
+
942
1041
  /**
943
1042
  * Start / stop canvas animation
944
1043
  *
945
- * @param {boolean} [value] if undefined, inverts the current status
946
- * @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
947
1046
  */
948
- toggleAnalyzer( value ) {
949
- const started = this.isOn;
1047
+ toggleAnalyzer( force ) {
1048
+ const hasStarted = this.isOn;
950
1049
 
951
- if ( value === undefined )
952
- value = ! started;
1050
+ if ( force === undefined )
1051
+ force = ! hasStarted;
953
1052
 
954
- if ( started && ! value ) {
1053
+ // Stop the analyzer if it was already running and must be disabled
1054
+ if ( hasStarted && ! force ) {
955
1055
  cancelAnimationFrame( this._runId );
956
- this._runId = undefined;
1056
+ this._runId = 0;
957
1057
  }
958
- else if ( ! started && value ) {
959
- 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;
960
1061
  this._time = performance.now();
961
- this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
1062
+ this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) ); // arrow function preserves the scope of *this*
962
1063
  }
963
1064
 
964
1065
  return this.isOn;
@@ -976,6 +1077,8 @@ export default class AudioMotionAnalyzer {
976
1077
  }
977
1078
  else {
978
1079
  const fsEl = this._fsEl;
1080
+ if ( ! fsEl )
1081
+ return;
979
1082
  if ( fsEl.requestFullscreen )
980
1083
  fsEl.requestFullscreen();
981
1084
  else if ( fsEl.webkitRequestFullscreen )
@@ -1004,8 +1107,10 @@ export default class AudioMotionAnalyzer {
1004
1107
  _calcBars() {
1005
1108
  const bars = this._bars = []; // initialize object property
1006
1109
 
1007
- 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 };
1008
1112
  return;
1113
+ }
1009
1114
 
1010
1115
  const barSpace = this._barSpace,
1011
1116
  canvas = this.canvas,
@@ -1369,28 +1474,22 @@ export default class AudioMotionAnalyzer {
1369
1474
  return;
1370
1475
 
1371
1476
  const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
1372
- canvas = this._canvasCtx.canvas,
1373
- scaleX = this._scaleX,
1374
- scaleR = this._scaleR,
1375
- canvasX = scaleX.canvas,
1376
- canvasR = scaleR.canvas,
1477
+ { canvas, _frequencyScale, _mirror, _noteLabels, _radial, _scaleX, _scaleR } = this,
1478
+ canvasX = _scaleX.canvas,
1479
+ canvasR = _scaleR.canvas,
1377
1480
  freqLabels = [],
1378
- frequencyScale= this._frequencyScale,
1379
- isNoteLabels = this._noteLabels,
1380
- isRadial = this._radial,
1381
1481
  isVertical = this._chLayout == CHANNEL_VERTICAL,
1382
- mirror = this._mirror,
1383
1482
  scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
1384
1483
  scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
1385
1484
  fontSizeX = canvasX.height >> 1,
1386
1485
  fontSizeR = scaleHeight >> 1,
1387
- labelWidthX = fontSizeX * ( isNoteLabels ? .7 : 1.5 ),
1388
- labelWidthR = fontSizeR * ( isNoteLabels ? 1 : 2 ),
1486
+ labelWidthX = fontSizeX * ( _noteLabels ? .7 : 1.5 ),
1487
+ labelWidthR = fontSizeR * ( _noteLabels ? 1 : 2 ),
1389
1488
  root12 = 2 ** ( 1 / 12 );
1390
1489
 
1391
- if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
1490
+ if ( ! _noteLabels && ( this._ansiBands || _frequencyScale != SCALE_LOG ) ) {
1392
1491
  freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
1393
- if ( frequencyScale == SCALE_LINEAR )
1492
+ if ( _frequencyScale == SCALE_LINEAR )
1394
1493
  freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
1395
1494
  else
1396
1495
  freqLabels.push( 8e3, 16e3 );
@@ -1402,8 +1501,8 @@ export default class AudioMotionAnalyzer {
1402
1501
  if ( freq >= this._minFreq && freq <= this._maxFreq ) {
1403
1502
  const pitch = scale[ note ],
1404
1503
  isC = pitch == 'C';
1405
- if ( ( pitch && isNoteLabels && ! mirror ) || isC )
1406
- freqLabels.push( isNoteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
1504
+ if ( ( pitch && _noteLabels && ! _mirror ) || isC )
1505
+ freqLabels.push( _noteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
1407
1506
  }
1408
1507
  freq *= root12;
1409
1508
  }
@@ -1423,27 +1522,27 @@ export default class AudioMotionAnalyzer {
1423
1522
  posX = radialY * Math.cos( adjAng ),
1424
1523
  posY = radialY * Math.sin( adjAng );
1425
1524
 
1426
- scaleR.save();
1427
- scaleR.translate( centerR + posX, centerR + posY );
1428
- scaleR.rotate( angle );
1429
- scaleR.fillText( label, 0, 0 );
1430
- 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();
1431
1530
  }
1432
1531
 
1433
1532
  // clear scale canvas
1434
1533
  canvasX.width |= 0;
1435
1534
 
1436
- scaleX.fillStyle = scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
1437
- 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 );
1438
1537
 
1439
- scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
1440
- scaleR.lineWidth = scaleHeight;
1441
- scaleR.stroke();
1538
+ _scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
1539
+ _scaleR.lineWidth = scaleHeight;
1540
+ _scaleR.stroke();
1442
1541
 
1443
- scaleX.fillStyle = scaleR.fillStyle = SCALEX_LABEL_COLOR;
1444
- scaleX.font = `${ fontSizeX }px ${FONT_FAMILY}`;
1445
- scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
1446
- 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';
1447
1546
 
1448
1547
  let prevX = -labelWidthX / 4,
1449
1548
  prevR = -labelWidthR;
@@ -1453,22 +1552,26 @@ export default class AudioMotionAnalyzer {
1453
1552
  x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
1454
1553
  y = canvasX.height * .75,
1455
1554
  isC = label[0] == 'C',
1456
- maxW = fontSizeX * ( isNoteLabels && ! mirror ? ( isC ? 1.2 : .6 ) : 3 );
1555
+ maxW = fontSizeX * ( _noteLabels && ! _mirror ? ( isC ? 1.2 : .6 ) : 3 );
1457
1556
 
1458
1557
  // set label color - no highlight when mirror effect is active (only Cs displayed)
1459
- 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;
1460
1559
 
1461
1560
  // prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
1462
- if ( isNoteLabels ) {
1561
+ if ( _noteLabels ) {
1562
+ const isLog = _frequencyScale == SCALE_LOG,
1563
+ isLinear = _frequencyScale == SCALE_LINEAR;
1564
+
1463
1565
  let allowedLabels = ['C'];
1464
- if ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 250 ) ||
1465
- ( ( ! 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 ) ) )
1466
1569
  allowedLabels.push('G');
1467
- if ( frequencyScale == SCALE_LOG || freq > 4e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ||
1468
- ( ( ! 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 ) ) )
1469
1572
  allowedLabels.push('E');
1470
- if ( frequencyScale == SCALE_LINEAR && freq > 4e3 ||
1471
- ( ( ! 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 ) ) ) )
1472
1575
  allowedLabels.push('D','F','A','B');
1473
1576
  if ( ! allowedLabels.includes( label[0] ) )
1474
1577
  continue; // skip this label
@@ -1476,16 +1579,16 @@ export default class AudioMotionAnalyzer {
1476
1579
 
1477
1580
  // linear scale
1478
1581
  if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
1479
- scaleX.fillText( label, initialX + x, y, maxW );
1480
- if ( mirror && ( x > labelWidthX || mirror == 1 ) )
1481
- scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
1482
- 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;
1483
1586
  }
1484
1587
 
1485
1588
  // radial scale
1486
1589
  if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
1487
1590
  radialLabel( x, label );
1488
- 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
1489
1592
  radialLabel( -x, label );
1490
1593
  prevR = x;
1491
1594
  }
@@ -1497,41 +1600,68 @@ export default class AudioMotionAnalyzer {
1497
1600
  * this is called 60 times per second by requestAnimationFrame()
1498
1601
  */
1499
1602
  _draw( timestamp ) {
1500
- const { isAlpha, isBands, isLeds, isLumi,
1501
- isOctaves, isOutline, isRound, noLedGap } = this._flg,
1502
- ctx = this._canvasCtx,
1503
- 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
+
1504
1647
  canvasX = this._scaleX.canvas,
1505
1648
  canvasR = this._scaleR.canvas,
1506
- canvasGradients= this._canvasGradients,
1507
1649
  centerX = canvas.width >> 1,
1508
1650
  centerY = canvas.height >> 1,
1509
- colorMode = this._colorMode,
1510
- energy = this._energy,
1511
- fillAlpha = this.fillAlpha,
1512
- mode = this._mode,
1513
- isLinear = this._linearAmplitude,
1514
- isOverlay = this.overlay,
1515
- isRadial = this._radial,
1516
- isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
1517
- channelLayout = this._chLayout,
1518
- lineWidth = this._lineWidth,
1519
- mirrorMode = this._mirror,
1520
- { analyzerHeight, channelCoords,
1521
- channelHeight, channelGap, initialX, radius } = this._aux,
1522
- 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,
1523
1657
  finalX = initialX + analyzerWidth,
1524
- showBgColor = this.showBgColor,
1525
- maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1526
- maxdB = this.maxDecibels,
1527
- mindB = this.minDecibels,
1528
- dbRange = maxdB - mindB,
1529
- useCanvas = this.useCanvas,
1530
- weightingFilter= this._weightingFilter,
1658
+ showPeakLine = showPeaks && this._peakLine && _mode == 10,
1659
+ maxBarHeight = _radial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1660
+ dbRange = maxDecibels - minDecibels,
1531
1661
  [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1532
1662
 
1533
- if ( energy.val > 0 )
1534
- 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
1535
1665
 
1536
1666
  /* HELPER FUNCTIONS */
1537
1667
 
@@ -1539,8 +1669,8 @@ export default class AudioMotionAnalyzer {
1539
1669
  const doReflex = channel => {
1540
1670
  if ( this._reflexRatio > 0 && ! isLumi ) {
1541
1671
  let posY, height;
1542
- if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
1543
- 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;
1544
1674
  height = channelHeight - analyzerHeight;
1545
1675
  }
1546
1676
  else {
@@ -1548,34 +1678,34 @@ export default class AudioMotionAnalyzer {
1548
1678
  height = analyzerHeight;
1549
1679
  }
1550
1680
 
1551
- ctx.save();
1681
+ _ctx.save();
1552
1682
 
1553
1683
  // set alpha and brightness for the reflection
1554
- ctx.globalAlpha = this.reflexAlpha;
1684
+ _ctx.globalAlpha = this.reflexAlpha;
1555
1685
  if ( this.reflexBright != 1 )
1556
- ctx.filter = `brightness(${this.reflexBright})`;
1686
+ _ctx.filter = `brightness(${this.reflexBright})`;
1557
1687
 
1558
1688
  // create the reflection
1559
- ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
1560
- 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 );
1561
1691
 
1562
- ctx.restore();
1692
+ _ctx.restore();
1563
1693
  }
1564
1694
  }
1565
1695
 
1566
1696
  // draw scale on X-axis
1567
1697
  const drawScaleX = () => {
1568
1698
  if ( this.showScaleX ) {
1569
- if ( isRadial ) {
1570
- ctx.save();
1571
- ctx.translate( centerX, centerY );
1699
+ if ( _radial ) {
1700
+ _ctx.save();
1701
+ _ctx.translate( centerX, centerY );
1572
1702
  if ( this._spinSpeed )
1573
- ctx.rotate( this._spinAngle + HALF_PI );
1574
- ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1575
- ctx.restore();
1703
+ _ctx.rotate( this._spinAngle + HALF_PI );
1704
+ _ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
1705
+ _ctx.restore();
1576
1706
  }
1577
1707
  else
1578
- ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1708
+ _ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
1579
1709
  }
1580
1710
  }
1581
1711
 
@@ -1583,16 +1713,16 @@ export default class AudioMotionAnalyzer {
1583
1713
  const drawScaleY = channelTop => {
1584
1714
  const scaleWidth = canvasX.height,
1585
1715
  fontSize = scaleWidth >> 1,
1586
- max = isLinear ? 100 : maxdB,
1587
- min = isLinear ? 0 : mindB,
1588
- incr = isLinear ? 20 : 5,
1716
+ max = _linearAmplitude ? 100 : maxDecibels,
1717
+ min = _linearAmplitude ? 0 : minDecibels,
1718
+ incr = _linearAmplitude ? 20 : 5,
1589
1719
  interval = analyzerHeight / ( max - min );
1590
1720
 
1591
- ctx.save();
1592
- ctx.fillStyle = SCALEY_LABEL_COLOR;
1593
- ctx.font = `${fontSize}px ${FONT_FAMILY}`;
1594
- ctx.textAlign = 'right';
1595
- 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;
1596
1726
 
1597
1727
  for ( let val = max; val > min; val -= incr ) {
1598
1728
  const posY = channelTop + ( max - val ) * interval,
@@ -1600,26 +1730,26 @@ export default class AudioMotionAnalyzer {
1600
1730
 
1601
1731
  if ( even ) {
1602
1732
  const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
1603
- if ( mirrorMode != -1 )
1604
- ctx.fillText( val, scaleWidth * .85, labelY );
1605
- if ( mirrorMode != 1 )
1606
- ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
1607
- ctx.strokeStyle = SCALEY_LABEL_COLOR;
1608
- ctx.setLineDash([2,4]);
1609
- 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;
1610
1740
  }
1611
1741
  else {
1612
- ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1613
- ctx.setLineDash([2,8]);
1614
- ctx.lineDashOffset = 1;
1742
+ _ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
1743
+ _ctx.setLineDash([2,8]);
1744
+ _ctx.lineDashOffset = 1;
1615
1745
  }
1616
1746
 
1617
- ctx.beginPath();
1618
- ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
1619
- ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
1620
- 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();
1621
1751
  }
1622
- ctx.restore();
1752
+ _ctx.restore();
1623
1753
  }
1624
1754
 
1625
1755
  // returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
@@ -1632,7 +1762,7 @@ export default class AudioMotionAnalyzer {
1632
1762
  SQ12194 = 148693636,
1633
1763
  linearTodB = value => 20 * Math.log10( value );
1634
1764
 
1635
- switch ( weightingFilter ) {
1765
+ switch ( _weightingFilter ) {
1636
1766
  case FILTER_A : // A-weighting https://en.wikipedia.org/wiki/A-weighting
1637
1767
  const rA = ( SQ12194 * f2 ** 2 ) / ( ( f2 + SQ20_6 ) * Math.sqrt( ( f2 + SQ107_7 ) * ( f2 + SQ737_9 ) ) * ( f2 + SQ12194 ) );
1638
1768
  return 2 + linearTodB( rA );
@@ -1662,19 +1792,19 @@ export default class AudioMotionAnalyzer {
1662
1792
 
1663
1793
  // draws (stroke) a bar from x,y1 to x,y2
1664
1794
  const strokeBar = ( x, y1, y2 ) => {
1665
- ctx.beginPath();
1666
- ctx.moveTo( x, y1 );
1667
- ctx.lineTo( x, y2 );
1668
- ctx.stroke();
1795
+ _ctx.beginPath();
1796
+ _ctx.moveTo( x, y1 );
1797
+ _ctx.lineTo( x, y2 );
1798
+ _ctx.stroke();
1669
1799
  }
1670
1800
 
1671
1801
  // conditionally strokes current path on canvas
1672
1802
  const strokeIf = flag => {
1673
- if ( flag && lineWidth ) {
1674
- const alpha = ctx.globalAlpha;
1675
- ctx.globalAlpha = 1;
1676
- ctx.stroke();
1677
- ctx.globalAlpha = alpha;
1803
+ if ( flag && _lineWidth ) {
1804
+ const alpha = _ctx.globalAlpha;
1805
+ _ctx.globalAlpha = 1;
1806
+ _ctx.stroke();
1807
+ _ctx.globalAlpha = alpha;
1678
1808
  }
1679
1809
  }
1680
1810
 
@@ -1682,7 +1812,7 @@ export default class AudioMotionAnalyzer {
1682
1812
  const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
1683
1813
 
1684
1814
  // converts planar X,Y coordinates to radial coordinates
1685
- const radialXY = ( x, y, dir ) => {
1815
+ const radialXY = ( x, y, dir = 1 ) => {
1686
1816
  const height = radius + y,
1687
1817
  angle = getAngle( x, dir );
1688
1818
  return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
@@ -1690,21 +1820,21 @@ export default class AudioMotionAnalyzer {
1690
1820
 
1691
1821
  // draws a polygon of width `w` and height `h` at (x,y) in radial mode
1692
1822
  const radialPoly = ( x, y, w, h, stroke ) => {
1693
- ctx.beginPath();
1694
- for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
1823
+ _ctx.beginPath();
1824
+ for ( const dir of ( _mirror ? [1,-1] : [1] ) ) {
1695
1825
  const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
1696
- ctx.moveTo( ...radialXY( x, y, dir ) );
1697
- ctx.lineTo( ...radialXY( x, y + h, dir ) );
1826
+ _ctx.moveTo( ...radialXY( x, y, dir ) );
1827
+ _ctx.lineTo( ...radialXY( x, y + h, dir ) );
1698
1828
  if ( isRound )
1699
- ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
1829
+ _ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
1700
1830
  else
1701
- ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1702
- ctx.lineTo( ...radialXY( x + w, y, dir ) );
1831
+ _ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
1832
+ _ctx.lineTo( ...radialXY( x + w, y, dir ) );
1703
1833
  if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
1704
- ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
1834
+ _ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
1705
1835
  }
1706
1836
  strokeIf( stroke );
1707
- ctx.fill();
1837
+ _ctx.fill();
1708
1838
  }
1709
1839
 
1710
1840
  // converts a value in [0;1] range to a height in pixels that fits into the current LED elements
@@ -1712,35 +1842,36 @@ export default class AudioMotionAnalyzer {
1712
1842
 
1713
1843
  // update energy information
1714
1844
  const updateEnergy = newVal => {
1715
- energy.val = newVal;
1716
- if ( newVal >= energy.peak ) {
1717
- energy.peak = newVal;
1718
- 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 );
1719
1850
  }
1720
- else {
1721
- if ( energy.hold > 0 )
1722
- energy.hold--;
1723
- else if ( energy.peak > 0 )
1724
- 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;
1725
1854
  }
1726
1855
  }
1727
1856
 
1728
1857
  // calculate and display (if enabled) the current frame rate
1729
1858
  const updateFPS = () => {
1730
- this._frame++;
1731
- 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++;
1732
1863
 
1733
1864
  if ( elapsed >= 1000 ) {
1734
- this._fps = this._frame / ( elapsed / 1000 );
1735
- this._frame = 0;
1865
+ this._fps = this._frames / ( elapsed / 1000 );
1866
+ this._frames = 0;
1736
1867
  this._time = timestamp;
1737
1868
  }
1738
1869
  if ( this.showFPS ) {
1739
1870
  const size = canvasX.height;
1740
- ctx.font = `bold ${size}px ${FONT_FAMILY}`;
1741
- ctx.fillStyle = FPS_COLOR;
1742
- ctx.textAlign = 'right';
1743
- 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 );
1744
1875
  }
1745
1876
  }
1746
1877
 
@@ -1748,9 +1879,8 @@ export default class AudioMotionAnalyzer {
1748
1879
 
1749
1880
  let currentEnergy = 0;
1750
1881
 
1751
- const bars = this._bars,
1752
- nBars = bars.length,
1753
- nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
1882
+ const nBars = _bars.length,
1883
+ nChannels = isSingle ? 1 : 2;
1754
1884
 
1755
1885
  for ( let channel = 0; channel < nChannels; channel++ ) {
1756
1886
 
@@ -1758,8 +1888,9 @@ export default class AudioMotionAnalyzer {
1758
1888
  channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
1759
1889
  colorStops = channelGradient.colorStops,
1760
1890
  colorCount = colorStops.length,
1761
- bgColor = ( ! showBgColor || isLeds && ! isOverlay ) ? '#000' : channelGradient.bgColor,
1762
- 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
1763
1894
 
1764
1895
  // helper function for FFT data interpolation (uses fftData)
1765
1896
  const interpolate = ( bin, ratio ) => {
@@ -1771,53 +1902,53 @@ export default class AudioMotionAnalyzer {
1771
1902
  const setBarColor = ( value = 0, barIndex = 0 ) => {
1772
1903
  let color;
1773
1904
  // for mode 10, always use the channel gradient (ignore colorMode)
1774
- if ( ( colorMode == COLOR_GRADIENT && ! isTrueLeds ) || mode == 10 )
1775
- color = canvasGradients[ channel ];
1905
+ if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode == 10 )
1906
+ color = _canvasGradients[ channel ];
1776
1907
  else {
1777
- 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 );
1778
1909
  color = colorStops[ selectedIndex ].color;
1779
1910
  }
1780
- ctx.fillStyle = ctx.strokeStyle = color;
1911
+ _ctx.fillStyle = _ctx.strokeStyle = color;
1781
1912
  }
1782
1913
 
1783
1914
  if ( useCanvas ) {
1784
1915
  // clear the channel area, if in overlay mode
1785
1916
  // this is done per channel to clear any residue below 0 off the top channel (especially in line graph mode with lineWidth > 1)
1786
- if ( isOverlay && mustClear )
1787
- ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1917
+ if ( overlay && mustClear )
1918
+ _ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1788
1919
 
1789
1920
  // fill the analyzer background if needed (not overlay or overlay + showBgColor)
1790
- if ( ! isOverlay || showBgColor ) {
1791
- if ( isOverlay )
1792
- ctx.globalAlpha = this.bgAlpha;
1921
+ if ( ! overlay || showBgColor ) {
1922
+ if ( overlay )
1923
+ _ctx.globalAlpha = this.bgAlpha;
1793
1924
 
1794
- ctx.fillStyle = bgColor;
1925
+ _ctx.fillStyle = bgColor;
1795
1926
 
1796
1927
  // exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
1797
1928
  if ( mustClear )
1798
- 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 );
1799
1930
 
1800
- ctx.globalAlpha = 1;
1931
+ _ctx.globalAlpha = 1;
1801
1932
  }
1802
1933
 
1803
1934
  // draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
1804
- if ( this.showScaleY && ! isLumi && ! isRadial && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) )
1935
+ if ( this.showScaleY && ! isLumi && ! _radial && ( channel == 0 || ! isDualCombined ) )
1805
1936
  drawScaleY( channelTop );
1806
1937
 
1807
1938
  // set line width and dash for LEDs effect
1808
1939
  if ( isLeds ) {
1809
- ctx.setLineDash( [ ledHeight, ledSpaceV ] );
1810
- ctx.lineWidth = bars[0].width;
1940
+ _ctx.setLineDash( [ ledHeight, ledSpaceV ] );
1941
+ _ctx.lineWidth = _bars[0].width;
1811
1942
  }
1812
1943
  else // for outline effect ensure linewidth is not greater than half the bar width
1813
- ctx.lineWidth = isOutline ? Math.min( lineWidth, bars[0].width / 2 ) : lineWidth;
1944
+ _ctx.lineWidth = isOutline ? Math.min( _lineWidth, _bars[0].width / 2 ) : _lineWidth;
1814
1945
 
1815
1946
  // set clip region
1816
- ctx.save();
1817
- if ( ! isRadial ) {
1947
+ _ctx.save();
1948
+ if ( ! _radial ) {
1818
1949
  const channelRegion = new Path2D();
1819
1950
  channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
1820
- ctx.clip( channelRegion );
1951
+ _ctx.clip( channelRegion );
1821
1952
  }
1822
1953
  } // if ( useCanvas )
1823
1954
 
@@ -1826,11 +1957,11 @@ export default class AudioMotionAnalyzer {
1826
1957
  this._analyzer[ channel ].getFloatFrequencyData( fftData );
1827
1958
 
1828
1959
  // apply weighting
1829
- if ( weightingFilter )
1960
+ if ( _weightingFilter )
1830
1961
  fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
1831
1962
 
1832
1963
  // start drawing path (for mode 10)
1833
- ctx.beginPath();
1964
+ _ctx.beginPath();
1834
1965
 
1835
1966
  // store line graph points to create mirror effect in radial mode
1836
1967
  let points = [];
@@ -1839,7 +1970,7 @@ export default class AudioMotionAnalyzer {
1839
1970
 
1840
1971
  for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
1841
1972
 
1842
- const bar = bars[ barIndex ],
1973
+ const bar = _bars[ barIndex ],
1843
1974
  { posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
1844
1975
 
1845
1976
  let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
@@ -1861,13 +1992,13 @@ export default class AudioMotionAnalyzer {
1861
1992
  bar.hold[ channel ]--;
1862
1993
  // if hold is negative, it becomes the "acceleration" for peak drop
1863
1994
  if ( bar.hold[ channel ] < 0 )
1864
- bar.peak[ channel ] += bar.hold[ channel ] / maxBarHeight;
1995
+ bar.peak[ channel ] += bar.hold[ channel ] / ( holdFrames * holdFrames / 2 );
1865
1996
  }
1866
1997
 
1867
1998
  // check if it's a new peak for this bar
1868
1999
  if ( barValue >= bar.peak[ channel ] ) {
1869
2000
  bar.peak[ channel ] = barValue;
1870
- bar.hold[ channel ] = 30; // set peak hold time to 30 frames (0.5s)
2001
+ bar.hold[ channel ] = holdFrames;
1871
2002
  }
1872
2003
 
1873
2004
  // if not using the canvas, move earlier to the next bar
@@ -1876,71 +2007,67 @@ export default class AudioMotionAnalyzer {
1876
2007
 
1877
2008
  // set opacity for bar effects
1878
2009
  if ( isLumi || isAlpha )
1879
- ctx.globalAlpha = barValue;
2010
+ _ctx.globalAlpha = barValue;
1880
2011
  else if ( isOutline )
1881
- ctx.globalAlpha = fillAlpha;
2012
+ _ctx.globalAlpha = fillAlpha;
1882
2013
 
1883
2014
  // set fillStyle and strokeStyle for the current bar
1884
2015
  setBarColor( barValue, barIndex );
1885
2016
 
1886
2017
  // compute actual bar height on screen
1887
- let barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
1888
-
1889
- // invert bar for radial channel 1
1890
- if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
1891
- barHeight *= -1;
2018
+ const barHeight = ( isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0 ) * direction;
1892
2019
 
1893
2020
  // Draw current bar or line segment
1894
2021
 
1895
- if ( mode == 10 ) {
2022
+ if ( _mode == 10 ) {
1896
2023
  // compute the average between the initial bar (barIndex==0) and the next one
1897
2024
  // used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
1898
- 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;
1899
2026
 
1900
- if ( isRadial ) {
2027
+ if ( _radial ) {
1901
2028
  if ( barIndex == 0 )
1902
- ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ), 1 ) );
2029
+ _ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ) ) );
1903
2030
  // draw line to the current point, avoiding overlapping wrap-around frequencies
1904
2031
  if ( posX >= 0 ) {
1905
2032
  const point = [ posX, barHeight ];
1906
- ctx.lineTo( ...radialXY( ...point, 1 ) );
2033
+ _ctx.lineTo( ...radialXY( ...point ) );
1907
2034
  points.push( point );
1908
2035
  }
1909
2036
  }
1910
2037
  else { // Linear
1911
2038
  if ( barIndex == 0 ) {
1912
2039
  // start the line off-screen using the previous FFT bin value as the initial amplitude
1913
- if ( mirrorMode != -1 ) {
2040
+ if ( _mirror != -1 ) {
1914
2041
  const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
1915
- ctx.moveTo( initialX - lineWidth, analyzerBottom - prevFFTData );
2042
+ _ctx.moveTo( initialX - _lineWidth, analyzerBottom - prevFFTData );
1916
2043
  }
1917
2044
  else
1918
- ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
2045
+ _ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
1919
2046
  }
1920
2047
  // draw line to the current point
1921
2048
  // avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
1922
- if ( mirrorMode != -1 || posX >= initialX )
1923
- ctx.lineTo( posX, analyzerBottom - barHeight );
2049
+ if ( _mirror != -1 || posX >= initialX )
2050
+ _ctx.lineTo( posX, analyzerBottom - barHeight );
1924
2051
  }
1925
2052
  }
1926
2053
  else {
1927
2054
  if ( isLeds ) {
1928
2055
  // draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
1929
- if ( showBgColor && ! isOverlay && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) ) {
1930
- const alpha = ctx.globalAlpha;
1931
- ctx.strokeStyle = LEDS_UNLIT_COLOR;
1932
- 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;
1933
2060
  strokeBar( barCenter, channelTop, analyzerBottom );
1934
2061
  // restore properties
1935
- ctx.strokeStyle = ctx.fillStyle;
1936
- ctx.globalAlpha = alpha;
2062
+ _ctx.strokeStyle = _ctx.fillStyle;
2063
+ _ctx.globalAlpha = alpha;
1937
2064
  }
1938
2065
  if ( isTrueLeds ) {
1939
2066
  // ledPosY() is used below to fit one entire led height into the selected range
1940
2067
  const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
1941
2068
  let last = analyzerBottom;
1942
2069
  for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
1943
- ctx.strokeStyle = colorStops[ i ].color;
2070
+ _ctx.strokeStyle = colorStops[ i ].color;
1944
2071
  let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
1945
2072
  strokeBar( barCenter, last, y );
1946
2073
  last = y - ledSpaceV;
@@ -1950,53 +2077,53 @@ export default class AudioMotionAnalyzer {
1950
2077
  strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
1951
2078
  }
1952
2079
  else if ( posX >= initialX ) {
1953
- if ( isRadial )
2080
+ if ( _radial )
1954
2081
  radialPoly( posX, 0, width, barHeight, isOutline );
1955
2082
  else if ( isRound ) {
1956
2083
  const halfWidth = width / 2,
1957
2084
  y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
1958
2085
 
1959
- ctx.beginPath();
1960
- ctx.moveTo( posX, y );
1961
- ctx.lineTo( posX, y - barHeight );
1962
- ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
1963
- 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 );
1964
2091
  strokeIf( isOutline );
1965
- ctx.fill();
2092
+ _ctx.fill();
1966
2093
  }
1967
2094
  else {
1968
- const offset = isOutline ? ctx.lineWidth : 0;
1969
- ctx.beginPath();
1970
- 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 );
1971
2098
  strokeIf( isOutline );
1972
- ctx.fill();
2099
+ _ctx.fill();
1973
2100
  }
1974
2101
  }
1975
2102
  }
1976
2103
 
1977
2104
  // Draw peak
1978
2105
  const peak = bar.peak[ channel ];
1979
- if ( peak > 0 && this.showPeaks && ! isLumi && posX >= initialX && posX < finalX ) {
2106
+ if ( peak > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
1980
2107
  // set opacity
1981
- if ( isOutline && lineWidth > 0 )
1982
- ctx.globalAlpha = 1;
2108
+ if ( isOutline && _lineWidth > 0 )
2109
+ _ctx.globalAlpha = 1;
1983
2110
  else if ( isAlpha )
1984
- ctx.globalAlpha = peak;
2111
+ _ctx.globalAlpha = peak;
1985
2112
 
1986
2113
  // select the peak color for 'bar-level' colorMode or 'trueLeds'
1987
- if ( colorMode == COLOR_BAR_LEVEL || isTrueLeds )
2114
+ if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
1988
2115
  setBarColor( peak );
1989
2116
 
1990
2117
  // render peak according to current mode / effect
1991
2118
  if ( isLeds ) {
1992
2119
  const ledPeak = ledPosY( peak );
1993
2120
  if ( ledPeak >= ledSpaceV ) // avoid peak below first led
1994
- ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
2121
+ _ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
1995
2122
  }
1996
- else if ( ! isRadial )
1997
- ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
1998
- else if ( mode != 10 ) // radial - no peaks for mode 10
1999
- 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 );
2000
2127
  }
2001
2128
 
2002
2129
  } // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
@@ -2005,41 +2132,72 @@ export default class AudioMotionAnalyzer {
2005
2132
  if ( ! useCanvas )
2006
2133
  continue;
2007
2134
 
2008
- ctx.restore(); // restore clip region
2135
+ _ctx.restore(); // restore clip region
2009
2136
 
2010
2137
  // restore global alpha
2011
- ctx.globalAlpha = 1;
2138
+ _ctx.globalAlpha = 1;
2012
2139
 
2013
2140
  // Fill/stroke drawing path for mode 10
2014
- if ( mode == 10 ) {
2141
+ if ( _mode == 10 ) {
2015
2142
  setBarColor(); // select channel gradient
2016
2143
 
2017
- if ( isRadial ) {
2018
- if ( mirrorMode ) {
2144
+ if ( _radial ) {
2145
+ if ( _mirror ) {
2019
2146
  let p;
2020
2147
  while ( p = points.pop() )
2021
- ctx.lineTo( ...radialXY( ...p, -1 ) );
2148
+ _ctx.lineTo( ...radialXY( ...p, -1 ) );
2022
2149
  }
2023
- ctx.closePath();
2150
+ _ctx.closePath();
2024
2151
  }
2025
2152
 
2026
- if ( lineWidth > 0 )
2027
- ctx.stroke();
2153
+ if ( _lineWidth > 0 )
2154
+ _ctx.stroke();
2028
2155
 
2029
2156
  if ( fillAlpha > 0 ) {
2030
- if ( isRadial ) {
2157
+ if ( _radial ) {
2031
2158
  // exclude the center circle from the fill area
2032
- ctx.moveTo( centerX + radius, centerY );
2033
- ctx.arc( centerX, centerY, radius, 0, TAU, true );
2159
+ _ctx.moveTo( centerX + radius, centerY );
2160
+ _ctx.arc( centerX, centerY, radius, 0, TAU, true );
2034
2161
  }
2035
2162
  else { // close the fill area
2036
- ctx.lineTo( finalX, analyzerBottom );
2037
- ctx.lineTo( initialX, analyzerBottom );
2163
+ _ctx.lineTo( finalX, analyzerBottom );
2164
+ _ctx.lineTo( initialX, analyzerBottom );
2038
2165
  }
2039
2166
 
2040
- ctx.globalAlpha = fillAlpha;
2041
- ctx.fill();
2042
- 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
+ }
2043
2201
  }
2044
2202
  }
2045
2203
 
@@ -2052,14 +2210,14 @@ export default class AudioMotionAnalyzer {
2052
2210
 
2053
2211
  if ( useCanvas ) {
2054
2212
  // Mirror effect
2055
- if ( mirrorMode && ! isRadial ) {
2056
- ctx.setTransform( -1, 0, 0, 1, canvas.width - initialX, 0 );
2057
- ctx.drawImage( canvas, initialX, 0, centerX, canvas.height, 0, 0, centerX, canvas.height );
2058
- 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 );
2059
2217
  }
2060
2218
 
2061
2219
  // restore solid lines
2062
- ctx.setLineDash([]);
2220
+ _ctx.setLineDash([]);
2063
2221
 
2064
2222
  // draw frequency scale (X-axis)
2065
2223
  drawScaleX();
@@ -2070,14 +2228,11 @@ export default class AudioMotionAnalyzer {
2070
2228
 
2071
2229
  // call callback function, if defined
2072
2230
  if ( this.onCanvasDraw ) {
2073
- ctx.save();
2074
- ctx.fillStyle = ctx.strokeStyle = canvasGradients[0];
2075
- this.onCanvasDraw( this, { timestamp, canvasGradients } );
2076
- ctx.restore();
2231
+ _ctx.save();
2232
+ _ctx.fillStyle = _ctx.strokeStyle = _canvasGradients[0];
2233
+ this.onCanvasDraw( this, { timestamp, _canvasGradients } );
2234
+ _ctx.restore();
2077
2235
  }
2078
-
2079
- // schedule next canvas update
2080
- this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
2081
2236
  }
2082
2237
 
2083
2238
  /**
@@ -2110,24 +2265,19 @@ export default class AudioMotionAnalyzer {
2110
2265
  * Generate currently selected gradient
2111
2266
  */
2112
2267
  _makeGrad() {
2113
-
2114
2268
  if ( ! this._ready )
2115
2269
  return;
2116
2270
 
2117
- const ctx = this._canvasCtx,
2118
- canvas = ctx.canvas,
2119
- channelLayout = this._chLayout,
2271
+ const { canvas, _ctx, _radial, _reflexRatio } = this,
2272
+ { analyzerWidth, initialX, radius } = this._aux,
2120
2273
  { isLumi } = this._flg,
2121
- isRadial = this._radial,
2122
- 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;
2123
2280
  // for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
2124
- analyzerRatio = 1 - this._reflexRatio,
2125
- { analyzerWidth, initialX, radius } = this._aux;
2126
-
2127
- // for radial mode
2128
- const centerX = canvas.width >> 1,
2129
- centerY = canvas.height >> 1,
2130
- maxRadius = Math.min( centerX, centerY );
2131
2281
 
2132
2282
  for ( const channel of [0,1] ) {
2133
2283
  const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
@@ -2136,13 +2286,13 @@ export default class AudioMotionAnalyzer {
2136
2286
 
2137
2287
  let grad;
2138
2288
 
2139
- if ( isRadial )
2140
- 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 );
2141
2291
  else
2142
- 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 ] ) );
2143
2293
 
2144
2294
  if ( colorStops ) {
2145
- const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
2295
+ const dual = isDualVertical && ! this._splitGradient && ( ! isHorizontal || _radial );
2146
2296
 
2147
2297
  for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
2148
2298
  const maxIndex = colorStops.length - 1;
@@ -2155,17 +2305,17 @@ export default class AudioMotionAnalyzer {
2155
2305
  offset /= 2;
2156
2306
 
2157
2307
  // constrain the offset within the useful analyzer areas (avoid reflex areas)
2158
- if ( channelLayout == CHANNEL_VERTICAL && ! isLumi && ! isRadial && ! isHorizontal ) {
2308
+ if ( isDualVertical && ! isLumi && ! _radial && ! isHorizontal ) {
2159
2309
  offset *= analyzerRatio;
2160
2310
  // skip the first reflex area in split mode
2161
2311
  if ( ! dual && offset > .5 * analyzerRatio )
2162
- offset += .5 * this._reflexRatio;
2312
+ offset += .5 * _reflexRatio;
2163
2313
  }
2164
2314
 
2165
2315
  // only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
2166
2316
  if ( channelArea == 1 ) {
2167
2317
  // add colors in reverse order if radial or lumi are active
2168
- if ( isRadial || isLumi ) {
2318
+ if ( _radial || isLumi ) {
2169
2319
  const revIndex = maxIndex - index;
2170
2320
  colorStop = colorStops[ revIndex ];
2171
2321
  offset = 1 - colorStop.pos / 2;
@@ -2183,7 +2333,7 @@ export default class AudioMotionAnalyzer {
2183
2333
  grad.addColorStop( offset, colorStop.color );
2184
2334
 
2185
2335
  // create additional color stop at the end of first channel to prevent bleeding
2186
- if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
2336
+ if ( isDualVertical && index == maxIndex && offset < .5 )
2187
2337
  grad.addColorStop( .5, colorStop.color );
2188
2338
  });
2189
2339
  } // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
@@ -2218,12 +2368,10 @@ export default class AudioMotionAnalyzer {
2218
2368
  * Internal function to change canvas dimensions on demand
2219
2369
  */
2220
2370
  _setCanvas( reason ) {
2221
- // if initialization is not finished, quit
2222
2371
  if ( ! this._ready )
2223
2372
  return;
2224
2373
 
2225
- const ctx = this._canvasCtx,
2226
- canvas = ctx.canvas,
2374
+ const { canvas, _ctx } = this,
2227
2375
  canvasX = this._scaleX.canvas,
2228
2376
  pixelRatio = window.devicePixelRatio / ( this._loRes + 1 );
2229
2377
 
@@ -2254,12 +2402,12 @@ export default class AudioMotionAnalyzer {
2254
2402
 
2255
2403
  // if not in overlay mode, paint the canvas black
2256
2404
  if ( ! this.overlay ) {
2257
- ctx.fillStyle = '#000';
2258
- ctx.fillRect( 0, 0, newWidth, newHeight );
2405
+ _ctx.fillStyle = '#000';
2406
+ _ctx.fillRect( 0, 0, newWidth, newHeight );
2259
2407
  }
2260
2408
 
2261
2409
  // set lineJoin property for area fill mode (this is reset whenever the canvas size changes)
2262
- ctx.lineJoin = 'bevel';
2410
+ _ctx.lineJoin = 'bevel';
2263
2411
 
2264
2412
  // update dimensions of the scale canvas
2265
2413
  canvasX.width = newWidth;
@@ -2324,6 +2472,7 @@ export default class AudioMotionAnalyzer {
2324
2472
  loRes : false,
2325
2473
  lumiBars : false,
2326
2474
  maxDecibels : -25,
2475
+ maxFPS : 0,
2327
2476
  maxFreq : 22000,
2328
2477
  minDecibels : -85,
2329
2478
  minFreq : 20,
@@ -2332,6 +2481,7 @@ export default class AudioMotionAnalyzer {
2332
2481
  noteLabels : false,
2333
2482
  outlineBars : false,
2334
2483
  overlay : false,
2484
+ peakLine : false,
2335
2485
  radial : false,
2336
2486
  reflexAlpha : 0.15,
2337
2487
  reflexBright : 1,