audiomotion-analyzer 4.0.0-beta.3 → 4.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,7 +35,7 @@ What users are saying:
35
35
  + Optional effects: LED bars, luminance bars, mirroring and reflection, radial spectrum
36
36
  + Comes with 3 predefined color gradients - easily add your own!
37
37
  + Fullscreen support, ready for retina / HiDPI displays
38
- + Zero-dependency native ES6+ module (ESM), \~20kB minified
38
+ + Zero-dependency native ES6+ module (ESM), \~25kB minified
39
39
 
40
40
  ## Online demos
41
41
 
@@ -119,12 +119,15 @@ options = {<br>
119
119
  &emsp;&emsp;[audioCtx](#audioctx-audiocontext-object): *undefined*, // constructor only<br>
120
120
  &emsp;&emsp;[barSpace](#barspace-number): **0.1**,<br>
121
121
  &emsp;&emsp;[bgAlpha](#bgalpha-number): **0.7**,<br>
122
+ &emsp;&emsp;[channelLayout](#channellayout-string): **'single'**,<br>
122
123
  &emsp;&emsp;[connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
123
124
  &emsp;&emsp;[fftSize](#fftsize-number): **8192**,<br>
124
125
  &emsp;&emsp;[fillAlpha](#fillalpha-number): **1**,<br>
125
126
  &emsp;&emsp;[frequencyScale](#frequencyscale-string): **'log'**,<br>
126
127
  &emsp;&emsp;[fsElement](#fselement-htmlelement-object): *undefined*, // constructor only<br>
127
128
  &emsp;&emsp;[gradient](#gradient-string): **'classic'**,<br>
129
+ &emsp;&emsp;[gradientLeft](#gradientleft-string): *undefined*,<br>
130
+ &emsp;&emsp;[gradientRight](#gradientright-string): *undefined*,<br>
128
131
  &emsp;&emsp;[height](#height-number): *undefined*,<br>
129
132
  &emsp;&emsp;[ledBars](#ledbars-boolean): **false**,<br>
130
133
  &emsp;&emsp;[linearAmplitude](#linearamplitude-boolean): **false**,<br>
@@ -158,7 +161,7 @@ options = {<br>
158
161
  &emsp;&emsp;[spinSpeed](#spinspeed-number): **0**,<br>
159
162
  &emsp;&emsp;[splitGradient](#splitgradient-boolean): **false**,<br>
160
163
  &emsp;&emsp;[start](#start-boolean): **true**,<br>
161
- &emsp;&emsp;[stereo](#stereo-boolean): **false**,<br>
164
+ &emsp;&emsp;[stereo](#stereo-deprecated-boolean): **false**, // DEPRECATED - use channelLayout instead<br>
162
165
  &emsp;&emsp;[useCanvas](#usecanvas-boolean): **true**,<br>
163
166
  &emsp;&emsp;[volume](#volume-number): **1**,<br>
164
167
  &emsp;&emsp;[weightingFilter](#weightingFilter-string): **''**<br>
@@ -315,6 +318,23 @@ Defaults to **0.7**.
315
318
 
316
319
  [2D rendering context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) used for drawing in audioMotion's *Canvas*.
317
320
 
321
+ ### `channelLayout` *string*
322
+
323
+ *Available since v4.0.0*
324
+
325
+ Defines the number and layout of analyzer channels.
326
+
327
+ channelLayout | description
328
+ -------------- |------------
329
+ 'single' | Single channel analyzer, representing the combined output of both left and right channels.
330
+ 'dualVertical' | Dual channel analyzer, with left channel shown at the top and right channel at the bottom.
331
+ 'dualCombined' | Left and right channel graphs are shown overlaid. Works best with semi-transparent **Graph** [`mode`](#mode-number) or [`outlineBars`](#outlinebars-boolean).
332
+
333
+ !> When a *dual* layout is selected, any mono (single channel) audio source connected to the analyzer will output sound only from the left speaker,
334
+ unless a stereo source is simultaneously connected to the analyzer, which will force the mono input to be upmixed to stereo.
335
+
336
+ See also [`gradientLeft`](#gradientleft-string), [`gradientRight`](#gradientright-string) and [`splitGradient`](#splitgradient-boolean).
337
+
318
338
  ### `connectedSources` *array* *(Read only)*
319
339
 
320
340
  *Available since v3.0.0*
@@ -398,8 +418,21 @@ Name of the color gradient used for analyzer graphs.
398
418
 
399
419
  It must be a built-in or [registered](#registergradient-name-options-) gradient name. Built-in gradients are *'classic'*, *'prism'* and *'rainbow'*.
400
420
 
421
+ See also [`gradientLeft`](#gradientleft-string) and [`gradientRight`](#gradientright-string).
422
+
401
423
  Defaults to **'classic'**.
402
424
 
425
+ ### `gradientLeft` *string*
426
+ ### `gradientRight` *string*
427
+
428
+ *Available since v4.0.0*
429
+
430
+ When using a dual [`channelLayout`](#channellayout-string), different gradients can be selected for the left and right channels.
431
+
432
+ When [`channelLayout`](#channellayout-string) is set to *'single'*, the gradient selected by `gradientLeft` is used.
433
+
434
+ The [`gradient`](#gradient-string) property can be used as a shorthand to set the same gradient for both channels. Its read value returns only the left (or single) channel gradient though.
435
+
403
436
  ### `height` *number*
404
437
  ### `width` *number*
405
438
 
@@ -655,7 +688,7 @@ When *true*, the spectrum analyzer is rendered in a circular shape, with radial
655
688
  In radial visualization, [`ledBars`](#ledbars-boolean) and [`lumiBars`](#lumibars-boolean) effects are disabled, and
656
689
  [`showPeaks`](#showpeaks-boolean) has no effect for [**Graph** mode](#mode-number).
657
690
 
658
- When [`stereo`](#stereo-boolean) is *true*, a larger diameter is used and the right channel bars are rendered towards the center of the analyzer.
691
+ When [`channelLayout`](#channellayout-string) is set to *'dualVertical'*, a larger diameter is used and the right channel bars are rendered towards the center of the analyzer.
659
692
 
660
693
  See also [`spinSpeed`](#spinspeed-number).
661
694
 
@@ -782,24 +815,13 @@ When *true*, the gradient will be split between both channels, so each channel w
782
815
  |:--:|:--:|
783
816
  | ![split-on](img/splitGradient_on.png) | ![split-off](img/splitGradient_off.png) |
784
817
 
785
- This option has no effect on horizontal gradients, or when [`stereo`](#stereo-boolean) is set to *false*.
818
+ This option has no effect on horizontal gradients (except in [`radial`](#radial-boolean) visualization - see note in [`registerGradient()`](#registergradient-name-options-)), or when [`channelLayout`](#channellayout-string) is set to *'single'* or *'dualCombined'*.
786
819
 
787
820
  Defaults to **false**.
788
821
 
789
- ### `stereo` *boolean*
822
+ ### `stereo` **(DEPRECATED)** *boolean*
790
823
 
791
- *Available since v3.0.0*
792
-
793
- When *true*, the spectrum analyzer will display separate graphs for the left and right audio channels.
794
-
795
- Notes:
796
- - Stereo tracks will always output stereo audio, even if `stereo` is set to *false* (in such case the analyzer graph will represent both channels combined);
797
- - Mono (single channel) tracks will output audio only on the left channel when `stereo` is *true*, unless you have another stereo source simultaneously
798
- connected to the analyzer, which will force the mono source to be upmixed to stereo.
799
-
800
- See also [`splitGradient`](#splitgradient-boolean).
801
-
802
- Defaults to **false**.
824
+ **This property will be removed in version 5** - Use [`channelLayout`](#channellayout-string) instead.
803
825
 
804
826
  ### `useCanvas` *boolean*
805
827
 
@@ -1010,7 +1032,7 @@ Returns an array with current data for each analyzer bar. Each array element is
1010
1032
 
1011
1033
  `hold` values are integers and indicate the hold time (in frames) for the current peak. The maximum value is 30 and means the peak has just been set, while negative values mean the peak is currently falling down.
1012
1034
 
1013
- Please note that `hold` and `value` will have only one element when [`stereo`](#stereo-boolean) is *false*, but `peak` is always a two-element array.
1035
+ Please note that `hold` and `value` will have only one element when [`channelLayout`](#channellayout-string) is set to *'single'*, but `peak` is always a two-element array.
1014
1036
 
1015
1037
  You can use this method to create your own visualizations using the analyzer data. See [this pen](https://codepen.io/hvianna/pen/ZEKWWJb) for usage example.
1016
1038
 
@@ -1043,31 +1065,25 @@ Use this method inside your callback function to create additional visual effect
1043
1065
 
1044
1066
  Registers a custom color gradient.
1045
1067
 
1046
- `name` must be a non-empty *string* that will be used to select this gradient, via the [`gradient`](#gradient-string) property.
1068
+ `name` must be a non-empty string that will be used to select this gradient, via the [`gradient`](#gradient-string) property.
1069
+
1047
1070
  `options` must be an object as shown below:
1048
1071
 
1049
1072
  ```js
1050
1073
  const options = {
1051
1074
  bgColor: '#011a35', // background color (optional) - defaults to '#111'
1052
1075
  dir: 'h', // add this property to create a horizontal gradient (optional)
1053
- colorStops: [ // list your gradient colors in this array (at least 2 entries are required)
1076
+ colorStops: [ // list your gradient colors in this array (at least one color is required)
1054
1077
  'red', // colors may be defined in any valid CSS format
1055
1078
  { pos: .6, color: '#ff0' }, // use an object to adjust the offset (0 to 1) of a colorStop
1056
- 'hsl( 120, 100%, 50% )' // colors may be defined in any valid CSS format
1079
+ 'hsl( 120, 100%, 50% )'
1057
1080
  ]
1058
1081
  }
1059
1082
 
1060
1083
  audioMotion.registerGradient( 'myGradient', options );
1061
1084
  ```
1062
1085
 
1063
- !> In TypeScript projects make sure to import the `GradientOptions` definition and use it as the type of your object, like so:
1064
-
1065
- ```js
1066
- import AudioMotionAnalyzer, { GradientOptions } from 'audiomotion-analyzer'
1067
-
1068
- const options: GradientOptions = {
1069
-
1070
- ```
1086
+ ?> During [`radial`](#radial-boolean) visualization all gradients are rendered in radial direction, so the horizontal flag (`dir: 'h'`) has no effect.
1071
1087
 
1072
1088
  Additional information about [gradient color-stops](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient/addColorStop).
1073
1089
 
@@ -1264,11 +1280,15 @@ document.getElementById('stop').addEventListener( 'click', () => myAudio.pause()
1264
1280
  See [Changelog.md](Changelog.md)
1265
1281
 
1266
1282
 
1267
- ## Get in touch!
1283
+ ## Contributing
1284
+
1285
+ If you want to send feedback, ask a question, or need help with something, please use the [**Discussions**](https://github.com/hvianna/audioMotion-analyzer/discussions) area on GitHub.
1286
+
1287
+ I would love to see your cool projects using **audioMotion-analyzer** -- post them in the *Show and tell* section of [Discussions](https://github.com/hvianna/audioMotion-analyzer/discussions)!
1268
1288
 
1269
- If you create something cool with **audioMotion-analyzer**, or simply think it's useful, I would love to know! Please drop me an e-mail at hvianna@gmail.com
1289
+ For **bug reports** and **feature requests**, feel free to [open an issue](https://github.com/hvianna/audioMotion-analyzer/issues).
1270
1290
 
1271
- For feature requests, suggestions or feedback, please see the [CONTRIBUTING.md](CONTRIBUTING.md) file.
1291
+ If you want to submit a **Pull Request**, please branch it off the project's `develop` branch.
1272
1292
 
1273
1293
  And if you're feeling generous, maybe:
1274
1294
 
@@ -1279,5 +1299,5 @@ And if you're feeling generous, maybe:
1279
1299
 
1280
1300
  ## License
1281
1301
 
1282
- audioMotion-analyzer copyright (c) 2018-2022 [Henrique Avila Vianna](https://henriquevianna.com)<br>
1302
+ audioMotion-analyzer copyright (c) 2018-2023 [Henrique Avila Vianna](https://henriquevianna.com)<br>
1283
1303
  Licensed under the [GNU Affero General Public License, version 3 or later](https://www.gnu.org/licenses/agpl.html).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "audiomotion-analyzer",
3
3
  "description": "High-resolution real-time graphic audio spectrum analyzer JavaScript module with no dependencies.",
4
- "version": "4.0.0-beta.3",
4
+ "version": "4.0.0-beta.4",
5
5
  "main": "./src/audioMotion-analyzer.js",
6
6
  "module": "./src/audioMotion-analyzer.js",
7
7
  "types": "./src/index.d.ts",
@@ -2,12 +2,12 @@
2
2
  * audioMotion-analyzer
3
3
  * High-resolution real-time graphic audio spectrum analyzer JS module
4
4
  *
5
- * @version 4.0.0-beta.3
5
+ * @version 4.0.0-beta.4
6
6
  * @author Henrique Avila Vianna <hvianna@gmail.com> <https://henriquevianna.com>
7
7
  * @license AGPL-3.0-or-later
8
8
  */
9
9
 
10
- const VERSION = '4.0.0-beta.3';
10
+ const VERSION = '4.0.0-beta.4';
11
11
 
12
12
  // internal constants
13
13
  const TAU = 2 * Math.PI,
@@ -16,6 +16,9 @@ const TAU = 2 * Math.PI,
16
16
  C_1 = 8.17579892; // frequency for C -1
17
17
 
18
18
  const CANVAS_BACKGROUND_COLOR = '#000',
19
+ CHANNEL_COMBINED = 'dualCombined',
20
+ CHANNEL_SINGLE = 'single',
21
+ CHANNEL_VERTICAL = 'dualVertical',
19
22
  GRADIENT_DEFAULT_BGCOLOR = '#111',
20
23
  FILTER_NONE = '',
21
24
  FILTER_A = 'A',
@@ -54,7 +57,7 @@ const ERR_AUDIO_CONTEXT_FAIL = [ 'ERR_AUDIO_CONTEXT_FAIL', 'Could not create
54
57
  ERR_INVALID_AUDIO_SOURCE = [ 'ERR_INVALID_AUDIO_SOURCE', 'Audio source must be an instance of HTMLMediaElement or AudioNode' ],
55
58
  ERR_GRADIENT_INVALID_NAME = [ 'ERR_GRADIENT_INVALID_NAME', 'Gradient name must be a non-empty string' ],
56
59
  ERR_GRADIENT_NOT_AN_OBJECT = [ 'ERR_GRADIENT_NOT_AN_OBJECT', 'Gradient options must be an object' ],
57
- ERR_GRADIENT_MISSING_COLOR = [ 'ERR_GRADIENT_MISSING_COLOR', 'Gradient must define at least two colors' ];
60
+ ERR_GRADIENT_MISSING_COLOR = [ 'ERR_GRADIENT_MISSING_COLOR', 'Gradient colorStops must be a non-empty array' ];
58
61
 
59
62
  class AudioMotionError extends Error {
60
63
  constructor( error, value ) {
@@ -65,6 +68,9 @@ class AudioMotionError extends Error {
65
68
  }
66
69
  }
67
70
 
71
+ // helper function
72
+ const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated. Use ${alternative} instead.` );
73
+
68
74
  // AudioMotionAnalyzer class
69
75
 
70
76
  export default class AudioMotionAnalyzer {
@@ -80,8 +86,10 @@ export default class AudioMotionAnalyzer {
80
86
 
81
87
  this._ready = false;
82
88
 
83
- // Initialize internal gradients object
84
- this._gradients = {};
89
+ // Initialize internal gradient objects
90
+ this._gradients = {}; // registered gradients
91
+ this._gradientNames = []; // names of the currently selected gradients for channels 0 and 1
92
+ this._canvasGradients = []; // actual CanvasGradient objects for channels 0 and 1
85
93
 
86
94
  // Register built-in gradients
87
95
  this.registerGradient( GRADIENT_CLASSIC, {
@@ -147,13 +155,13 @@ export default class AudioMotionAnalyzer {
147
155
  Connection routing:
148
156
  ===================
149
157
 
150
- for STEREO: +---> analyzer[0] ---+
158
+ for dual channel modes: +---> analyzer[0] ---+
151
159
  | |
152
160
  (source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
153
161
  | |
154
162
  +---> analyzer[1] ---+
155
163
 
156
- for MONO:
164
+ for single channel mode:
157
165
 
158
166
  (source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
159
167
 
@@ -303,6 +311,27 @@ export default class AudioMotionAnalyzer {
303
311
  this._calcAux();
304
312
  }
305
313
 
314
+ get channelLayout() {
315
+ return this._chLayout;
316
+ }
317
+ set channelLayout( value ) {
318
+ const MODES = [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ];
319
+ this._chLayout = MODES[ Math.max( 0, MODES.findIndex( el => el.toLowerCase() == ( '' + value ).toLowerCase() ) ) ];
320
+
321
+ // update node connections
322
+ this._input.disconnect();
323
+ this._input.connect( this._chLayout != CHANNEL_SINGLE ? this._splitter : this._analyzer[0] );
324
+ this._analyzer[0].disconnect();
325
+ if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
326
+ this._analyzer[0].connect( this._chLayout != CHANNEL_SINGLE ? this._merger : this._output );
327
+
328
+ // update properties affected by channel layout
329
+ this._calcAux();
330
+ this._createScales();
331
+ this._calcLeds();
332
+ this._makeGrad();
333
+ }
334
+
306
335
  get fftSize() {
307
336
  return this._analyzer[0].fftSize;
308
337
  }
@@ -325,14 +354,24 @@ export default class AudioMotionAnalyzer {
325
354
  }
326
355
 
327
356
  get gradient() {
328
- return this._gradient;
357
+ return this._gradientNames[0];
329
358
  }
330
359
  set gradient( value ) {
331
- if ( ! this._gradients.hasOwnProperty( value ) )
332
- throw new AudioMotionError( ERR_UNKNOWN_GRADIENT, value );
360
+ this._setGradient( value );
361
+ }
333
362
 
334
- this._gradient = value;
335
- this._makeGrad();
363
+ get gradientLeft() {
364
+ return this._gradientNames[0];
365
+ }
366
+ set gradientLeft( value ) {
367
+ this._setGradient( value, 0 );
368
+ }
369
+
370
+ get gradientRight() {
371
+ return this._gradientNames[1];
372
+ }
373
+ set gradientRight( value ) {
374
+ this._setGradient( value, 1 );
336
375
  }
337
376
 
338
377
  get height() {
@@ -516,23 +555,12 @@ export default class AudioMotionAnalyzer {
516
555
  }
517
556
 
518
557
  get stereo() {
519
- return this._stereo;
558
+ deprecate( 'stereo', 'channelLayout' );
559
+ return this._chLayout != CHANNEL_SINGLE;
520
560
  }
521
561
  set stereo( value ) {
522
- this._stereo = !! value;
523
-
524
- // update node connections
525
- this._input.disconnect();
526
- this._input.connect( this._stereo ? this._splitter : this._analyzer[0] );
527
- this._analyzer[0].disconnect();
528
- if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
529
- this._analyzer[0].connect( this._stereo ? this._merger : this._output );
530
-
531
- // update properties affected by stereo
532
- this._calcAux();
533
- this._createScales();
534
- this._calcLeds();
535
- this._makeGrad();
562
+ deprecate( 'stereo', 'channelLayout' );
563
+ this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
536
564
  }
537
565
 
538
566
  get volume() {
@@ -661,7 +689,7 @@ export default class AudioMotionAnalyzer {
661
689
  // when connecting the first node, also connect the analyzer nodes to the merger / output nodes
662
690
  if ( this._outNodes.length == 1 ) {
663
691
  for ( const i of [0,1] )
664
- this._analyzer[ i ].connect( ( ! this._stereo && ! i ? this._output : this._merger ), 0, i );
692
+ this._analyzer[ i ].connect( ( this._chLayout == CHANNEL_SINGLE && ! i ? this._output : this._merger ), 0, i );
665
693
  }
666
694
  }
667
695
 
@@ -746,7 +774,7 @@ export default class AudioMotionAnalyzer {
746
774
 
747
775
  const startBin = this._freqToBin( startFreq ),
748
776
  endBin = endFreq ? this._freqToBin( endFreq ) : startBin,
749
- chnCount = this._stereo + 1;
777
+ chnCount = this._chLayout == CHANNEL_SINGLE ? 1 : 2;
750
778
 
751
779
  let energy = 0;
752
780
  for ( let channel = 0; channel < chnCount; channel++ ) {
@@ -770,7 +798,7 @@ export default class AudioMotionAnalyzer {
770
798
  if ( typeof options !== 'object' )
771
799
  throw new AudioMotionError( ERR_GRADIENT_NOT_AN_OBJECT );
772
800
 
773
- if ( options.colorStops === undefined || options.colorStops.length < 2 )
801
+ if ( ! Array.isArray( options.colorStops ) || ! options.colorStops.length )
774
802
  throw new AudioMotionError( ERR_GRADIENT_MISSING_COLOR );
775
803
 
776
804
  this._gradients[ name ] = {
@@ -779,8 +807,8 @@ export default class AudioMotionAnalyzer {
779
807
  colorStops: options.colorStops
780
808
  };
781
809
 
782
- // if the registered gradient is the current one, regenerate it
783
- if ( name == this._gradient )
810
+ // if the registered gradient is one of the currently selected gradients, regenerate them
811
+ if ( this._gradientNames.includes( name ) )
784
812
  this._makeGrad();
785
813
  }
786
814
 
@@ -918,10 +946,10 @@ export default class AudioMotionAnalyzer {
918
946
  _calcAux() {
919
947
  const canvas = this.canvas,
920
948
  isRadial = this._radial,
921
- isDual = this._stereo && ! isRadial,
949
+ isDual = this._chLayout == CHANNEL_VERTICAL && ! isRadial,
922
950
  centerX = canvas.width >> 1;
923
951
 
924
- this._radius = Math.min( canvas.width, canvas.height ) * ( this._stereo ? .375 : .125 ) | 0;
952
+ this._radius = Math.min( canvas.width, canvas.height ) * ( this._chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
925
953
  this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
926
954
  this._isBandsMode = this._mode % 10 != 0;
927
955
  this._isOctaveBands = this._isBandsMode && this._frequencyScale == SCALE_LOG;
@@ -929,7 +957,7 @@ export default class AudioMotionAnalyzer {
929
957
  this._isLumiBars = this._lumiBars && this._isBandsMode && ! isRadial;
930
958
  this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
931
959
  this._isOutline = this._outlineBars && this._isBandsMode && ! this._isLumiBars && ! this._isLedDisplay;
932
- this._maximizeLeds = ! this._stereo || this._reflexRatio > 0 && ! this._isLumiBars;
960
+ this._maximizeLeds = this._chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! this._isLumiBars;
933
961
 
934
962
  this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
935
963
  this._analyzerHeight = this._channelHeight * ( this._isLumiBars || isRadial ? 1 : 1 - this._reflexRatio ) | 0;
@@ -1221,7 +1249,7 @@ export default class AudioMotionAnalyzer {
1221
1249
  freqLabels = [],
1222
1250
  frequencyScale= this._frequencyScale,
1223
1251
  initialX = this._initialX,
1224
- isStereo = this._stereo,
1252
+ isDual = this._chLayout == CHANNEL_VERTICAL,
1225
1253
  isMirror = this._mirror,
1226
1254
  isNoteLabels = this._noteLabels,
1227
1255
  scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
@@ -1253,14 +1281,14 @@ export default class AudioMotionAnalyzer {
1253
1281
  }
1254
1282
 
1255
1283
  // in radial stereo mode, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
1256
- canvasR.width = canvasR.height = ( this._radius << 1 ) + ( isStereo * scaleHeight );
1284
+ canvasR.width = canvasR.height = ( this._radius << 1 ) + ( isDual * scaleHeight );
1257
1285
 
1258
1286
  const radius = canvasR.width >> 1, // this is also used as the center X and Y coordinates of the circular scale canvas
1259
1287
  radialY = radius - scaleHeight * .7; // vertical position of text labels in the circular scale
1260
1288
 
1261
1289
  // helper function
1262
1290
  const radialLabel = ( x, label ) => {
1263
- if ( isNoteLabels && ! isStereo && ! ['C','E','G'].includes( label[0] ) )
1291
+ if ( isNoteLabels && ! isDual && ! ['C','E','G'].includes( label[0] ) )
1264
1292
  return;
1265
1293
 
1266
1294
  const angle = TAU * ( x / canvas.width ),
@@ -1338,7 +1366,7 @@ export default class AudioMotionAnalyzer {
1338
1366
  isBandsMode = this._isBandsMode,
1339
1367
  isOutline = this._isOutline,
1340
1368
  isRadial = this._radial,
1341
- isStereo = this._stereo,
1369
+ channelLayout = this._chLayout,
1342
1370
  lineWidth = +this.lineWidth, // make sure the damn thing is a number!
1343
1371
  mirrorMode = this._mirror,
1344
1372
  channelHeight = this._channelHeight,
@@ -1434,7 +1462,7 @@ export default class AudioMotionAnalyzer {
1434
1462
  const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
1435
1463
 
1436
1464
  // select background color
1437
- const bgColor = ( ! this.showBgColor || isLedDisplay && ! this.overlay ) ? '#000' : this._gradients[ this._gradient ].bgColor;
1465
+ const bgColor = ( ! this.showBgColor || isLedDisplay && ! this.overlay ) ? '#000' : this._gradients[ this._gradientNames[0] ].bgColor;
1438
1466
 
1439
1467
  // compute the effective bar width, considering the selected bar spacing
1440
1468
  // if led effect is active, ensure at least the spacing from led definitions
@@ -1446,18 +1474,19 @@ export default class AudioMotionAnalyzer {
1446
1474
 
1447
1475
  let currentEnergy = 0;
1448
1476
 
1449
- const nBars = this._bars.length;
1477
+ const nBars = this._bars.length,
1478
+ nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
1450
1479
 
1451
- for ( let channel = 0; channel < isStereo + 1; channel++ ) {
1480
+ for ( let channel = 0; channel < nChannels; channel++ ) {
1452
1481
 
1453
- const channelTop = channelHeight * channel + channelGap * channel,
1482
+ const channelTop = channelLayout == CHANNEL_VERTICAL ? channelHeight * channel + channelGap * channel : 0,
1454
1483
  channelBottom = channelTop + channelHeight,
1455
1484
  analyzerBottom = channelTop + analyzerHeight - ( isLedDisplay && ! this._maximizeLeds ? ledSpaceV : 0 );
1456
1485
 
1457
1486
  if ( useCanvas ) {
1458
1487
  // clear the channel area, if in overlay mode
1459
1488
  // this is done per channel to clear any residue below 0 off the top channel (especially in line graph mode with lineWidth > 1)
1460
- if ( this.overlay )
1489
+ if ( this.overlay && ( ! isRadial || channel == 0 ) )
1461
1490
  ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1462
1491
 
1463
1492
  // fill the analyzer background if needed (not overlay or overlay + showBgColor)
@@ -1468,7 +1497,7 @@ export default class AudioMotionAnalyzer {
1468
1497
  ctx.fillStyle = bgColor;
1469
1498
 
1470
1499
  // exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
1471
- if ( ! isRadial || channel == 0 )
1500
+ if ( ! isRadial && channelLayout != CHANNEL_COMBINED || channel == 0 )
1472
1501
  ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( this.overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
1473
1502
 
1474
1503
  ctx.globalAlpha = 1;
@@ -1527,7 +1556,7 @@ export default class AudioMotionAnalyzer {
1527
1556
  ctx.lineWidth = isOutline ? Math.min( lineWidth, width / 2 ) : lineWidth;
1528
1557
 
1529
1558
  // set selected gradient for fill and stroke
1530
- ctx.fillStyle = ctx.strokeStyle = this._canvasGradient;
1559
+ ctx.fillStyle = ctx.strokeStyle = this._canvasGradients[ channel ];
1531
1560
  } // if ( useCanvas )
1532
1561
 
1533
1562
  // get a new array of data from the FFT
@@ -1599,7 +1628,7 @@ export default class AudioMotionAnalyzer {
1599
1628
  barHeight = isLedDisplay ? ledPosY( barHeight ) : barHeight * maxBarHeight | 0;
1600
1629
 
1601
1630
  // invert bar for radial channel 1
1602
- if ( isRadial && channel == 1 )
1631
+ if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
1603
1632
  barHeight *= -1;
1604
1633
 
1605
1634
  // bar width may need small adjustments for some bars, when barSpace == 0
@@ -1611,7 +1640,7 @@ export default class AudioMotionAnalyzer {
1611
1640
  if ( mode == 10 ) {
1612
1641
  // compute the average between the initial bar (i==0) and the next one
1613
1642
  // used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
1614
- const nextBarAvg = i ? 0 : ( this._normalizedB( fftData[ this._bars[1].binLo ] ) * maxBarHeight * ( ! isRadial || ! channel || - 1 ) + barHeight ) / 2;
1643
+ const nextBarAvg = i ? 0 : ( this._normalizedB( fftData[ this._bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
1615
1644
 
1616
1645
  if ( isRadial ) {
1617
1646
  if ( i == 0 )
@@ -1715,7 +1744,7 @@ export default class AudioMotionAnalyzer {
1715
1744
  else if ( ! isRadial )
1716
1745
  ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
1717
1746
  else if ( mode != 10 ) // radial - no peaks for mode 10
1718
- radialPoly( posX, peak * maxBarHeight * ( ! channel || -1 ), adjWidth, -2 );
1747
+ radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), adjWidth, -2 );
1719
1748
  }
1720
1749
 
1721
1750
  } // for ( let i = 0; i < nBars; i++ )
@@ -1761,8 +1790,8 @@ export default class AudioMotionAnalyzer {
1761
1790
  // Reflex effect
1762
1791
  if ( this._reflexRatio > 0 && ! isLumiBars ) {
1763
1792
  let posY, height;
1764
- if ( this.reflexFit || isStereo ) { // always fit reflex in stereo mode
1765
- posY = isStereo && channel == 0 ? channelHeight + channelGap : 0;
1793
+ if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
1794
+ posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
1766
1795
  height = channelHeight - analyzerHeight;
1767
1796
  }
1768
1797
  else {
@@ -1785,10 +1814,10 @@ export default class AudioMotionAnalyzer {
1785
1814
  ctx.globalAlpha = 1;
1786
1815
  }
1787
1816
 
1788
- } // for ( let channel = 0; channel < isStereo + 1; channel++ ) {
1817
+ } // for ( let channel = 0; channel < nChannels; channel++ ) {
1789
1818
 
1790
1819
  // Update energy
1791
- energy.val = currentEnergy / ( nBars << isStereo );
1820
+ energy.val = currentEnergy / ( nBars << ( nChannels - 1 ) );
1792
1821
  if ( energy.val >= energy.peak ) {
1793
1822
  energy.peak = energy.val;
1794
1823
  energy.hold = 30;
@@ -1847,7 +1876,7 @@ export default class AudioMotionAnalyzer {
1847
1876
  // call callback function, if defined
1848
1877
  if ( this.onCanvasDraw ) {
1849
1878
  ctx.save();
1850
- ctx.fillStyle = ctx.strokeStyle = this._canvasGradient;
1879
+ ctx.fillStyle = ctx.strokeStyle = this._canvasGradients[0];
1851
1880
  this.onCanvasDraw( this );
1852
1881
  ctx.restore();
1853
1882
  }
@@ -1893,8 +1922,9 @@ export default class AudioMotionAnalyzer {
1893
1922
  const ctx = this._canvasCtx,
1894
1923
  canvas = ctx.canvas,
1895
1924
  isLumiBars = this._isLumiBars,
1896
- gradientHeight = isLumiBars ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ! this._stereo ) | 0,
1897
- // for stereo we keep the full canvas height and handle the reflex areas while generating the color stops
1925
+ isRadial = this._radial,
1926
+ gradientHeight = isLumiBars ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( this._chLayout != CHANNEL_VERTICAL ) ) | 0,
1927
+ // for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
1898
1928
  analyzerRatio = 1 - this._reflexRatio,
1899
1929
  initialX = this._initialX;
1900
1930
 
@@ -1904,70 +1934,72 @@ export default class AudioMotionAnalyzer {
1904
1934
  maxRadius = Math.min( centerX, centerY ),
1905
1935
  radius = this._radius;
1906
1936
 
1907
- const currGradient = this._gradients[ this._gradient ],
1908
- colorStops = currGradient.colorStops,
1909
- isHorizontal = currGradient.dir == 'h';
1910
-
1911
- let grad;
1937
+ for ( let chn = 0; chn < 2; chn++ ) {
1938
+ const currGradient = this._gradients[ this._gradientNames[ chn ] ],
1939
+ colorStops = currGradient.colorStops,
1940
+ isHorizontal = currGradient.dir == 'h';
1912
1941
 
1913
- if ( this._radial )
1914
- grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * this._stereo );
1915
- else
1916
- grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + this._analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
1942
+ let grad;
1917
1943
 
1918
- if ( colorStops ) {
1919
- const dual = this._stereo && ! this._splitGradient && ! isHorizontal;
1944
+ if ( isRadial )
1945
+ grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( this._chLayout == CHANNEL_VERTICAL ) );
1946
+ else
1947
+ grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + this._analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
1920
1948
 
1921
- // helper function
1922
- const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
1949
+ if ( colorStops ) {
1950
+ const dual = this._chLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
1923
1951
 
1924
- for ( let channel = 0; channel < 1 + dual; channel++ ) {
1925
- colorStops.forEach( ( colorInfo, index ) => {
1952
+ // helper function
1953
+ const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
1926
1954
 
1927
- const maxIndex = colorStops.length - 1;
1955
+ for ( let channel = 0; channel < 1 + dual; channel++ ) {
1956
+ colorStops.forEach( ( colorInfo, index ) => {
1928
1957
 
1929
- let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / maxIndex;
1958
+ const maxIndex = colorStops.length - 1;
1930
1959
 
1931
- // in dual mode (not split), use half the original offset for each channel
1932
- if ( dual )
1933
- offset /= 2;
1960
+ let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / Math.max( 1, maxIndex );
1934
1961
 
1935
- // constrain the offset within the useful analyzer areas (avoid reflex areas)
1936
- if ( this._stereo && ! isLumiBars && ! this._radial && ! isHorizontal ) {
1937
- offset *= analyzerRatio;
1938
- // skip the first reflex area in split mode
1939
- if ( ! dual && offset > .5 * analyzerRatio )
1940
- offset += .5 * this._reflexRatio;
1941
- }
1962
+ // in dual mode (not split), use half the original offset for each channel
1963
+ if ( dual )
1964
+ offset /= 2;
1942
1965
 
1943
- // only for split mode
1944
- if ( channel == 1 ) {
1945
- // add colors in reverse order if radial or lumi are active
1946
- if ( this._radial || isLumiBars ) {
1947
- const revIndex = maxIndex - index;
1948
- colorInfo = colorStops[ revIndex ];
1949
- offset = 1 - ( colorInfo.pos !== undefined ? colorInfo.pos : revIndex / maxIndex ) / 2;
1966
+ // constrain the offset within the useful analyzer areas (avoid reflex areas)
1967
+ if ( this._chLayout == CHANNEL_VERTICAL && ! isLumiBars && ! isRadial && ! isHorizontal ) {
1968
+ offset *= analyzerRatio;
1969
+ // skip the first reflex area in split mode
1970
+ if ( ! dual && offset > .5 * analyzerRatio )
1971
+ offset += .5 * this._reflexRatio;
1950
1972
  }
1951
- else {
1952
- // if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
1953
- if ( index == 0 && offset > 0 )
1954
- addColorStop( .5, colorInfo );
1955
- // bump the offset to the second half of the gradient
1956
- offset += .5;
1973
+
1974
+ // only for non-split gradient
1975
+ if ( channel == 1 ) {
1976
+ // add colors in reverse order if radial or lumi are active
1977
+ if ( isRadial || isLumiBars ) {
1978
+ const revIndex = maxIndex - index;
1979
+ colorInfo = colorStops[ revIndex ];
1980
+ offset = 1 - ( colorInfo.pos !== undefined ? colorInfo.pos : revIndex / Math.max( 1, maxIndex ) ) / 2;
1981
+ }
1982
+ else {
1983
+ // if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
1984
+ if ( index == 0 && offset > 0 )
1985
+ addColorStop( .5, colorInfo );
1986
+ // bump the offset to the second half of the gradient
1987
+ offset += .5;
1988
+ }
1957
1989
  }
1958
- }
1959
1990
 
1960
- // add gradient color stop
1961
- addColorStop( offset, colorInfo );
1991
+ // add gradient color stop
1992
+ addColorStop( offset, colorInfo );
1962
1993
 
1963
- // create additional color stop at the end of first channel to prevent bleeding
1964
- if ( this._stereo && index == maxIndex && offset < .5 )
1965
- addColorStop( .5, colorInfo );
1966
- });
1994
+ // create additional color stop at the end of first channel to prevent bleeding
1995
+ if ( this._chLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
1996
+ addColorStop( .5, colorInfo );
1997
+ });
1998
+ }
1967
1999
  }
1968
- }
1969
2000
 
1970
- this._canvasGradient = grad;
2001
+ this._canvasGradients[ chn ] = grad;
2002
+ } // for (chn)
1971
2003
  }
1972
2004
 
1973
2005
  /**
@@ -2061,6 +2093,25 @@ export default class AudioMotionAnalyzer {
2061
2093
  this.onCanvasResize( reason, this );
2062
2094
  }
2063
2095
 
2096
+ /**
2097
+ * Select a gradient for one or both channels
2098
+ *
2099
+ * @param {string} name gradient name
2100
+ * @param [{number}] desired channel (0 or 1) - if empty or invalid, sets both channels
2101
+ */
2102
+ _setGradient( name, channel ) {
2103
+ if ( ! this._gradients.hasOwnProperty( name ) )
2104
+ throw new AudioMotionError( ERR_UNKNOWN_GRADIENT, name );
2105
+
2106
+ if ( ! [0,1].includes( channel ) ) {
2107
+ this._gradientNames[1] = name;
2108
+ channel = 0;
2109
+ }
2110
+
2111
+ this._gradientNames[ channel ] = name;
2112
+ this._makeGrad();
2113
+ }
2114
+
2064
2115
  /**
2065
2116
  * Set object properties
2066
2117
  */
@@ -2072,6 +2123,7 @@ export default class AudioMotionAnalyzer {
2072
2123
  ansiBands : false,
2073
2124
  barSpace : 0.1,
2074
2125
  bgAlpha : 0.7,
2126
+ channelLayout : CHANNEL_SINGLE,
2075
2127
  fftSize : 8192,
2076
2128
  fillAlpha : 1,
2077
2129
  frequencyScale : SCALE_LOG,
@@ -2105,7 +2157,6 @@ export default class AudioMotionAnalyzer {
2105
2157
  spinSpeed : 0,
2106
2158
  splitGradient : false,
2107
2159
  start : true,
2108
- stereo : false,
2109
2160
  useCanvas : true,
2110
2161
  volume : 1,
2111
2162
  weightingFilter: FILTER_NONE
@@ -2114,8 +2165,11 @@ export default class AudioMotionAnalyzer {
2114
2165
  // callback functions properties
2115
2166
  const callbacks = [ 'onCanvasDraw', 'onCanvasResize' ];
2116
2167
 
2168
+ // properties undefined by default
2169
+ const defaultUndefined = [ 'gradientLeft', 'gradientRight', 'height', 'width', 'stereo' ];
2170
+
2117
2171
  // build an array of valid properties; `start` is not an actual property and is handled after setting everything else
2118
- const validProps = Object.keys( defaults ).filter( e => e != 'start' ).concat( callbacks, ['height', 'width'] );
2172
+ const validProps = Object.keys( defaults ).filter( e => e != 'start' ).concat( callbacks, defaultUndefined );
2119
2173
 
2120
2174
  if ( useDefaults || options === undefined )
2121
2175
  options = { ...defaults, ...options }; // merge options with defaults
package/src/index.d.ts CHANGED
@@ -11,10 +11,13 @@ export interface Options {
11
11
  ansiBands?: boolean;
12
12
  barSpace?: number;
13
13
  bgAlpha?: number;
14
+ channelLayout?: ChannelLayout;
14
15
  fftSize?: number;
15
16
  fillAlpha?: number;
16
17
  frequencyScale?: FrequencyScale;
17
18
  gradient?: string;
19
+ gradientLeft?: string;
20
+ gradientRight?: string;
18
21
  height?: number;
19
22
  ledBars?: boolean;
20
23
  linearAmplitude?: boolean;
@@ -71,6 +74,8 @@ export interface ConstructorOptions extends Options {
71
74
  source?: HTMLMediaElement | AudioNode;
72
75
  }
73
76
 
77
+ export type ChannelLayout = "single" | "dualVertical" | "dualCombined";
78
+
74
79
  export type EnergyPreset = "peak" | "bass" | "lowMid" | "mid" | "highMid" | "treble";
75
80
 
76
81
  export type FrequencyScale = "bark" | "linear" | "log" | "mel";
@@ -79,15 +84,10 @@ export type GradientColorStop = string | { pos: number; color: string };
79
84
 
80
85
  export type WeightingFilter = "" | "A" | "B" | "C" | "D" | "468";
81
86
 
82
- type ArrayTwoOrMore<T> = {
83
- 0: T
84
- 1: T
85
- } & Array<T>;
86
-
87
87
  export interface GradientOptions {
88
88
  bgColor: string;
89
89
  dir?: "h";
90
- colorStops: ArrayTwoOrMore<GradientColorStop>
90
+ colorStops: GradientColorStop[];
91
91
  }
92
92
 
93
93
  export interface LedParameters {
@@ -114,6 +114,9 @@ declare class AudioMotionAnalyzer {
114
114
 
115
115
  public bgAlpha: number;
116
116
 
117
+ get channelLayout(): ChannelLayout;
118
+ set channelLayout(value: ChannelLayout);
119
+
117
120
  get connectedSources(): AudioNode[];
118
121
  get connectedTo(): AudioNode[];
119
122
 
@@ -133,6 +136,12 @@ declare class AudioMotionAnalyzer {
133
136
  get gradient(): string;
134
137
  set gradient(value: string);
135
138
 
139
+ get gradientLeft(): string;
140
+ set gradientLeft(value: string);
141
+
142
+ get gradientRight(): string;
143
+ set gradientRight(value: string);
144
+
136
145
  get height(): number;
137
146
  set height(h: number);
138
147