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

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,6 @@ 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>
162
164
  &emsp;&emsp;[useCanvas](#usecanvas-boolean): **true**,<br>
163
165
  &emsp;&emsp;[volume](#volume-number): **1**,<br>
164
166
  &emsp;&emsp;[weightingFilter](#weightingFilter-string): **''**<br>
@@ -233,7 +235,7 @@ Defaults to **true**, so the analyzer will start running right after initializat
233
235
 
234
236
  When set to *true* each bar's amplitude affects its opacity, i.e., higher bars are rendered more opaque while shorter bars are more transparent.
235
237
 
236
- This is similar to the [`lumiBars`](#lumibars-boolean) effect, but bars' amplitudes are preserved and it also works on **Discrete** [mode](#mode-number) and [radial](#radial-boolean) visualization.
238
+ This is similar to the [`lumiBars`](#lumibars-boolean) effect, but bars' amplitudes are preserved and it also works on **Discrete** [mode](#mode-number) and [radial](#radial-boolean) spectrum.
237
239
 
238
240
  For effect priority when combined with other settings, see [`isAlphaBars`](#isalphabars-boolean-read-only).
239
241
 
@@ -315,6 +317,23 @@ Defaults to **0.7**.
315
317
 
316
318
  [2D rendering context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) used for drawing in audioMotion's *Canvas*.
317
319
 
320
+ ### `channelLayout` *string*
321
+
322
+ *Available since v4.0.0*
323
+
324
+ Defines the number and layout of analyzer channels.
325
+
326
+ channelLayout | description
327
+ ----------------|------------
328
+ 'single' | Single channel analyzer, representing the combined output of both left and right channels.
329
+ 'dual-vertical' | Dual channel analyzer, with left channel shown at the top and right channel at the bottom.
330
+ 'dual-combined' | Left and right channel graphs are shown overlaid. Works best with semi-transparent **Graph** [`mode`](#mode-number) or [`outlineBars`](#outlinebars-boolean).
331
+
332
+ !> When a *dual* layout is selected, any mono (single channel) audio source connected to the analyzer will output sound only from the left speaker,
333
+ unless a stereo source is simultaneously connected to the analyzer, which will force the mono input to be upmixed to stereo.
334
+
335
+ See also [`gradientLeft`](#gradientleft-string), [`gradientRight`](#gradientright-string) and [`splitGradient`](#splitgradient-boolean).
336
+
318
337
  ### `connectedSources` *array* *(Read only)*
319
338
 
320
339
  *Available since v3.0.0*
@@ -396,10 +415,39 @@ Canvas dimensions used during fullscreen mode. These take the current pixel rati
396
415
 
397
416
  Name of the color gradient used for analyzer graphs.
398
417
 
399
- It must be a built-in or [registered](#registergradient-name-options-) gradient name. Built-in gradients are *'classic'*, *'prism'* and *'rainbow'*.
418
+ It must be a built-in or registered gradient name (see [`registerGradient()`](#registergradient-name-options-)).
419
+
420
+ `gradient` sets the gradient for both analyzer channels, but its read value represents only the gradient on the left (or single) channel.
421
+
422
+ When using a dual [`channelLayout`](#channellayout-string), use [`gradientLeft`](#gradientleft-string) and [`gradientRight`](#gradientright-string) if you want to individually set/read the gradient for each channel.
423
+
424
+ Built-in gradients are shown below:
425
+
426
+ gradient | preview
427
+ ------------|---------
428
+ 'classic' | ![classic](img/gradient-classic.png)
429
+ 'orangered' | ![orangered](img/gradient-orangered.png)
430
+ 'prism' | ![prism](img/gradient-prism.png)
431
+ 'rainbow' | ![rainbow](img/gradient-rainbow.png)
432
+ 'steelblue' | ![steelblue](img/gradient-steelblue.png)
433
+
434
+ See also [`splitGradient`](#splitgradient-boolean).
400
435
 
401
436
  Defaults to **'classic'**.
402
437
 
438
+ ### `gradientLeft` *string*
439
+ ### `gradientRight` *string*
440
+
441
+ *Available since v4.0.0*
442
+
443
+ Select gradients for the left and right analyzer channels independently, for use with a dual [`channelLayout`](#channellayout-string).
444
+
445
+ **_Single_** channel layout will use the gradient selected by `gradientLeft`.
446
+
447
+ For **_dual-combined_** channel layout or [`radial`](#radial-boolean) spectrum, only the background color defined by `gradientLeft` will be applied when [`showBgColor`](#showbgcolor-boolean) is *true*.
448
+
449
+ See also [`gradient`](#gradient-string) and [`splitGradient`](#splitgradient-boolean).
450
+
403
451
  ### `height` *number*
404
452
  ### `width` *number*
405
453
 
@@ -534,7 +582,7 @@ This is only effective for [bands modes](#mode-number).
534
582
 
535
583
  When set to *true* all analyzer bars will be displayed at full height with varying luminance (opacity, actually) instead.
536
584
 
537
- `lumiBars` takes precedence over [`alphaBars`](#alphabars-boolean) and [`outlineBars`](#outlinebars-boolean), except in [`radial`](#radial-boolean) visualization.
585
+ `lumiBars` takes precedence over [`alphaBars`](#alphabars-boolean) and [`outlineBars`](#outlinebars-boolean), except on [`radial`](#radial-boolean) spectrum.
538
586
 
539
587
  For effect priority when combined with other settings, see [`isLumiBars`](#islumibars-boolean-read-only).
540
588
 
@@ -652,10 +700,10 @@ You can refer to this value to adjust any additional drawings done in the canvas
652
700
 
653
701
  When *true*, the spectrum analyzer is rendered in a circular shape, with radial frequency bars spreading from its center.
654
702
 
655
- In radial visualization, [`ledBars`](#ledbars-boolean) and [`lumiBars`](#lumibars-boolean) effects are disabled, and
703
+ On radial spectrum, [`ledBars`](#ledbars-boolean) and [`lumiBars`](#lumibars-boolean) effects are disabled, and
656
704
  [`showPeaks`](#showpeaks-boolean) has no effect for [**Graph** mode](#mode-number).
657
705
 
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.
706
+ When [`channelLayout`](#channellayout-string) is set to *'dual-vertical'*, a larger diameter is used and the right channel bars are rendered towards the center of the analyzer.
659
707
 
660
708
  See also [`spinSpeed`](#spinspeed-number).
661
709
 
@@ -716,6 +764,8 @@ Opacity can be adjusted via [`bgAlpha`](#bgalpha-number) property, when [`overla
716
764
  If ***false***, the canvas background will be painted black when [`overlay`](#overlay-boolean) is ***false***,
717
765
  or transparent when [`overlay`](#overlay-boolean) is ***true***.
718
766
 
767
+ See also [`registerGradient()`](#registergradient-name-options-).
768
+
719
769
  Defaults to **true**.
720
770
 
721
771
  ?> Please note that when [`overlay`](#overlay-boolean) is ***false*** and [`ledBars`](#ledbars-boolean) is ***true***, the background color will always be black,
@@ -776,30 +826,21 @@ Defaults to **0**.
776
826
 
777
827
  *Available since v3.0.0*
778
828
 
779
- When *true*, the gradient will be split between both channels, so each channel will have different colors. If *false*, both channels will use the full gradient.
829
+ When set to *true* and [`channelLayout`](#channellayout-string) is **_dual-vertical_**, the gradient will be split between channels.
780
830
 
781
- | splitGradient: *true* | splitGradient: *false* |
831
+ When *false*, both channels will use the full gradient.
832
+
833
+ | gradient: *'classic'* - splitGradient: *false* | gradient: *'classic'* - splitGradient: *true* |
782
834
  |:--:|:--:|
783
- | ![split-on](img/splitGradient_on.png) | ![split-off](img/splitGradient_off.png) |
835
+ | ![split-off](img/splitGradient_off.png) | ![split-on](img/splitGradient_on.png) |
784
836
 
785
- This option has no effect on horizontal gradients, or when [`stereo`](#stereo-boolean) is set to *false*.
837
+ This option has no effect on horizontal gradients, except on [`radial`](#radial-boolean) spectrum - see note in [`registerGradient()`](#registergradient-name-options-).
786
838
 
787
839
  Defaults to **false**.
788
840
 
789
- ### `stereo` *boolean*
841
+ ### `stereo` **(DEPRECATED)** *boolean*
790
842
 
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**.
843
+ **This property will be removed in version 5** - Use [`channelLayout`](#channellayout-string) instead.
803
844
 
804
845
  ### `useCanvas` *boolean*
805
846
 
@@ -1010,7 +1051,7 @@ Returns an array with current data for each analyzer bar. Each array element is
1010
1051
 
1011
1052
  `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
1053
 
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.
1054
+ 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
1055
 
1015
1056
  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
1057
 
@@ -1043,33 +1084,29 @@ Use this method inside your callback function to create additional visual effect
1043
1084
 
1044
1085
  Registers a custom color gradient.
1045
1086
 
1046
- `name` must be a non-empty *string* that will be used to select this gradient, via the [`gradient`](#gradient-string) property.
1087
+ `name` must be a non-empty string that will be used to select this gradient, via the [`gradient`](#gradient-string) property.
1088
+
1047
1089
  `options` must be an object as shown below:
1048
1090
 
1049
1091
  ```js
1050
1092
  const options = {
1051
1093
  bgColor: '#011a35', // background color (optional) - defaults to '#111'
1052
1094
  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)
1095
+ colorStops: [ // list your gradient colors in this array (at least one color is required)
1054
1096
  'red', // colors may be defined in any valid CSS format
1055
1097
  { 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
1098
+ 'hsl( 120, 100%, 50% )'
1057
1099
  ]
1058
1100
  }
1059
1101
 
1060
1102
  audioMotion.registerGradient( 'myGradient', options );
1061
1103
  ```
1062
1104
 
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'
1105
+ Check the [built-in **_'rainbow'_** gradient](#gradient-string) for an example of horizontal gradient.
1067
1106
 
1068
- const options: GradientOptions = {
1069
-
1070
- ```
1107
+ **Note:** the horizontal flag (`dir: 'h'`) has no effect on [`radial`](#radial-boolean) spectrum, because in that mode all gradients are rendered in radial direction.
1071
1108
 
1072
- Additional information about [gradient color-stops](https://developer.mozilla.org/en-US/docs/Web/API/CanvasGradient/addColorStop).
1109
+ ?> Any gradient, including the built-in ones, may be modified by (re-)registering the same gradient name (names are case sensitive).
1073
1110
 
1074
1111
  ### `setCanvasSize( width, height )`
1075
1112
 
@@ -1174,7 +1211,7 @@ which is [currently not supported in some browsers](https://caniuse.com/#feat=md
1174
1211
 
1175
1212
  ### alphaBars and fillAlpha won't work with Radial on Firefox <!-- {docsify-ignore} -->
1176
1213
 
1177
- On Firefox, [`alphaBars`](#alphaBars-boolean) and [`fillAlpha`](#fillalpha-number) won't work with [`radial`](#radial-boolean) visualization when using hardware acceleration, due to [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1164912).
1214
+ On Firefox, [`alphaBars`](#alphaBars-boolean) and [`fillAlpha`](#fillalpha-number) won't work with [`radial`](#radial-boolean) spectrum when using hardware acceleration, due to [this bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1164912).
1178
1215
 
1179
1216
  ### Visualization of live streams won't work on Safari {docsify-ignore}
1180
1217
 
@@ -1264,11 +1301,15 @@ document.getElementById('stop').addEventListener( 'click', () => myAudio.pause()
1264
1301
  See [Changelog.md](Changelog.md)
1265
1302
 
1266
1303
 
1267
- ## Get in touch!
1304
+ ## Contributing
1305
+
1306
+ 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.
1307
+
1308
+ 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
1309
 
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
1310
+ For **bug reports** and **feature requests**, feel free to [open an issue](https://github.com/hvianna/audioMotion-analyzer/issues).
1270
1311
 
1271
- For feature requests, suggestions or feedback, please see the [CONTRIBUTING.md](CONTRIBUTING.md) file.
1312
+ If you want to submit a **Pull Request**, please branch it off the project's `develop` branch.
1272
1313
 
1273
1314
  And if you're feeling generous, maybe:
1274
1315
 
@@ -1279,5 +1320,5 @@ And if you're feeling generous, maybe:
1279
1320
 
1280
1321
  ## License
1281
1322
 
1282
- audioMotion-analyzer copyright (c) 2018-2022 [Henrique Avila Vianna](https://henriquevianna.com)<br>
1323
+ audioMotion-analyzer copyright (c) 2018-2023 [Henrique Avila Vianna](https://henriquevianna.com)<br>
1283
1324
  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.5",
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.5
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.5';
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 = 'dual-combined',
20
+ CHANNEL_SINGLE = 'single',
21
+ CHANNEL_VERTICAL = 'dual-vertical',
19
22
  GRADIENT_DEFAULT_BGCOLOR = '#111',
20
23
  FILTER_NONE = '',
21
24
  FILTER_A = 'A',
@@ -25,9 +28,6 @@ const CANVAS_BACKGROUND_COLOR = '#000',
25
28
  FILTER_468 = '468',
26
29
  FONT_FAMILY = 'sans-serif',
27
30
  FPS_COLOR = '#0f0',
28
- GRADIENT_CLASSIC = 'classic',
29
- GRADIENT_PRISM = 'prism',
30
- GRADIENT_RAINBOW = 'rainbow',
31
31
  LEDS_UNLIT_COLOR = '#7f7f7f22',
32
32
  REASON_CREATE = 'create',
33
33
  REASON_FSCHANGE = 'fschange',
@@ -44,6 +44,46 @@ const CANVAS_BACKGROUND_COLOR = '#000',
44
44
  SCALE_LOG = 'log',
45
45
  SCALE_MEL = 'mel';
46
46
 
47
+ // built-in gradients
48
+ const GRADIENTS = [
49
+ [ 'classic', {
50
+ colorStops: [
51
+ 'hsl( 0, 100%, 50% )',
52
+ { pos: .6, color: 'hsl( 60, 100%, 50% )' },
53
+ 'hsl( 120, 100%, 50% )'
54
+ ]
55
+ }],
56
+ [ 'prism', {
57
+ colorStops: [
58
+ 'hsl( 0, 100%, 50% )',
59
+ 'hsl( 60, 100%, 50% )',
60
+ 'hsl( 120, 100%, 50% )',
61
+ 'hsl( 180, 100%, 50% )',
62
+ 'hsl( 240, 100%, 50% )'
63
+ ]
64
+ }],
65
+ [ 'rainbow', {
66
+ dir: 'h',
67
+ colorStops: [
68
+ 'hsl( 0, 100%, 50% )',
69
+ 'hsl( 60, 100%, 50% )',
70
+ 'hsl( 120, 100%, 50% )',
71
+ 'hsl( 180, 100%, 47% )',
72
+ 'hsl( 240, 100%, 58% )',
73
+ 'hsl( 300, 100%, 50% )',
74
+ 'hsl( 360, 100%, 50% )'
75
+ ]
76
+ }],
77
+ [ 'orangered', {
78
+ bgColor: '#3e2f29',
79
+ colorStops: [ 'OrangeRed' ]
80
+ }],
81
+ [ 'steelblue', {
82
+ bgColor: '#222c35',
83
+ colorStops: [ 'SteelBlue' ]
84
+ }]
85
+ ];
86
+
47
87
  // custom error messages
48
88
  const ERR_AUDIO_CONTEXT_FAIL = [ 'ERR_AUDIO_CONTEXT_FAIL', 'Could not create audio context. Web Audio API not supported?' ],
49
89
  ERR_INVALID_AUDIO_CONTEXT = [ 'ERR_INVALID_AUDIO_CONTEXT', 'Provided audio context is not valid' ],
@@ -54,7 +94,7 @@ const ERR_AUDIO_CONTEXT_FAIL = [ 'ERR_AUDIO_CONTEXT_FAIL', 'Could not create
54
94
  ERR_INVALID_AUDIO_SOURCE = [ 'ERR_INVALID_AUDIO_SOURCE', 'Audio source must be an instance of HTMLMediaElement or AudioNode' ],
55
95
  ERR_GRADIENT_INVALID_NAME = [ 'ERR_GRADIENT_INVALID_NAME', 'Gradient name must be a non-empty string' ],
56
96
  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' ];
97
+ ERR_GRADIENT_MISSING_COLOR = [ 'ERR_GRADIENT_MISSING_COLOR', 'Gradient colorStops must be a non-empty array' ];
58
98
 
59
99
  class AudioMotionError extends Error {
60
100
  constructor( error, value ) {
@@ -65,6 +105,9 @@ class AudioMotionError extends Error {
65
105
  }
66
106
  }
67
107
 
108
+ // helper function
109
+ const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated. Use ${alternative} instead.` );
110
+
68
111
  // AudioMotionAnalyzer class
69
112
 
70
113
  export default class AudioMotionAnalyzer {
@@ -80,38 +123,14 @@ export default class AudioMotionAnalyzer {
80
123
 
81
124
  this._ready = false;
82
125
 
83
- // Initialize internal gradients object
84
- this._gradients = {};
126
+ // Initialize internal gradient objects
127
+ this._gradients = {}; // registered gradients
128
+ this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
129
+ this._canvasGradients = []; // actual CanvasGradient objects for channels 0 and 1
85
130
 
86
131
  // Register built-in gradients
87
- this.registerGradient( GRADIENT_CLASSIC, {
88
- colorStops: [
89
- 'hsl( 0, 100%, 50% )',
90
- { pos: .6, color: 'hsl( 60, 100%, 50% )' },
91
- 'hsl( 120, 100%, 50% )'
92
- ]
93
- });
94
- this.registerGradient( GRADIENT_PRISM, {
95
- colorStops: [
96
- 'hsl( 0, 100%, 50% )',
97
- 'hsl( 60, 100%, 50% )',
98
- 'hsl( 120, 100%, 50% )',
99
- 'hsl( 180, 100%, 50% )',
100
- 'hsl( 240, 100%, 50% )'
101
- ]
102
- });
103
- this.registerGradient( GRADIENT_RAINBOW, {
104
- dir: 'h',
105
- colorStops: [
106
- 'hsl( 0, 100%, 50% )',
107
- 'hsl( 60, 100%, 50% )',
108
- 'hsl( 120, 100%, 50% )',
109
- 'hsl( 180, 100%, 47% )',
110
- 'hsl( 240, 100%, 58% )',
111
- 'hsl( 300, 100%, 50% )',
112
- 'hsl( 360, 100%, 50% )'
113
- ]
114
- });
132
+ for ( const [ name, options ] of GRADIENTS )
133
+ this.registerGradient( name, options );
115
134
 
116
135
  // Set container
117
136
  this._container = container || document.body;
@@ -147,13 +166,13 @@ export default class AudioMotionAnalyzer {
147
166
  Connection routing:
148
167
  ===================
149
168
 
150
- for STEREO: +---> analyzer[0] ---+
169
+ for dual channel modes: +---> analyzer[0] ---+
151
170
  | |
152
171
  (source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
153
172
  | |
154
173
  +---> analyzer[1] ---+
155
174
 
156
- for MONO:
175
+ for single channel mode:
157
176
 
158
177
  (source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
159
178
 
@@ -303,6 +322,27 @@ export default class AudioMotionAnalyzer {
303
322
  this._calcAux();
304
323
  }
305
324
 
325
+ get channelLayout() {
326
+ return this._chLayout;
327
+ }
328
+ set channelLayout( value ) {
329
+ const LAYOUTS = [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ];
330
+ this._chLayout = LAYOUTS[ Math.max( 0, LAYOUTS.indexOf( ( '' + value ).toLowerCase() ) ) ];
331
+
332
+ // update node connections
333
+ this._input.disconnect();
334
+ this._input.connect( this._chLayout != CHANNEL_SINGLE ? this._splitter : this._analyzer[0] );
335
+ this._analyzer[0].disconnect();
336
+ if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
337
+ this._analyzer[0].connect( this._chLayout != CHANNEL_SINGLE ? this._merger : this._output );
338
+
339
+ // update properties affected by channel layout
340
+ this._calcAux();
341
+ this._createScales();
342
+ this._calcLeds();
343
+ this._makeGrad();
344
+ }
345
+
306
346
  get fftSize() {
307
347
  return this._analyzer[0].fftSize;
308
348
  }
@@ -325,14 +365,24 @@ export default class AudioMotionAnalyzer {
325
365
  }
326
366
 
327
367
  get gradient() {
328
- return this._gradient;
368
+ return this._selectedGrads[0];
329
369
  }
330
370
  set gradient( value ) {
331
- if ( ! this._gradients.hasOwnProperty( value ) )
332
- throw new AudioMotionError( ERR_UNKNOWN_GRADIENT, value );
371
+ this._setGradient( value );
372
+ }
333
373
 
334
- this._gradient = value;
335
- this._makeGrad();
374
+ get gradientLeft() {
375
+ return this._selectedGrads[0];
376
+ }
377
+ set gradientLeft( value ) {
378
+ this._setGradient( value, 0 );
379
+ }
380
+
381
+ get gradientRight() {
382
+ return this._selectedGrads[1];
383
+ }
384
+ set gradientRight( value ) {
385
+ this._setGradient( value, 1 );
336
386
  }
337
387
 
338
388
  get height() {
@@ -516,23 +566,12 @@ export default class AudioMotionAnalyzer {
516
566
  }
517
567
 
518
568
  get stereo() {
519
- return this._stereo;
569
+ deprecate( 'stereo', 'channelLayout' );
570
+ return this._chLayout != CHANNEL_SINGLE;
520
571
  }
521
572
  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();
573
+ deprecate( 'stereo', 'channelLayout' );
574
+ this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
536
575
  }
537
576
 
538
577
  get volume() {
@@ -661,7 +700,7 @@ export default class AudioMotionAnalyzer {
661
700
  // when connecting the first node, also connect the analyzer nodes to the merger / output nodes
662
701
  if ( this._outNodes.length == 1 ) {
663
702
  for ( const i of [0,1] )
664
- this._analyzer[ i ].connect( ( ! this._stereo && ! i ? this._output : this._merger ), 0, i );
703
+ this._analyzer[ i ].connect( ( this._chLayout == CHANNEL_SINGLE && ! i ? this._output : this._merger ), 0, i );
665
704
  }
666
705
  }
667
706
 
@@ -746,7 +785,7 @@ export default class AudioMotionAnalyzer {
746
785
 
747
786
  const startBin = this._freqToBin( startFreq ),
748
787
  endBin = endFreq ? this._freqToBin( endFreq ) : startBin,
749
- chnCount = this._stereo + 1;
788
+ chnCount = this._chLayout == CHANNEL_SINGLE ? 1 : 2;
750
789
 
751
790
  let energy = 0;
752
791
  for ( let channel = 0; channel < chnCount; channel++ ) {
@@ -770,7 +809,7 @@ export default class AudioMotionAnalyzer {
770
809
  if ( typeof options !== 'object' )
771
810
  throw new AudioMotionError( ERR_GRADIENT_NOT_AN_OBJECT );
772
811
 
773
- if ( options.colorStops === undefined || options.colorStops.length < 2 )
812
+ if ( ! Array.isArray( options.colorStops ) || ! options.colorStops.length )
774
813
  throw new AudioMotionError( ERR_GRADIENT_MISSING_COLOR );
775
814
 
776
815
  this._gradients[ name ] = {
@@ -779,8 +818,8 @@ export default class AudioMotionAnalyzer {
779
818
  colorStops: options.colorStops
780
819
  };
781
820
 
782
- // if the registered gradient is the current one, regenerate it
783
- if ( name == this._gradient )
821
+ // if the registered gradient is one of the currently selected gradients, regenerate them
822
+ if ( this._selectedGrads.includes( name ) )
784
823
  this._makeGrad();
785
824
  }
786
825
 
@@ -918,10 +957,10 @@ export default class AudioMotionAnalyzer {
918
957
  _calcAux() {
919
958
  const canvas = this.canvas,
920
959
  isRadial = this._radial,
921
- isDual = this._stereo && ! isRadial,
960
+ isDual = this._chLayout == CHANNEL_VERTICAL && ! isRadial,
922
961
  centerX = canvas.width >> 1;
923
962
 
924
- this._radius = Math.min( canvas.width, canvas.height ) * ( this._stereo ? .375 : .125 ) | 0;
963
+ this._radius = Math.min( canvas.width, canvas.height ) * ( this._chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
925
964
  this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
926
965
  this._isBandsMode = this._mode % 10 != 0;
927
966
  this._isOctaveBands = this._isBandsMode && this._frequencyScale == SCALE_LOG;
@@ -929,7 +968,7 @@ export default class AudioMotionAnalyzer {
929
968
  this._isLumiBars = this._lumiBars && this._isBandsMode && ! isRadial;
930
969
  this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
931
970
  this._isOutline = this._outlineBars && this._isBandsMode && ! this._isLumiBars && ! this._isLedDisplay;
932
- this._maximizeLeds = ! this._stereo || this._reflexRatio > 0 && ! this._isLumiBars;
971
+ this._maximizeLeds = this._chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! this._isLumiBars;
933
972
 
934
973
  this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
935
974
  this._analyzerHeight = this._channelHeight * ( this._isLumiBars || isRadial ? 1 : 1 - this._reflexRatio ) | 0;
@@ -1221,7 +1260,7 @@ export default class AudioMotionAnalyzer {
1221
1260
  freqLabels = [],
1222
1261
  frequencyScale= this._frequencyScale,
1223
1262
  initialX = this._initialX,
1224
- isStereo = this._stereo,
1263
+ isDual = this._chLayout == CHANNEL_VERTICAL,
1225
1264
  isMirror = this._mirror,
1226
1265
  isNoteLabels = this._noteLabels,
1227
1266
  scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
@@ -1253,14 +1292,14 @@ export default class AudioMotionAnalyzer {
1253
1292
  }
1254
1293
 
1255
1294
  // 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 );
1295
+ canvasR.width = canvasR.height = ( this._radius << 1 ) + ( isDual * scaleHeight );
1257
1296
 
1258
1297
  const radius = canvasR.width >> 1, // this is also used as the center X and Y coordinates of the circular scale canvas
1259
1298
  radialY = radius - scaleHeight * .7; // vertical position of text labels in the circular scale
1260
1299
 
1261
1300
  // helper function
1262
1301
  const radialLabel = ( x, label ) => {
1263
- if ( isNoteLabels && ! isStereo && ! ['C','E','G'].includes( label[0] ) )
1302
+ if ( isNoteLabels && ! isDual && ! ['C','E','G'].includes( label[0] ) )
1264
1303
  return;
1265
1304
 
1266
1305
  const angle = TAU * ( x / canvas.width ),
@@ -1325,11 +1364,14 @@ export default class AudioMotionAnalyzer {
1325
1364
  * this is called 60 times per second by requestAnimationFrame()
1326
1365
  */
1327
1366
  _draw( timestamp ) {
1328
- const ctx = this._canvasCtx,
1367
+ const barSpace = this._barSpace,
1368
+ barSpacePx = this._barSpacePx,
1369
+ ctx = this._canvasCtx,
1329
1370
  canvas = ctx.canvas,
1330
1371
  canvasX = this._scaleX.canvas,
1331
1372
  canvasR = this._scaleR.canvas,
1332
1373
  energy = this._energy,
1374
+ fillAlpha = this.fillAlpha,
1333
1375
  mode = this._mode,
1334
1376
  isAlphaBars = this._isAlphaBars,
1335
1377
  isLedDisplay = this._isLedDisplay,
@@ -1337,8 +1379,9 @@ export default class AudioMotionAnalyzer {
1337
1379
  isLumiBars = this._isLumiBars,
1338
1380
  isBandsMode = this._isBandsMode,
1339
1381
  isOutline = this._isOutline,
1382
+ isOverlay = this.overlay,
1340
1383
  isRadial = this._radial,
1341
- isStereo = this._stereo,
1384
+ channelLayout = this._chLayout,
1342
1385
  lineWidth = +this.lineWidth, // make sure the damn thing is a number!
1343
1386
  mirrorMode = this._mirror,
1344
1387
  channelHeight = this._channelHeight,
@@ -1350,6 +1393,7 @@ export default class AudioMotionAnalyzer {
1350
1393
  centerX = canvas.width >> 1,
1351
1394
  centerY = canvas.height >> 1,
1352
1395
  radius = this._radius,
1396
+ showBgColor = this.showBgColor,
1353
1397
  maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
1354
1398
  maxdB = this.maxDecibels,
1355
1399
  mindB = this.minDecibels,
@@ -1433,43 +1477,43 @@ export default class AudioMotionAnalyzer {
1433
1477
  const [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1434
1478
  const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
1435
1479
 
1436
- // select background color
1437
- const bgColor = ( ! this.showBgColor || isLedDisplay && ! this.overlay ) ? '#000' : this._gradients[ this._gradient ].bgColor;
1438
-
1439
1480
  // compute the effective bar width, considering the selected bar spacing
1440
1481
  // if led effect is active, ensure at least the spacing from led definitions
1441
- let width = this._barWidth - ( ! isBandsMode ? 0 : Math.max( isLedDisplay ? ledSpaceH : 0, this._barSpacePx ) );
1482
+ let width = this._barWidth - ( ! isBandsMode ? 0 : Math.max( isLedDisplay ? ledSpaceH : 0, barSpacePx ) );
1442
1483
 
1443
1484
  // make sure width is integer for pixel accurate calculation, when no bar spacing is required
1444
- if ( this._barSpace == 0 && ! isLedDisplay )
1485
+ if ( barSpace == 0 && ! isLedDisplay )
1445
1486
  width |= 0;
1446
1487
 
1447
1488
  let currentEnergy = 0;
1448
1489
 
1449
- const nBars = this._bars.length;
1490
+ const nBars = this._bars.length,
1491
+ nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
1450
1492
 
1451
- for ( let channel = 0; channel < isStereo + 1; channel++ ) {
1493
+ for ( let channel = 0; channel < nChannels; channel++ ) {
1452
1494
 
1453
- const channelTop = channelHeight * channel + channelGap * channel,
1495
+ const channelTop = channelLayout == CHANNEL_VERTICAL ? channelHeight * channel + channelGap * channel : 0,
1454
1496
  channelBottom = channelTop + channelHeight,
1455
- analyzerBottom = channelTop + analyzerHeight - ( isLedDisplay && ! this._maximizeLeds ? ledSpaceV : 0 );
1497
+ analyzerBottom = channelTop + analyzerHeight - ( isLedDisplay && ! this._maximizeLeds ? ledSpaceV : 0 ),
1498
+ bgColor = ( ! showBgColor || isLedDisplay && ! isOverlay ) ? '#000' : this._gradients[ this._selectedGrads[ channel ] ].bgColor,
1499
+ mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
1456
1500
 
1457
1501
  if ( useCanvas ) {
1458
1502
  // clear the channel area, if in overlay mode
1459
1503
  // 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 )
1504
+ if ( isOverlay && mustClear )
1461
1505
  ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
1462
1506
 
1463
1507
  // fill the analyzer background if needed (not overlay or overlay + showBgColor)
1464
- if ( ! this.overlay || this.showBgColor ) {
1465
- if ( this.overlay )
1508
+ if ( ! isOverlay || showBgColor ) {
1509
+ if ( isOverlay )
1466
1510
  ctx.globalAlpha = this.bgAlpha;
1467
1511
 
1468
1512
  ctx.fillStyle = bgColor;
1469
1513
 
1470
1514
  // 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 )
1472
- ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( this.overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
1515
+ if ( mustClear )
1516
+ ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( isOverlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
1473
1517
 
1474
1518
  ctx.globalAlpha = 1;
1475
1519
  }
@@ -1527,7 +1571,7 @@ export default class AudioMotionAnalyzer {
1527
1571
  ctx.lineWidth = isOutline ? Math.min( lineWidth, width / 2 ) : lineWidth;
1528
1572
 
1529
1573
  // set selected gradient for fill and stroke
1530
- ctx.fillStyle = ctx.strokeStyle = this._canvasGradient;
1574
+ ctx.fillStyle = ctx.strokeStyle = this._canvasGradients[ channel ];
1531
1575
  } // if ( useCanvas )
1532
1576
 
1533
1577
  // get a new array of data from the FFT
@@ -1593,13 +1637,13 @@ export default class AudioMotionAnalyzer {
1593
1637
  if ( isLumiBars || isAlphaBars )
1594
1638
  ctx.globalAlpha = barHeight;
1595
1639
  else if ( isOutline )
1596
- ctx.globalAlpha = this.fillAlpha;
1640
+ ctx.globalAlpha = fillAlpha;
1597
1641
 
1598
1642
  // compute actual bar height on screen
1599
1643
  barHeight = isLedDisplay ? ledPosY( barHeight ) : barHeight * maxBarHeight | 0;
1600
1644
 
1601
1645
  // invert bar for radial channel 1
1602
- if ( isRadial && channel == 1 )
1646
+ if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
1603
1647
  barHeight *= -1;
1604
1648
 
1605
1649
  // bar width may need small adjustments for some bars, when barSpace == 0
@@ -1611,7 +1655,7 @@ export default class AudioMotionAnalyzer {
1611
1655
  if ( mode == 10 ) {
1612
1656
  // compute the average between the initial bar (i==0) and the next one
1613
1657
  // 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;
1658
+ const nextBarAvg = i ? 0 : ( this._normalizedB( fftData[ this._bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
1615
1659
 
1616
1660
  if ( isRadial ) {
1617
1661
  if ( i == 0 )
@@ -1642,9 +1686,9 @@ export default class AudioMotionAnalyzer {
1642
1686
  else {
1643
1687
  if ( mode > 0 ) {
1644
1688
  if ( isLedDisplay )
1645
- posX += Math.max( ledSpaceH / 2, this._barSpacePx / 2 );
1689
+ posX += Math.max( ledSpaceH / 2, barSpacePx / 2 );
1646
1690
  else {
1647
- if ( this._barSpace == 0 ) {
1691
+ if ( barSpace == 0 ) {
1648
1692
  posX |= 0;
1649
1693
  if ( i > 0 && posX > this._bars[ i - 1 ].posX + width ) {
1650
1694
  posX--;
@@ -1652,14 +1696,14 @@ export default class AudioMotionAnalyzer {
1652
1696
  }
1653
1697
  }
1654
1698
  else
1655
- posX += this._barSpacePx / 2;
1699
+ posX += barSpacePx / 2;
1656
1700
  }
1657
1701
  }
1658
1702
 
1659
1703
  if ( isLedDisplay ) {
1660
1704
  const x = posX + width / 2;
1661
1705
  // draw "unlit" leds
1662
- if ( this.showBgColor && ! this.overlay ) {
1706
+ if ( showBgColor && ! isOverlay ) {
1663
1707
  const alpha = ctx.globalAlpha;
1664
1708
  ctx.beginPath();
1665
1709
  ctx.moveTo( x, channelTop );
@@ -1715,7 +1759,7 @@ export default class AudioMotionAnalyzer {
1715
1759
  else if ( ! isRadial )
1716
1760
  ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
1717
1761
  else if ( mode != 10 ) // radial - no peaks for mode 10
1718
- radialPoly( posX, peak * maxBarHeight * ( ! channel || -1 ), adjWidth, -2 );
1762
+ radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), adjWidth, -2 );
1719
1763
  }
1720
1764
 
1721
1765
  } // for ( let i = 0; i < nBars; i++ )
@@ -1741,7 +1785,7 @@ export default class AudioMotionAnalyzer {
1741
1785
  if ( lineWidth > 0 )
1742
1786
  ctx.stroke();
1743
1787
 
1744
- if ( this.fillAlpha > 0 ) {
1788
+ if ( fillAlpha > 0 ) {
1745
1789
  if ( isRadial ) {
1746
1790
  // exclude the center circle from the fill area
1747
1791
  ctx.moveTo( centerX + radius, centerY );
@@ -1752,7 +1796,7 @@ export default class AudioMotionAnalyzer {
1752
1796
  ctx.lineTo( initialX, analyzerBottom );
1753
1797
  }
1754
1798
 
1755
- ctx.globalAlpha = this.fillAlpha;
1799
+ ctx.globalAlpha = fillAlpha;
1756
1800
  ctx.fill();
1757
1801
  ctx.globalAlpha = 1;
1758
1802
  }
@@ -1761,8 +1805,8 @@ export default class AudioMotionAnalyzer {
1761
1805
  // Reflex effect
1762
1806
  if ( this._reflexRatio > 0 && ! isLumiBars ) {
1763
1807
  let posY, height;
1764
- if ( this.reflexFit || isStereo ) { // always fit reflex in stereo mode
1765
- posY = isStereo && channel == 0 ? channelHeight + channelGap : 0;
1808
+ if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
1809
+ posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
1766
1810
  height = channelHeight - analyzerHeight;
1767
1811
  }
1768
1812
  else {
@@ -1785,10 +1829,10 @@ export default class AudioMotionAnalyzer {
1785
1829
  ctx.globalAlpha = 1;
1786
1830
  }
1787
1831
 
1788
- } // for ( let channel = 0; channel < isStereo + 1; channel++ ) {
1832
+ } // for ( let channel = 0; channel < nChannels; channel++ ) {
1789
1833
 
1790
1834
  // Update energy
1791
- energy.val = currentEnergy / ( nBars << isStereo );
1835
+ energy.val = currentEnergy / ( nBars << ( nChannels - 1 ) );
1792
1836
  if ( energy.val >= energy.peak ) {
1793
1837
  energy.peak = energy.val;
1794
1838
  energy.hold = 30;
@@ -1847,7 +1891,7 @@ export default class AudioMotionAnalyzer {
1847
1891
  // call callback function, if defined
1848
1892
  if ( this.onCanvasDraw ) {
1849
1893
  ctx.save();
1850
- ctx.fillStyle = ctx.strokeStyle = this._canvasGradient;
1894
+ ctx.fillStyle = ctx.strokeStyle = this._canvasGradients[0];
1851
1895
  this.onCanvasDraw( this );
1852
1896
  ctx.restore();
1853
1897
  }
@@ -1892,9 +1936,11 @@ export default class AudioMotionAnalyzer {
1892
1936
 
1893
1937
  const ctx = this._canvasCtx,
1894
1938
  canvas = ctx.canvas,
1939
+ channelLayout = this._chLayout,
1895
1940
  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
1941
+ isRadial = this._radial,
1942
+ gradientHeight = isLumiBars ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
1943
+ // for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
1898
1944
  analyzerRatio = 1 - this._reflexRatio,
1899
1945
  initialX = this._initialX;
1900
1946
 
@@ -1904,70 +1950,71 @@ export default class AudioMotionAnalyzer {
1904
1950
  maxRadius = Math.min( centerX, centerY ),
1905
1951
  radius = this._radius;
1906
1952
 
1907
- const currGradient = this._gradients[ this._gradient ],
1908
- colorStops = currGradient.colorStops,
1909
- isHorizontal = currGradient.dir == 'h';
1910
-
1911
- let grad;
1912
-
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 ] ) );
1953
+ for ( const channel of [0,1] ) {
1954
+ const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
1955
+ colorStops = currGradient.colorStops,
1956
+ isHorizontal = currGradient.dir == 'h';
1917
1957
 
1918
- if ( colorStops ) {
1919
- const dual = this._stereo && ! this._splitGradient && ! isHorizontal;
1958
+ let grad;
1920
1959
 
1921
- // helper function
1922
- const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
1960
+ if ( isRadial )
1961
+ grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( channelLayout == CHANNEL_VERTICAL ) );
1962
+ else
1963
+ grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + this._analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
1923
1964
 
1924
- for ( let channel = 0; channel < 1 + dual; channel++ ) {
1925
- colorStops.forEach( ( colorInfo, index ) => {
1965
+ if ( colorStops ) {
1966
+ const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
1926
1967
 
1927
- const maxIndex = colorStops.length - 1;
1968
+ // helper function
1969
+ const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
1928
1970
 
1929
- let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / maxIndex;
1971
+ for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
1930
1972
 
1931
- // in dual mode (not split), use half the original offset for each channel
1932
- if ( dual )
1933
- offset /= 2;
1973
+ colorStops.forEach( ( colorInfo, index ) => {
1974
+ const maxIndex = colorStops.length - 1;
1975
+ let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / Math.max( 1, maxIndex );
1934
1976
 
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
- }
1977
+ // in dual mode (not split), use half the original offset for each channel
1978
+ if ( dual )
1979
+ offset /= 2;
1942
1980
 
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;
1981
+ // constrain the offset within the useful analyzer areas (avoid reflex areas)
1982
+ if ( channelLayout == CHANNEL_VERTICAL && ! isLumiBars && ! isRadial && ! isHorizontal ) {
1983
+ offset *= analyzerRatio;
1984
+ // skip the first reflex area in split mode
1985
+ if ( ! dual && offset > .5 * analyzerRatio )
1986
+ offset += .5 * this._reflexRatio;
1950
1987
  }
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;
1988
+
1989
+ // only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
1990
+ if ( channelArea == 1 ) {
1991
+ // add colors in reverse order if radial or lumi are active
1992
+ if ( isRadial || isLumiBars ) {
1993
+ const revIndex = maxIndex - index;
1994
+ colorInfo = colorStops[ revIndex ];
1995
+ offset = 1 - ( colorInfo.pos !== undefined ? colorInfo.pos : revIndex / Math.max( 1, maxIndex ) ) / 2;
1996
+ }
1997
+ else {
1998
+ // if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
1999
+ if ( index == 0 && offset > 0 )
2000
+ addColorStop( .5, colorInfo );
2001
+ // bump the offset to the second half of the gradient
2002
+ offset += .5;
2003
+ }
1957
2004
  }
1958
- }
1959
2005
 
1960
- // add gradient color stop
1961
- addColorStop( offset, colorInfo );
2006
+ // add gradient color stop
2007
+ addColorStop( offset, colorInfo );
1962
2008
 
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
- });
2009
+ // create additional color stop at the end of first channel to prevent bleeding
2010
+ if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
2011
+ addColorStop( .5, colorInfo );
2012
+ });
2013
+ } // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
1967
2014
  }
1968
- }
1969
2015
 
1970
- this._canvasGradient = grad;
2016
+ this._canvasGradients[ channel ] = grad;
2017
+ } // for ( const channel of [0,1] )
1971
2018
  }
1972
2019
 
1973
2020
  /**
@@ -2061,6 +2108,25 @@ export default class AudioMotionAnalyzer {
2061
2108
  this.onCanvasResize( reason, this );
2062
2109
  }
2063
2110
 
2111
+ /**
2112
+ * Select a gradient for one or both channels
2113
+ *
2114
+ * @param {string} name gradient name
2115
+ * @param [{number}] desired channel (0 or 1) - if empty or invalid, sets both channels
2116
+ */
2117
+ _setGradient( name, channel ) {
2118
+ if ( ! this._gradients.hasOwnProperty( name ) )
2119
+ throw new AudioMotionError( ERR_UNKNOWN_GRADIENT, name );
2120
+
2121
+ if ( ! [0,1].includes( channel ) ) {
2122
+ this._selectedGrads[1] = name;
2123
+ channel = 0;
2124
+ }
2125
+
2126
+ this._selectedGrads[ channel ] = name;
2127
+ this._makeGrad();
2128
+ }
2129
+
2064
2130
  /**
2065
2131
  * Set object properties
2066
2132
  */
@@ -2072,10 +2138,11 @@ export default class AudioMotionAnalyzer {
2072
2138
  ansiBands : false,
2073
2139
  barSpace : 0.1,
2074
2140
  bgAlpha : 0.7,
2141
+ channelLayout : CHANNEL_SINGLE,
2075
2142
  fftSize : 8192,
2076
2143
  fillAlpha : 1,
2077
2144
  frequencyScale : SCALE_LOG,
2078
- gradient : GRADIENT_CLASSIC,
2145
+ gradient : GRADIENTS[0][0],
2079
2146
  ledBars : false,
2080
2147
  linearAmplitude: false,
2081
2148
  linearBoost : 1,
@@ -2105,7 +2172,6 @@ export default class AudioMotionAnalyzer {
2105
2172
  spinSpeed : 0,
2106
2173
  splitGradient : false,
2107
2174
  start : true,
2108
- stereo : false,
2109
2175
  useCanvas : true,
2110
2176
  volume : 1,
2111
2177
  weightingFilter: FILTER_NONE
@@ -2114,8 +2180,11 @@ export default class AudioMotionAnalyzer {
2114
2180
  // callback functions properties
2115
2181
  const callbacks = [ 'onCanvasDraw', 'onCanvasResize' ];
2116
2182
 
2183
+ // properties undefined by default
2184
+ const defaultUndefined = [ 'gradientLeft', 'gradientRight', 'height', 'width', 'stereo' ];
2185
+
2117
2186
  // 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'] );
2187
+ const validProps = Object.keys( defaults ).filter( e => e != 'start' ).concat( callbacks, defaultUndefined );
2119
2188
 
2120
2189
  if ( useDefaults || options === undefined )
2121
2190
  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" | "dual-vertical" | "dual-combined";
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