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 +82 -41
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +225 -156
- package/src/index.d.ts +15 -6
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), \~
|
|
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
|
  [audioCtx](#audioctx-audiocontext-object): *undefined*, // constructor only<br>
|
|
120
120
|
  [barSpace](#barspace-number): **0.1**,<br>
|
|
121
121
|
  [bgAlpha](#bgalpha-number): **0.7**,<br>
|
|
122
|
+
  [channelLayout](#channellayout-string): **'single'**,<br>
|
|
122
123
|
  [connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
|
|
123
124
|
  [fftSize](#fftsize-number): **8192**,<br>
|
|
124
125
|
  [fillAlpha](#fillalpha-number): **1**,<br>
|
|
125
126
|
  [frequencyScale](#frequencyscale-string): **'log'**,<br>
|
|
126
127
|
  [fsElement](#fselement-htmlelement-object): *undefined*, // constructor only<br>
|
|
127
128
|
  [gradient](#gradient-string): **'classic'**,<br>
|
|
129
|
+
  [gradientLeft](#gradientleft-string): *undefined*,<br>
|
|
130
|
+
  [gradientRight](#gradientright-string): *undefined*,<br>
|
|
128
131
|
  [height](#height-number): *undefined*,<br>
|
|
129
132
|
  [ledBars](#ledbars-boolean): **false**,<br>
|
|
130
133
|
  [linearAmplitude](#linearamplitude-boolean): **false**,<br>
|
|
@@ -158,7 +161,6 @@ options = {<br>
|
|
|
158
161
|
  [spinSpeed](#spinspeed-number): **0**,<br>
|
|
159
162
|
  [splitGradient](#splitgradient-boolean): **false**,<br>
|
|
160
163
|
  [start](#start-boolean): **true**,<br>
|
|
161
|
-
  [stereo](#stereo-boolean): **false**,<br>
|
|
162
164
|
  [useCanvas](#usecanvas-boolean): **true**,<br>
|
|
163
165
|
  [volume](#volume-number): **1**,<br>
|
|
164
166
|
  [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)
|
|
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 [
|
|
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' | 
|
|
429
|
+
'orangered' | 
|
|
430
|
+
'prism' | 
|
|
431
|
+
'rainbow' | 
|
|
432
|
+
'steelblue' | 
|
|
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
|
|
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
|
-
|
|
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 [`
|
|
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
|
|
829
|
+
When set to *true* and [`channelLayout`](#channellayout-string) is **_dual-vertical_**, the gradient will be split between channels.
|
|
780
830
|
|
|
781
|
-
|
|
831
|
+
When *false*, both channels will use the full gradient.
|
|
832
|
+
|
|
833
|
+
| gradient: *'classic'* - splitGradient: *false* | gradient: *'classic'* - splitGradient: *true* |
|
|
782
834
|
|:--:|:--:|
|
|
783
|
-
|  |  |
|
|
784
836
|
|
|
785
|
-
This option has no effect on horizontal gradients,
|
|
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
|
-
|
|
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 [`
|
|
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
|
|
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
|
|
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% )'
|
|
1098
|
+
'hsl( 120, 100%, 50% )'
|
|
1057
1099
|
]
|
|
1058
1100
|
}
|
|
1059
1101
|
|
|
1060
1102
|
audioMotion.registerGradient( 'myGradient', options );
|
|
1061
1103
|
```
|
|
1062
1104
|
|
|
1063
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
##
|
|
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
|
-
|
|
1310
|
+
For **bug reports** and **feature requests**, feel free to [open an issue](https://github.com/hvianna/audioMotion-analyzer/issues).
|
|
1270
1311
|
|
|
1271
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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.
|
|
368
|
+
return this._selectedGrads[0];
|
|
329
369
|
}
|
|
330
370
|
set gradient( value ) {
|
|
331
|
-
|
|
332
|
-
|
|
371
|
+
this._setGradient( value );
|
|
372
|
+
}
|
|
333
373
|
|
|
334
|
-
|
|
335
|
-
this.
|
|
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
|
-
|
|
569
|
+
deprecate( 'stereo', 'channelLayout' );
|
|
570
|
+
return this._chLayout != CHANNEL_SINGLE;
|
|
520
571
|
}
|
|
521
572
|
set stereo( value ) {
|
|
522
|
-
|
|
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( (
|
|
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.
|
|
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
|
|
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
|
|
783
|
-
if (
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
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 ) + (
|
|
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 && !
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
1485
|
+
if ( barSpace == 0 && ! isLedDisplay )
|
|
1445
1486
|
width |= 0;
|
|
1446
1487
|
|
|
1447
1488
|
let currentEnergy = 0;
|
|
1448
1489
|
|
|
1449
|
-
const nBars
|
|
1490
|
+
const nBars = this._bars.length,
|
|
1491
|
+
nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
|
|
1450
1492
|
|
|
1451
|
-
for ( let channel = 0; 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 (
|
|
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 ( !
|
|
1465
|
-
if (
|
|
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 (
|
|
1472
|
-
ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, (
|
|
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.
|
|
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 =
|
|
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 * (
|
|
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,
|
|
1689
|
+
posX += Math.max( ledSpaceH / 2, barSpacePx / 2 );
|
|
1646
1690
|
else {
|
|
1647
|
-
if (
|
|
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 +=
|
|
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 (
|
|
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 * (
|
|
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 (
|
|
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 =
|
|
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 ||
|
|
1765
|
-
posY =
|
|
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 <
|
|
1832
|
+
} // for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1789
1833
|
|
|
1790
1834
|
// Update energy
|
|
1791
|
-
energy.val = currentEnergy / ( nBars <<
|
|
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.
|
|
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
|
-
|
|
1897
|
-
|
|
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
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
1919
|
-
const dual = this._stereo && ! this._splitGradient && ! isHorizontal;
|
|
1958
|
+
let grad;
|
|
1920
1959
|
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|
-
|
|
1925
|
-
|
|
1965
|
+
if ( colorStops ) {
|
|
1966
|
+
const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
|
|
1926
1967
|
|
|
1927
|
-
|
|
1968
|
+
// helper function
|
|
1969
|
+
const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
|
|
1928
1970
|
|
|
1929
|
-
|
|
1971
|
+
for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
|
|
1930
1972
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
offset
|
|
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
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
1961
|
-
|
|
2006
|
+
// add gradient color stop
|
|
2007
|
+
addColorStop( offset, colorInfo );
|
|
1962
2008
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
-
|
|
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 :
|
|
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,
|
|
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:
|
|
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
|
|