audiomotion-analyzer 4.0.0-beta.1 → 4.0.0-beta.3
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 +81 -47
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +218 -148
- package/src/index.d.ts +14 -7
package/README.md
CHANGED
|
@@ -28,17 +28,18 @@ What users are saying:
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
30
30
|
+ High-resolution real-time dual channel audio spectrum analyzer
|
|
31
|
-
+ Logarithmic frequency
|
|
32
|
-
+
|
|
33
|
-
+
|
|
34
|
-
+
|
|
31
|
+
+ Logarithmic, linear and perceptual (Bark/Mel) frequency scales, with customizable range
|
|
32
|
+
+ Visualization of discrete FFT frequencies or up to 240 frequency bands (supports ANSI and equal-tempered octave bands)
|
|
33
|
+
+ Decibel and linear amplitude scales, with customizable sensitivity
|
|
34
|
+
+ A, B, C, D and ITU-R 468 weighting filters
|
|
35
|
+
+ Optional effects: LED bars, luminance bars, mirroring and reflection, radial spectrum
|
|
35
36
|
+ Comes with 3 predefined color gradients - easily add your own!
|
|
36
37
|
+ Fullscreen support, ready for retina / HiDPI displays
|
|
37
38
|
+ Zero-dependency native ES6+ module (ESM), \~20kB minified
|
|
38
39
|
|
|
39
40
|
## Online demos
|
|
40
41
|
|
|
41
|
-
[](https://audiomotion.dev/demo/)
|
|
42
43
|
|
|
43
44
|
?> https://audiomotion.dev/demo/
|
|
44
45
|
|
|
@@ -121,11 +122,13 @@ options = {<br>
|
|
|
121
122
|
  [connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
|
|
122
123
|
  [fftSize](#fftsize-number): **8192**,<br>
|
|
123
124
|
  [fillAlpha](#fillalpha-number): **1**,<br>
|
|
125
|
+
  [frequencyScale](#frequencyscale-string): **'log'**,<br>
|
|
124
126
|
  [fsElement](#fselement-htmlelement-object): *undefined*, // constructor only<br>
|
|
125
127
|
  [gradient](#gradient-string): **'classic'**,<br>
|
|
126
128
|
  [height](#height-number): *undefined*,<br>
|
|
127
129
|
  [ledBars](#ledbars-boolean): **false**,<br>
|
|
128
130
|
  [linearAmplitude](#linearamplitude-boolean): **false**,<br>
|
|
131
|
+
  [linearBoost](#linearboost-number): **1**,<br>
|
|
129
132
|
  [lineWidth](#linewidth-number): **0**,<br>
|
|
130
133
|
  [loRes](#lores-boolean): **false**,<br>
|
|
131
134
|
  [lumiBars](#lumibars-boolean): **false**,<br>
|
|
@@ -281,7 +284,7 @@ See also the [fluid demo](/demo/fluid.html) and the [multi-instance demo](/demo/
|
|
|
281
284
|
|
|
282
285
|
*Available since v2.0.0*
|
|
283
286
|
|
|
284
|
-
Customize the spacing between bars in
|
|
287
|
+
Customize the spacing between bars in [bands modes](#mode-number).
|
|
285
288
|
|
|
286
289
|
Use a value between 0 and 1 for spacing proportional to the band width. Values >= 1 will be considered as a literal number of pixels.
|
|
287
290
|
|
|
@@ -341,13 +344,13 @@ Defaults to **8192**.
|
|
|
341
344
|
|
|
342
345
|
*Available since v2.0.0*
|
|
343
346
|
|
|
344
|
-
Opacity of the area fill in
|
|
347
|
+
Opacity of the area fill in [Graph mode](#mode-number), or inner fill of bars in [bands modes](#mode-number) when [`outlineBars`](#outlinebars-boolean) is *true*.
|
|
345
348
|
|
|
346
349
|
It must be a number between 0 (completely transparent) and 1 (completely opaque).
|
|
347
350
|
|
|
348
351
|
Please note that the line stroke (when [`lineWidth`](#linewidth-number) > 0) is always drawn at full opacity, regardless of the `fillAlpha` value.
|
|
349
352
|
|
|
350
|
-
Also, for
|
|
353
|
+
Also, for [bands modes](#mode-number), [`alphaBars`](#alphabars-boolean) set to *true* takes precedence over `fillAlpha`.
|
|
351
354
|
|
|
352
355
|
Defaults to **1**.
|
|
353
356
|
|
|
@@ -357,6 +360,25 @@ Defaults to **1**.
|
|
|
357
360
|
|
|
358
361
|
Current frame rate.
|
|
359
362
|
|
|
363
|
+
### `frequencyScale` *string*
|
|
364
|
+
|
|
365
|
+
*Available since v4.0.0*
|
|
366
|
+
|
|
367
|
+
Scale used to represent frequencies in the horizontal axis.
|
|
368
|
+
|
|
369
|
+
frequencyScale | description
|
|
370
|
+
---------------|------------
|
|
371
|
+
'bark' | [Bark scale](https://en.wikipedia.org/wiki/Bark_scale)
|
|
372
|
+
'linear' | Linear scale
|
|
373
|
+
'log' | Logarithmic scale
|
|
374
|
+
'mel' | [Mel scale](https://en.wikipedia.org/wiki/Mel_scale)
|
|
375
|
+
|
|
376
|
+
Logarithmic scale is required to visualize proper [octave bands](#mode-number) and it's also recommended when using [`noteLabels`](#notelabels-boolean).
|
|
377
|
+
|
|
378
|
+
*Bark* and *Mel* are perceptual pitch scales which provide better visualization of midrange and high frequencies, especially in the [discrete frequencies mode](#mode-number).
|
|
379
|
+
|
|
380
|
+
Defaults to **'log'**.
|
|
381
|
+
|
|
360
382
|
### `fsElement` *HTMLElement object* *(Read only)*
|
|
361
383
|
|
|
362
384
|
*Available since v3.4.0*
|
|
@@ -372,9 +394,9 @@ Canvas dimensions used during fullscreen mode. These take the current pixel rati
|
|
|
372
394
|
|
|
373
395
|
### `gradient` *string*
|
|
374
396
|
|
|
375
|
-
|
|
397
|
+
Name of the color gradient used for analyzer graphs.
|
|
376
398
|
|
|
377
|
-
It must be
|
|
399
|
+
It must be a built-in or [registered](#registergradient-name-options-) gradient name. Built-in gradients are *'classic'*, *'prism'* and *'rainbow'*.
|
|
378
400
|
|
|
379
401
|
Defaults to **'classic'**.
|
|
380
402
|
|
|
@@ -385,7 +407,7 @@ Nominal dimensions of the analyzer.
|
|
|
385
407
|
|
|
386
408
|
If one or both of these are `undefined`, the analyzer will try to adjust to the container's width and/or height.
|
|
387
409
|
If the container's width and/or height are 0 (inline elements), a reference size of **640 x 270 pixels** will be used to replace the missing dimension(s).
|
|
388
|
-
This should be considered the minimum dimensions for proper visualization of all available modes
|
|
410
|
+
This should be considered the minimum dimensions for proper visualization of all available modes and effects.
|
|
389
411
|
|
|
390
412
|
You can set both values at once using the [`setCanvasSize()`](#setcanvassize-width-height-) method.
|
|
391
413
|
|
|
@@ -396,7 +418,15 @@ You can set both values at once using the [`setCanvasSize()`](#setcanvassize-wid
|
|
|
396
418
|
*Available since v3.6.0*
|
|
397
419
|
|
|
398
420
|
***true*** when alpha bars are effectively being displayed, i.e., [`alphaBars`](#alphabars-boolean) is set to *true* and [`mode`](#mode-number) is set to discrete frequencies
|
|
399
|
-
or one of the
|
|
421
|
+
or one of the bands modes, in which case [`lumiBars`](#lumibars-boolean) must be set to *false* or [`radial`](#radial-boolean) must be set to *true*.
|
|
422
|
+
|
|
423
|
+
### `isBandsMode` *boolean* *(Read only)*
|
|
424
|
+
|
|
425
|
+
*Available since v4.0.0*
|
|
426
|
+
|
|
427
|
+
***true*** when [`mode`](#mode-number) is set to one of the bands mode (modes 1 to 8).
|
|
428
|
+
|
|
429
|
+
See also [`isOctaveBands`](#isoctavebands-boolean-read-only).
|
|
400
430
|
|
|
401
431
|
### `isFullscreen` *boolean* *(Read only)*
|
|
402
432
|
|
|
@@ -408,19 +438,19 @@ See [`toggleFullscreen()`](#togglefullscreen).
|
|
|
408
438
|
|
|
409
439
|
*Available since v3.6.0* (formerly `isLedDisplay`)
|
|
410
440
|
|
|
411
|
-
***true*** when LED bars are effectively being displayed, i.e., [`
|
|
441
|
+
***true*** when LED bars are effectively being displayed, i.e., [`isBandsMode`](#isbandsmode-boolean-read-only) is *true*, [`ledBars`](#ledBars-boolean) is set to *true* and [`radial`](#radial-boolean) is set to *false*.
|
|
412
442
|
|
|
413
443
|
### `isLumiBars` *boolean* *(Read only)*
|
|
414
444
|
|
|
415
445
|
*Available since v3.0.0*
|
|
416
446
|
|
|
417
|
-
***true*** when luminance bars are effectively being displayed, i.e., [`
|
|
447
|
+
***true*** when luminance bars are effectively being displayed, i.e., [`isBandsMode`](#isbandsmode-boolean-read-only) is *true*, [`lumiBars`](#lumibars-boolean) is set to *true* and [`radial`](#radial-boolean) is set to *false*.
|
|
418
448
|
|
|
419
449
|
### `isOctaveBands` *boolean* *(Read only)*
|
|
420
450
|
|
|
421
451
|
*Available since v3.0.0*
|
|
422
452
|
|
|
423
|
-
***true*** when [`
|
|
453
|
+
***true*** when [`isBandsMode`](#isbandsmode-boolean-read-only) is *true* and [`frequencyScale`](#frequencyscale-string) is set to *'log'*.
|
|
424
454
|
|
|
425
455
|
### `isOn` *boolean* *(Read only)*
|
|
426
456
|
|
|
@@ -432,14 +462,14 @@ See [`toggleAnalyzer()`](#toggleanalyzer-boolean-).
|
|
|
432
462
|
|
|
433
463
|
*Available since v3.6.0*
|
|
434
464
|
|
|
435
|
-
***true*** when outlined bars are effectively being displayed, i.e., [`
|
|
436
|
-
|
|
465
|
+
***true*** when outlined bars are effectively being displayed, i.e., [`isBandsMode`](#isbandsmode-boolean-read-only) is *true*, [`outlineBars`](#outlinebars-boolean) is set to *true*
|
|
466
|
+
and both [`ledBars`](#ledbars-boolean) and [`lumiBars`](#lumibars-boolean) are set to *false*, or [`radial`](#radial-boolean) is set to *true*.
|
|
437
467
|
|
|
438
468
|
### `ledBars` *boolean*
|
|
439
469
|
|
|
440
470
|
*Available since v3.6.0* (formerly `showLeds`)
|
|
441
471
|
|
|
442
|
-
*true* to activate
|
|
472
|
+
*true* to activate the vintage LED bars effect for [bands modes](#mode-number).
|
|
443
473
|
|
|
444
474
|
This effect can be customized via [`setLedParams()`](#setledparams-params-) method.
|
|
445
475
|
|
|
@@ -473,7 +503,7 @@ Defaults to **1**.
|
|
|
473
503
|
|
|
474
504
|
*Available since v2.0.0*
|
|
475
505
|
|
|
476
|
-
Line width for
|
|
506
|
+
Line width for [Graph mode](#mode-number), or outline stroke in [bands modes](#mode-number) when [`outlineBars`](#outlinebars-boolean) is *true*.
|
|
477
507
|
|
|
478
508
|
For the line to be distinguishable, set also [`fillAlpha`](#fillalpha-number) < 1.
|
|
479
509
|
|
|
@@ -500,7 +530,7 @@ This will prevent the canvas size from changing, when switching the low resoluti
|
|
|
500
530
|
|
|
501
531
|
*Available since v1.1.0*
|
|
502
532
|
|
|
503
|
-
This is only effective for
|
|
533
|
+
This is only effective for [bands modes](#mode-number).
|
|
504
534
|
|
|
505
535
|
When set to *true* all analyzer bars will be displayed at full height with varying luminance (opacity, actually) instead.
|
|
506
536
|
|
|
@@ -553,26 +583,26 @@ Defaults to **0**.
|
|
|
553
583
|
|
|
554
584
|
### `mode` *number*
|
|
555
585
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
+ **Discrete** mode provides the highest resolution, allowing you to visualize individual frequencies amplitudes as provided by the [FFT](https://en.wikipedia.org/wiki/Fast_Fourier_transform) computation;
|
|
559
|
-
+ **Octave bands** modes display wider vertical bars, each one representing the *n*th part of an octave, based on a [24-tone equal tempered scale](https://en.wikipedia.org/wiki/Quarter_tone);
|
|
560
|
-
+ **Graph** mode uses the discrete data points to draw a continuous line and/or filled area graph (see [`fillAlpha`](#fillalpha-number) and [`lineWidth`](#linewidth-number) properties).
|
|
586
|
+
Visualization mode.
|
|
561
587
|
|
|
562
588
|
mode | description | notes
|
|
563
|
-
|
|
589
|
+
----:|:-----------:|------
|
|
564
590
|
0 | Discrete frequencies |
|
|
565
|
-
1 | 1/24th octave bands |
|
|
566
|
-
2 | 1/12th octave bands |
|
|
567
|
-
3 | 1/8th octave bands |
|
|
568
|
-
4 | 1/6th octave bands |
|
|
569
|
-
5 | 1/4th octave bands |
|
|
570
|
-
6 | 1/3rd octave bands |
|
|
571
|
-
7 | Half octave bands |
|
|
572
|
-
8 | Full octave bands |
|
|
591
|
+
1 | 1/24th octave bands or 240 bands |
|
|
592
|
+
2 | 1/12th octave bands or 120 bands |
|
|
593
|
+
3 | 1/8th octave bands or 80 bands |
|
|
594
|
+
4 | 1/6th octave bands or 60 bands |
|
|
595
|
+
5 | 1/4th octave bands or 40 bands |
|
|
596
|
+
6 | 1/3rd octave bands or 30 bands |
|
|
597
|
+
7 | Half octave bands or 20 bands |
|
|
598
|
+
8 | Full octave bands or 10 bands |
|
|
573
599
|
9 | *(not valid)* | *reserved*
|
|
574
600
|
10 | Graph | *added in v1.1.0*
|
|
575
601
|
|
|
602
|
+
+ **Mode 0** provides the highest resolution, allowing you to visualize individual frequencies as provided by the [FFT](https://en.wikipedia.org/wiki/Fast_Fourier_transform) computation;
|
|
603
|
+
+ **Modes 1 - 8** divide the frequency spectrum in bands; when using the default **logarithmic** [frequency scale](#frequencyscale-string), each band represents the *n*th part of an octave (see also [`ansiBands`](#ansibands-boolean)); otherwise, a fixed number of bands is used for each mode;
|
|
604
|
+
+ **Mode 10** uses the discrete FFT data points to draw a continuous line and/or a filled area graph (see [`fillAlpha`](#fillalpha-number) and [`lineWidth`](#linewidth-number) properties).
|
|
605
|
+
|
|
576
606
|
Defaults to **0**.
|
|
577
607
|
|
|
578
608
|
### `noteLabels` *boolean*
|
|
@@ -581,7 +611,7 @@ Defaults to **0**.
|
|
|
581
611
|
|
|
582
612
|
When set to *true* displays musical note labels instead of frequency values, in the X axis (when [`showScaleX`](#showscalex-boolean) is also set to *true*).
|
|
583
613
|
|
|
584
|
-
For best visualization in octave bands modes, make sure [`ansiBands`](#ansibands-boolean) is set to *false*, so all bands will be perfectly aligned with notes frequencies.
|
|
614
|
+
For best visualization in [octave bands modes](#mode-number), make sure [`frequencyScale`](#frequencyscale-string) is set to *'log'* and [`ansiBands`](#ansibands-boolean) is set to *false*, so all bands will be perfectly aligned with notes frequencies.
|
|
585
615
|
|
|
586
616
|
Defaults to **false**.
|
|
587
617
|
|
|
@@ -589,7 +619,7 @@ Defaults to **false**.
|
|
|
589
619
|
|
|
590
620
|
*Available since v3.6.0*
|
|
591
621
|
|
|
592
|
-
When *true* and [`mode`](#mode-number) is set to one of the **
|
|
622
|
+
When *true* and [`mode`](#mode-number) is set to one of the **bands** modes, analyzer bars are rendered outlined, with customizable [`fillAlpha`](#fillalpha-number) and [`lineWidth`](#linewidth-number).
|
|
593
623
|
|
|
594
624
|
For effect priority when combined with other settings, see [`isOutlineBars`](#isoutlinebars-boolean-read-only).
|
|
595
625
|
|
|
@@ -750,9 +780,9 @@ When *true*, the gradient will be split between both channels, so each channel w
|
|
|
750
780
|
|
|
751
781
|
| splitGradient: *true* | splitGradient: *false* |
|
|
752
782
|
|:--:|:--:|
|
|
753
|
-
|  |  |
|
|
754
784
|
|
|
755
|
-
This option has no effect
|
|
785
|
+
This option has no effect on horizontal gradients, or when [`stereo`](#stereo-boolean) is set to *false*.
|
|
756
786
|
|
|
757
787
|
Defaults to **false**.
|
|
758
788
|
|
|
@@ -802,13 +832,16 @@ Defaults to **1**.
|
|
|
802
832
|
|
|
803
833
|
*Available since v4.0.0*
|
|
804
834
|
|
|
805
|
-
|
|
835
|
+
[Weighting filter](https://en.wikipedia.org/wiki/Weighting_filter) applied to frequency data for spectrum visualization.
|
|
836
|
+
|
|
837
|
+
?> Selecting a weighting filter **does NOT** affect the audio output.
|
|
806
838
|
|
|
807
839
|
Each filter applies a different curve of gain/attenuation to specific frequency ranges, but the general idea is to adjust the
|
|
808
840
|
visualization of frequencies to which the human ear is more or less sensitive.
|
|
809
|
-
Refer to the [weighting filters viewer tool](/tools/weighting-filters.html) for curves and response tables.
|
|
810
841
|
|
|
811
|
-
|
|
842
|
+
Refer to the [weighting filters viewer tool](/tools/weighting-filters.html) for response tables and an interactive version of the curves graph seen below.
|
|
843
|
+
|
|
844
|
+
<img src="img/weigthing-filters-curves.png" class="align-right">
|
|
812
845
|
|
|
813
846
|
weightingFilter | description
|
|
814
847
|
------|------------------------------
|
|
@@ -1075,7 +1108,7 @@ if necessary, the led count is decreased until both the led segment and the vert
|
|
|
1075
1108
|
|
|
1076
1109
|
You can try different values in the [fluid demo](https://audiomotion.dev/demo/fluid.html).
|
|
1077
1110
|
|
|
1078
|
-
|
|
1111
|
+
?> If called with no arguments or any invalid property, clears custom parameters previously set.
|
|
1079
1112
|
|
|
1080
1113
|
### `setOptions( [options] )`
|
|
1081
1114
|
|
|
@@ -1186,15 +1219,16 @@ myAudio.crossOrigin = 'anonymous';
|
|
|
1186
1219
|
|
|
1187
1220
|
### Sound only plays after the user clicks somewhere on the page. <!-- {docsify-ignore} -->
|
|
1188
1221
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1222
|
+
Browser autoplay policy dictates that audio output can only be initiated by a user gesture, and this is enforced by WebAudio API
|
|
1223
|
+
by creating [*AudioContext*](#audioctx-audiocontext-object-read-only) objects in *suspended* mode.
|
|
1191
1224
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1225
|
+
**audioMotion-analyzer** tries to automatically start its audio context on the first click on the page.
|
|
1226
|
+
However, if you're using an `audio` or `video` element with the `controls` property, clicks on those native media controls cannot be detected
|
|
1227
|
+
by JavaScript, so the audio will only be enabled if/when the user clicks somewhere else.
|
|
1194
1228
|
|
|
1195
1229
|
Two possible solutions: **1)** make **sure** your users have to click somewhere else before using the media controls,
|
|
1196
1230
|
like a "power on" button, or simply clicking to select a song from a list will do; or **2)** don't use the native
|
|
1197
|
-
controls at all, and create your own custom
|
|
1231
|
+
controls at all, and create your own custom play and stop buttons. A very simple example:
|
|
1198
1232
|
|
|
1199
1233
|
```html
|
|
1200
1234
|
<audio id="myAudio" src="track.mp3" crossorigin="anonymous"></audio> <!-- do not add the 'controls' property! -->
|
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.3",
|
|
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.3
|
|
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.3';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
13
|
const TAU = 2 * Math.PI,
|
|
@@ -17,15 +17,32 @@ const TAU = 2 * Math.PI,
|
|
|
17
17
|
|
|
18
18
|
const CANVAS_BACKGROUND_COLOR = '#000',
|
|
19
19
|
GRADIENT_DEFAULT_BGCOLOR = '#111',
|
|
20
|
+
FILTER_NONE = '',
|
|
21
|
+
FILTER_A = 'A',
|
|
22
|
+
FILTER_B = 'B',
|
|
23
|
+
FILTER_C = 'C',
|
|
24
|
+
FILTER_D = 'D',
|
|
25
|
+
FILTER_468 = '468',
|
|
20
26
|
FONT_FAMILY = 'sans-serif',
|
|
21
27
|
FPS_COLOR = '#0f0',
|
|
28
|
+
GRADIENT_CLASSIC = 'classic',
|
|
29
|
+
GRADIENT_PRISM = 'prism',
|
|
30
|
+
GRADIENT_RAINBOW = 'rainbow',
|
|
22
31
|
LEDS_UNLIT_COLOR = '#7f7f7f22',
|
|
32
|
+
REASON_CREATE = 'create',
|
|
33
|
+
REASON_FSCHANGE = 'fschange',
|
|
34
|
+
REASON_LORES = 'lores',
|
|
35
|
+
REASON_RESIZE = 'resize',
|
|
36
|
+
REASON_USER = 'user',
|
|
23
37
|
SCALEX_BACKGROUND_COLOR = '#000c',
|
|
24
38
|
SCALEX_LABEL_COLOR = '#fff',
|
|
25
39
|
SCALEX_HIGHLIGHT_COLOR = '#4f4',
|
|
26
40
|
SCALEY_LABEL_COLOR = '#888',
|
|
27
41
|
SCALEY_MIDLINE_COLOR = '#555',
|
|
28
|
-
|
|
42
|
+
SCALE_BARK = 'bark',
|
|
43
|
+
SCALE_LINEAR = 'linear',
|
|
44
|
+
SCALE_LOG = 'log',
|
|
45
|
+
SCALE_MEL = 'mel';
|
|
29
46
|
|
|
30
47
|
// custom error messages
|
|
31
48
|
const ERR_AUDIO_CONTEXT_FAIL = [ 'ERR_AUDIO_CONTEXT_FAIL', 'Could not create audio context. Web Audio API not supported?' ],
|
|
@@ -63,41 +80,38 @@ export default class AudioMotionAnalyzer {
|
|
|
63
80
|
|
|
64
81
|
this._ready = false;
|
|
65
82
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
]
|
|
99
|
-
},
|
|
100
|
-
};
|
|
83
|
+
// Initialize internal gradients object
|
|
84
|
+
this._gradients = {};
|
|
85
|
+
|
|
86
|
+
// 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
|
+
});
|
|
101
115
|
|
|
102
116
|
// Set container
|
|
103
117
|
this._container = container || document.body;
|
|
@@ -199,7 +213,7 @@ export default class AudioMotionAnalyzer {
|
|
|
199
213
|
// delay the resize to prioritize a possible following `fullscreenchange` event
|
|
200
214
|
this._fsTimeout = window.setTimeout( () => {
|
|
201
215
|
if ( ! this._fsChanging ) {
|
|
202
|
-
this._setCanvas(
|
|
216
|
+
this._setCanvas( REASON_RESIZE );
|
|
203
217
|
this._fsTimeout = 0;
|
|
204
218
|
}
|
|
205
219
|
}, 60 );
|
|
@@ -225,7 +239,7 @@ export default class AudioMotionAnalyzer {
|
|
|
225
239
|
window.clearTimeout( this._fsTimeout );
|
|
226
240
|
|
|
227
241
|
// update the canvas
|
|
228
|
-
this._setCanvas(
|
|
242
|
+
this._setCanvas( REASON_FSCHANGE );
|
|
229
243
|
|
|
230
244
|
// delay clearing the flag to prevent any shortly following resize event
|
|
231
245
|
this._fsTimeout = window.setTimeout( () => {
|
|
@@ -254,7 +268,7 @@ export default class AudioMotionAnalyzer {
|
|
|
254
268
|
|
|
255
269
|
// Finish canvas setup
|
|
256
270
|
this._ready = true;
|
|
257
|
-
this._setCanvas(
|
|
271
|
+
this._setCanvas( REASON_CREATE );
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
/**
|
|
@@ -300,6 +314,16 @@ export default class AudioMotionAnalyzer {
|
|
|
300
314
|
this._calcBars();
|
|
301
315
|
}
|
|
302
316
|
|
|
317
|
+
get frequencyScale() {
|
|
318
|
+
return this._frequencyScale;
|
|
319
|
+
}
|
|
320
|
+
set frequencyScale( value ) {
|
|
321
|
+
const FREQUENCY_SCALES = [ SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR ];
|
|
322
|
+
this._frequencyScale = FREQUENCY_SCALES[ Math.max( 0, FREQUENCY_SCALES.indexOf( ( '' + value ).toLowerCase() ) ) ];
|
|
323
|
+
this._calcAux();
|
|
324
|
+
this._calcBars();
|
|
325
|
+
}
|
|
326
|
+
|
|
303
327
|
get gradient() {
|
|
304
328
|
return this._gradient;
|
|
305
329
|
}
|
|
@@ -316,7 +340,7 @@ export default class AudioMotionAnalyzer {
|
|
|
316
340
|
}
|
|
317
341
|
set height( h ) {
|
|
318
342
|
this._height = h;
|
|
319
|
-
this._setCanvas(
|
|
343
|
+
this._setCanvas( REASON_USER );
|
|
320
344
|
}
|
|
321
345
|
|
|
322
346
|
get ledBars() {
|
|
@@ -346,7 +370,7 @@ export default class AudioMotionAnalyzer {
|
|
|
346
370
|
}
|
|
347
371
|
set loRes( value ) {
|
|
348
372
|
this._loRes = !! value;
|
|
349
|
-
this._setCanvas(
|
|
373
|
+
this._setCanvas( REASON_LORES );
|
|
350
374
|
}
|
|
351
375
|
|
|
352
376
|
get lumiBars() {
|
|
@@ -522,6 +546,7 @@ export default class AudioMotionAnalyzer {
|
|
|
522
546
|
return this._weightingFilter;
|
|
523
547
|
}
|
|
524
548
|
set weightingFilter( value ) {
|
|
549
|
+
const WEIGHTING_FILTERS = [ FILTER_NONE, FILTER_A, FILTER_B, FILTER_C, FILTER_D, FILTER_468 ];
|
|
525
550
|
this._weightingFilter = WEIGHTING_FILTERS[ Math.max( 0, WEIGHTING_FILTERS.indexOf( ( '' + value ).toUpperCase() ) ) ];
|
|
526
551
|
}
|
|
527
552
|
|
|
@@ -530,7 +555,7 @@ export default class AudioMotionAnalyzer {
|
|
|
530
555
|
}
|
|
531
556
|
set width( w ) {
|
|
532
557
|
this._width = w;
|
|
533
|
-
this._setCanvas(
|
|
558
|
+
this._setCanvas( REASON_USER );
|
|
534
559
|
}
|
|
535
560
|
|
|
536
561
|
// Read only properties
|
|
@@ -562,6 +587,9 @@ export default class AudioMotionAnalyzer {
|
|
|
562
587
|
get isAlphaBars() {
|
|
563
588
|
return this._isAlphaBars;
|
|
564
589
|
}
|
|
590
|
+
get isBandsMode() {
|
|
591
|
+
return this._isBandsMode;
|
|
592
|
+
}
|
|
565
593
|
get isFullscreen() {
|
|
566
594
|
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
567
595
|
}
|
|
@@ -765,7 +793,7 @@ export default class AudioMotionAnalyzer {
|
|
|
765
793
|
setCanvasSize( w, h ) {
|
|
766
794
|
this._width = w;
|
|
767
795
|
this._height = h;
|
|
768
|
-
this._setCanvas(
|
|
796
|
+
this._setCanvas( REASON_USER );
|
|
769
797
|
}
|
|
770
798
|
|
|
771
799
|
/**
|
|
@@ -895,11 +923,12 @@ export default class AudioMotionAnalyzer {
|
|
|
895
923
|
|
|
896
924
|
this._radius = Math.min( canvas.width, canvas.height ) * ( this._stereo ? .375 : .125 ) | 0;
|
|
897
925
|
this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
|
|
898
|
-
this.
|
|
899
|
-
this.
|
|
900
|
-
this.
|
|
926
|
+
this._isBandsMode = this._mode % 10 != 0;
|
|
927
|
+
this._isOctaveBands = this._isBandsMode && this._frequencyScale == SCALE_LOG;
|
|
928
|
+
this._isLedDisplay = this._showLeds && this._isBandsMode && ! isRadial;
|
|
929
|
+
this._isLumiBars = this._lumiBars && this._isBandsMode && ! isRadial;
|
|
901
930
|
this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
|
|
902
|
-
this._isOutline = this._outlineBars && this.
|
|
931
|
+
this._isOutline = this._outlineBars && this._isBandsMode && ! this._isLumiBars && ! this._isLedDisplay;
|
|
903
932
|
this._maximizeLeds = ! this._stereo || this._reflexRatio > 0 && ! this._isLumiBars;
|
|
904
933
|
|
|
905
934
|
this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
|
|
@@ -914,27 +943,9 @@ export default class AudioMotionAnalyzer {
|
|
|
914
943
|
}
|
|
915
944
|
|
|
916
945
|
/**
|
|
917
|
-
*
|
|
946
|
+
* Calculate the X-coordinate on canvas for each analyzer bar
|
|
918
947
|
*/
|
|
919
948
|
_calcBars() {
|
|
920
|
-
/*
|
|
921
|
-
Since the frequency scale is logarithmic, each position in the X-axis actually represents a power of 10.
|
|
922
|
-
To improve performace, the position of each frequency is calculated in advance and stored in an array.
|
|
923
|
-
Canvas space usage is optimized to accommodate exactly the frequency range the user needs.
|
|
924
|
-
Positions need to be recalculated whenever the frequency range, FFT size or canvas size change.
|
|
925
|
-
|
|
926
|
-
+-------------------------- canvas --------------------------+
|
|
927
|
-
| |
|
|
928
|
-
|-------------------|-----|-------------|-------------------|-------------------|------|------------|
|
|
929
|
-
1 10 | 100 1K 10K | 100K (Hz)
|
|
930
|
-
(10^0) (10^1) | (10^2) (10^3) (10^4) | (10^5)
|
|
931
|
-
|-------------|<--- logWidth ---->|--------------------------|
|
|
932
|
-
minFreq--> 20 (pixels) 22K <--maxFreq
|
|
933
|
-
(10^1.3) (10^4.34)
|
|
934
|
-
^
|
|
935
|
-
minLog
|
|
936
|
-
*/
|
|
937
|
-
|
|
938
949
|
const bars = this._bars = []; // initialize object property
|
|
939
950
|
|
|
940
951
|
if ( ! this._ready )
|
|
@@ -944,46 +955,46 @@ export default class AudioMotionAnalyzer {
|
|
|
944
955
|
// bar object: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
|
|
945
956
|
const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
|
|
946
957
|
|
|
958
|
+
/*
|
|
959
|
+
A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
|
|
960
|
+
from the available FFT data. We find the FFT bin which closer matches the desired frequency and
|
|
961
|
+
interpolate its value with that of the next adjacent bin, like so:
|
|
962
|
+
|
|
963
|
+
v = v0 + ( v1 - v0 ) * ( log2( f / f0 ) / log2( f1 / f0 ) )
|
|
964
|
+
\__________________________________/
|
|
965
|
+
|
|
|
966
|
+
ratio
|
|
967
|
+
where:
|
|
968
|
+
|
|
969
|
+
f - desired frequency
|
|
970
|
+
v - amplitude (volume) of desired frequency
|
|
971
|
+
f0 - frequency represented by the lower FFT bin
|
|
972
|
+
f1 - frequency represented by the upper FFT bin
|
|
973
|
+
v0 - amplitude of f0
|
|
974
|
+
v1 - amplitude of f1
|
|
975
|
+
|
|
976
|
+
ratio is calculated in advance here, to reduce computational complexity during real-time rendering.
|
|
977
|
+
*/
|
|
978
|
+
|
|
979
|
+
// helper function to calculate FFT bin and interpolation ratio for a given frequency
|
|
980
|
+
const calcRatio = freq => {
|
|
981
|
+
const bin = this._freqToBin( freq, 'floor' ), // find closest FFT bin
|
|
982
|
+
lower = this._binToFreq( bin ),
|
|
983
|
+
upper = this._binToFreq( bin + 1 ),
|
|
984
|
+
ratio = Math.log2( freq / lower ) / Math.log2( upper / lower );
|
|
985
|
+
|
|
986
|
+
return [ bin, ratio ];
|
|
987
|
+
}
|
|
988
|
+
|
|
947
989
|
const analyzerWidth = this._analyzerWidth,
|
|
948
990
|
initialX = this._initialX,
|
|
949
991
|
isAnsiBands = this._ansiBands,
|
|
950
992
|
maxFreq = this._maxFreq,
|
|
951
993
|
minFreq = this._minFreq;
|
|
952
994
|
|
|
953
|
-
let
|
|
995
|
+
let scaleMin, unitWidth;
|
|
954
996
|
|
|
955
997
|
if ( this._isOctaveBands ) {
|
|
956
|
-
/*
|
|
957
|
-
A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
|
|
958
|
-
from the available FFT data. We find the FFT bin which closer matches the desired frequency and
|
|
959
|
-
interpolate its value with that of the next adjacent bin, like so:
|
|
960
|
-
|
|
961
|
-
v = v0 + ( v1 - v0 ) * ( log2( f / f0 ) / log2( f1 / f0 ) )
|
|
962
|
-
\__________________________________/
|
|
963
|
-
|
|
|
964
|
-
ratio
|
|
965
|
-
where:
|
|
966
|
-
|
|
967
|
-
f - desired frequency
|
|
968
|
-
v - amplitude (volume) of desired frequency
|
|
969
|
-
f0 - frequency represented by the lower FFT bin
|
|
970
|
-
f1 - frequency represented by the upper FFT bin
|
|
971
|
-
v0 - amplitude of f0
|
|
972
|
-
v1 - amplitude of f1
|
|
973
|
-
|
|
974
|
-
ratio is calculated in advance here, to reduce computational complexity during real-time rendering.
|
|
975
|
-
*/
|
|
976
|
-
|
|
977
|
-
// helper function to calculate FFT bin and interpolation ratio for a given frequency
|
|
978
|
-
const calcRatio = freq => {
|
|
979
|
-
const bin = this._freqToBin( freq, 'floor' ), // find closest FFT bin
|
|
980
|
-
lower = this._binToFreq( bin ),
|
|
981
|
-
upper = this._binToFreq( bin + 1 ),
|
|
982
|
-
ratio = Math.log2( freq / lower ) / Math.log2( upper / lower );
|
|
983
|
-
|
|
984
|
-
return [ bin, ratio ];
|
|
985
|
-
}
|
|
986
|
-
|
|
987
998
|
// helper function to round a value to a given number of significant digits
|
|
988
999
|
// `atLeast` set to true prevents reducing the number of integer significant digits
|
|
989
1000
|
const roundSD = ( value, digits, atLeast ) => +value.toPrecision( atLeast ? Math.max( digits, 1 + Math.log10( value ) | 0 ) : digits );
|
|
@@ -1045,11 +1056,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1045
1056
|
const firstBar = bars[0],
|
|
1046
1057
|
lastBar = bars[ bars.length - 1 ];
|
|
1047
1058
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1059
|
+
scaleMin = this._freqScaling( firstBar.freqLo );
|
|
1060
|
+
unitWidth = analyzerWidth / ( this._freqScaling( lastBar.freqHi ) - scaleMin );
|
|
1050
1061
|
|
|
1051
1062
|
// clamp edge frequencies to minFreq / maxFreq, if necessary
|
|
1052
|
-
// this is done after computing
|
|
1063
|
+
// this is done after computing scaleMin and unitWidth, for the proper positioning of labels on the X-axis
|
|
1053
1064
|
if ( firstBar.freqLo < minFreq ) {
|
|
1054
1065
|
firstBar.freqLo = minFreq;
|
|
1055
1066
|
[ firstBar.binLo, firstBar.ratioLo ] = calcRatio( minFreq );
|
|
@@ -1060,14 +1071,42 @@ export default class AudioMotionAnalyzer {
|
|
|
1060
1071
|
[ lastBar.binHi, lastBar.ratioHi ] = calcRatio( maxFreq );
|
|
1061
1072
|
}
|
|
1062
1073
|
}
|
|
1063
|
-
else {
|
|
1074
|
+
else if ( this._isBandsMode ) { // a bands mode is selected, but frequency scale is not logarithmic
|
|
1075
|
+
|
|
1076
|
+
const bands = [0,24,12,8,6,4,3,2,1][ this._mode ] * 10;
|
|
1077
|
+
|
|
1078
|
+
const invFreqScaling = x => {
|
|
1079
|
+
switch ( this._frequencyScale ) {
|
|
1080
|
+
case SCALE_BARK :
|
|
1081
|
+
return 1960 / ( 26.81 / ( x + .53 ) - 1 );
|
|
1082
|
+
case SCALE_MEL :
|
|
1083
|
+
return 700 * ( 2 ** x - 1 );
|
|
1084
|
+
case SCALE_LINEAR :
|
|
1085
|
+
return x;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1064
1088
|
|
|
1065
|
-
|
|
1089
|
+
this._barWidth = analyzerWidth / bands;
|
|
1066
1090
|
|
|
1091
|
+
scaleMin = this._freqScaling( minFreq );
|
|
1092
|
+
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
1093
|
+
|
|
1094
|
+
for ( let i = 0, posX = 0; i < bands; i++, posX += this._barWidth ) {
|
|
1095
|
+
const freqLo = invFreqScaling( scaleMin + posX / unitWidth ),
|
|
1096
|
+
freq = invFreqScaling( scaleMin + ( posX + this._barWidth / 2 ) / unitWidth ),
|
|
1097
|
+
freqHi = invFreqScaling( scaleMin + ( posX + this._barWidth ) / unitWidth ),
|
|
1098
|
+
[ binLo, ratioLo ] = calcRatio( freqLo ),
|
|
1099
|
+
[ binHi, ratioHi ] = calcRatio( freqHi );
|
|
1100
|
+
|
|
1101
|
+
barsPush( { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi } );
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
}
|
|
1105
|
+
else { // Discrete frequencies modes
|
|
1067
1106
|
this._barWidth = 1;
|
|
1068
1107
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1108
|
+
scaleMin = this._freqScaling( minFreq );
|
|
1109
|
+
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
1071
1110
|
|
|
1072
1111
|
const minIndex = this._freqToBin( minFreq, 'floor' ),
|
|
1073
1112
|
maxIndex = this._freqToBin( maxFreq );
|
|
@@ -1076,7 +1115,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1076
1115
|
|
|
1077
1116
|
for ( let i = minIndex; i <= maxIndex; i++ ) {
|
|
1078
1117
|
const freq = this._binToFreq( i ), // frequency represented by this index
|
|
1079
|
-
posX = initialX + Math.round(
|
|
1118
|
+
posX = initialX + Math.round( unitWidth * ( this._freqScaling( freq ) - scaleMin ) ); // avoid fractionary pixel values
|
|
1080
1119
|
|
|
1081
1120
|
// if it's on a different X-coordinate, create a new bar for this frequency
|
|
1082
1121
|
if ( posX > lastPos ) {
|
|
@@ -1093,8 +1132,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1093
1132
|
}
|
|
1094
1133
|
|
|
1095
1134
|
// save these for scale generation
|
|
1096
|
-
this.
|
|
1097
|
-
this.
|
|
1135
|
+
this._scaleMin = scaleMin;
|
|
1136
|
+
this._unitWidth = unitWidth;
|
|
1098
1137
|
|
|
1099
1138
|
// update internal variables
|
|
1100
1139
|
this._calcAux();
|
|
@@ -1110,7 +1149,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1110
1149
|
* Calculate attributes for the vintage LEDs effect, based on visualization mode and canvas resolution
|
|
1111
1150
|
*/
|
|
1112
1151
|
_calcLeds() {
|
|
1113
|
-
if ( ! this.
|
|
1152
|
+
if ( ! this._isBandsMode || ! this._ready )
|
|
1114
1153
|
return;
|
|
1115
1154
|
|
|
1116
1155
|
// adjustment for high pixel-ratio values on low-resolution screens (Android TV)
|
|
@@ -1180,16 +1219,24 @@ export default class AudioMotionAnalyzer {
|
|
|
1180
1219
|
canvasX = scaleX.canvas,
|
|
1181
1220
|
canvasR = scaleR.canvas,
|
|
1182
1221
|
freqLabels = [],
|
|
1222
|
+
frequencyScale= this._frequencyScale,
|
|
1183
1223
|
initialX = this._initialX,
|
|
1184
1224
|
isStereo = this._stereo,
|
|
1185
1225
|
isMirror = this._mirror,
|
|
1186
1226
|
isNoteLabels = this._noteLabels,
|
|
1187
1227
|
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1188
1228
|
scaleHeight = Math.min( canvas.width, canvas.height ) * .03 | 0, // circular scale height (radial mode)
|
|
1229
|
+
fontSizeX = canvasX.height >> 1,
|
|
1230
|
+
fontSizeR = scaleHeight >> 1,
|
|
1189
1231
|
root12 = 2 ** ( 1 / 12 );
|
|
1190
1232
|
|
|
1191
|
-
if ( this._ansiBands
|
|
1192
|
-
freqLabels.push(16,31.5,63,125,250,500,1e3,2e3,4e3
|
|
1233
|
+
if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
|
|
1234
|
+
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
1235
|
+
if ( frequencyScale == SCALE_LINEAR )
|
|
1236
|
+
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3 );
|
|
1237
|
+
else
|
|
1238
|
+
freqLabels.push( 8e3, 16e3 );
|
|
1239
|
+
}
|
|
1193
1240
|
else {
|
|
1194
1241
|
let freq = C_1;
|
|
1195
1242
|
for ( let octave = -1; octave < 11; octave++ ) {
|
|
@@ -1239,28 +1286,35 @@ export default class AudioMotionAnalyzer {
|
|
|
1239
1286
|
scaleR.stroke();
|
|
1240
1287
|
|
|
1241
1288
|
scaleX.fillStyle = scaleR.fillStyle = SCALEX_LABEL_COLOR;
|
|
1242
|
-
scaleX.font = `${
|
|
1243
|
-
scaleR.font = `${
|
|
1289
|
+
scaleX.font = `${ fontSizeX }px ${FONT_FAMILY}`;
|
|
1290
|
+
scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
|
|
1244
1291
|
scaleX.textAlign = scaleR.textAlign = 'center';
|
|
1245
1292
|
|
|
1293
|
+
let prevX = 0, prevR = 0;
|
|
1294
|
+
|
|
1246
1295
|
for ( const item of freqLabels ) {
|
|
1247
1296
|
const [ freq, label ] = Array.isArray( item ) ? item : [ item, item < 1e3 ? item | 0 : `${ ( item / 100 | 0 ) / 10 }k` ],
|
|
1248
|
-
x = this.
|
|
1297
|
+
x = this._unitWidth * ( this._freqScaling( freq ) - this._scaleMin ),
|
|
1249
1298
|
y = canvasX.height * .75,
|
|
1250
1299
|
isC = label[0] == 'C',
|
|
1251
|
-
maxW = isNoteLabels && ! isMirror ?
|
|
1300
|
+
maxW = fontSizeX * ( isNoteLabels && ! isMirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1252
1301
|
|
|
1253
1302
|
if ( x >= 0 && x <= analyzerWidth ) {
|
|
1303
|
+
|
|
1254
1304
|
scaleX.fillStyle = scaleR.fillStyle = isC && ! isMirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1255
1305
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1306
|
+
if ( x > prevX + fontSizeX / 2 ) {
|
|
1307
|
+
scaleX.fillText( label, initialX + x, y, maxW );
|
|
1308
|
+
if ( isMirror )
|
|
1309
|
+
scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
|
|
1310
|
+
prevX = x + Math.min( maxW, scaleX.measureText( label ).width ) / 2;
|
|
1311
|
+
}
|
|
1259
1312
|
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
if ( x >
|
|
1313
|
+
if ( x < analyzerWidth && ( x > prevR + fontSizeR || isC ) ) { // avoid wrapping-around the last label and overlapping the first one
|
|
1314
|
+
radialLabel( x, label );
|
|
1315
|
+
if ( isMirror && x > fontSizeR ) // avoid overlapping of first labels on mirror mode
|
|
1263
1316
|
radialLabel( -x, label );
|
|
1317
|
+
prevR = x;
|
|
1264
1318
|
}
|
|
1265
1319
|
}
|
|
1266
1320
|
}
|
|
@@ -1281,7 +1335,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1281
1335
|
isLedDisplay = this._isLedDisplay,
|
|
1282
1336
|
isLinear = this._linearAmplitude,
|
|
1283
1337
|
isLumiBars = this._isLumiBars,
|
|
1284
|
-
|
|
1338
|
+
isBandsMode = this._isBandsMode,
|
|
1285
1339
|
isOutline = this._isOutline,
|
|
1286
1340
|
isRadial = this._radial,
|
|
1287
1341
|
isStereo = this._stereo,
|
|
@@ -1317,24 +1371,24 @@ export default class AudioMotionAnalyzer {
|
|
|
1317
1371
|
linearTodB = value => 20 * Math.log10( value );
|
|
1318
1372
|
|
|
1319
1373
|
switch ( weightingFilter ) {
|
|
1320
|
-
case
|
|
1374
|
+
case FILTER_A : // A-weighting https://en.wikipedia.org/wiki/A-weighting
|
|
1321
1375
|
const rA = ( SQ12194 * f2 ** 2 ) / ( ( f2 + SQ20_6 ) * Math.sqrt( ( f2 + SQ107_7 ) * ( f2 + SQ737_9 ) ) * ( f2 + SQ12194 ) );
|
|
1322
1376
|
return 2 + linearTodB( rA );
|
|
1323
1377
|
|
|
1324
|
-
case
|
|
1378
|
+
case FILTER_B :
|
|
1325
1379
|
const rB = ( SQ12194 * f2 * freq ) / ( ( f2 + SQ20_6 ) * Math.sqrt( f2 + SQ158_5 ) * ( f2 + SQ12194 ) );
|
|
1326
1380
|
return .17 + linearTodB( rB );
|
|
1327
1381
|
|
|
1328
|
-
case
|
|
1382
|
+
case FILTER_C :
|
|
1329
1383
|
const rC = ( SQ12194 * f2 ) / ( ( f2 + SQ20_6 ) * ( f2 + SQ12194 ) );
|
|
1330
1384
|
return .06 + linearTodB( rC );
|
|
1331
1385
|
|
|
1332
|
-
case
|
|
1386
|
+
case FILTER_D :
|
|
1333
1387
|
const h = ( ( 1037918.48 - f2 ) ** 2 + 1080768.16 * f2 ) / ( ( 9837328 - f2 ) ** 2 + 11723776 * f2 ),
|
|
1334
1388
|
rD = ( freq / 6.8966888496476e-5 ) * Math.sqrt( h / ( ( f2 + 79919.29 ) * ( f2 + 1345600 ) ) );
|
|
1335
1389
|
return linearTodB( rD );
|
|
1336
1390
|
|
|
1337
|
-
case
|
|
1391
|
+
case FILTER_468 : // ITU-R 468 https://en.wikipedia.org/wiki/ITU-R_468_noise_weighting
|
|
1338
1392
|
const h1 = -4.737338981378384e-24 * freq ** 6 + 2.043828333606125e-15 * freq ** 4 - 1.363894795463638e-7 * f2 + 1,
|
|
1339
1393
|
h2 = 1.306612257412824e-19 * freq ** 5 - 2.118150887518656e-11 * freq ** 3 + 5.559488023498642e-4 * freq,
|
|
1340
1394
|
rI = 1.246332637532143e-4 * freq / Math.hypot( h1, h2 );
|
|
@@ -1377,14 +1431,14 @@ export default class AudioMotionAnalyzer {
|
|
|
1377
1431
|
|
|
1378
1432
|
// LED attributes and helper function for bar height calculation
|
|
1379
1433
|
const [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1380
|
-
const ledPosY = height => ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV;
|
|
1434
|
+
const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1381
1435
|
|
|
1382
1436
|
// select background color
|
|
1383
1437
|
const bgColor = ( ! this.showBgColor || isLedDisplay && ! this.overlay ) ? '#000' : this._gradients[ this._gradient ].bgColor;
|
|
1384
1438
|
|
|
1385
1439
|
// compute the effective bar width, considering the selected bar spacing
|
|
1386
1440
|
// if led effect is active, ensure at least the spacing from led definitions
|
|
1387
|
-
let width = this._barWidth - ( !
|
|
1441
|
+
let width = this._barWidth - ( ! isBandsMode ? 0 : Math.max( isLedDisplay ? ledSpaceH : 0, this._barSpacePx ) );
|
|
1388
1442
|
|
|
1389
1443
|
// make sure width is integer for pixel accurate calculation, when no bar spacing is required
|
|
1390
1444
|
if ( this._barSpace == 0 && ! isLedDisplay )
|
|
@@ -1485,8 +1539,10 @@ export default class AudioMotionAnalyzer {
|
|
|
1485
1539
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1486
1540
|
|
|
1487
1541
|
// helper function for FFT data interpolation
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1542
|
+
const interpolate = ( bin, ratio ) => {
|
|
1543
|
+
const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
|
|
1544
|
+
return isNaN( value ) ? -Infinity : value;
|
|
1545
|
+
}
|
|
1490
1546
|
|
|
1491
1547
|
// start drawing path (for mode 10)
|
|
1492
1548
|
ctx.beginPath();
|
|
@@ -1539,14 +1595,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1539
1595
|
else if ( isOutline )
|
|
1540
1596
|
ctx.globalAlpha = this.fillAlpha;
|
|
1541
1597
|
|
|
1542
|
-
//
|
|
1543
|
-
|
|
1544
|
-
barHeight = ledPosY( barHeight );
|
|
1545
|
-
if ( barHeight < 0 )
|
|
1546
|
-
barHeight = 0; // prevent showing leds below 0 when overlay and reflex are active
|
|
1547
|
-
}
|
|
1548
|
-
else
|
|
1549
|
-
barHeight = barHeight * maxBarHeight | 0;
|
|
1598
|
+
// compute actual bar height on screen
|
|
1599
|
+
barHeight = isLedDisplay ? ledPosY( barHeight ) : barHeight * maxBarHeight | 0;
|
|
1550
1600
|
|
|
1551
1601
|
// invert bar for radial channel 1
|
|
1552
1602
|
if ( isRadial && channel == 1 )
|
|
@@ -1561,7 +1611,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1561
1611
|
if ( mode == 10 ) {
|
|
1562
1612
|
// compute the average between the initial bar (i==0) and the next one
|
|
1563
1613
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
1564
|
-
const nextBarAvg = i ? 0 : ( fftData[ this._bars[1].binLo ]
|
|
1614
|
+
const nextBarAvg = i ? 0 : ( this._normalizedB( fftData[ this._bars[1].binLo ] ) * maxBarHeight * ( ! isRadial || ! channel || - 1 ) + barHeight ) / 2;
|
|
1565
1615
|
|
|
1566
1616
|
if ( isRadial ) {
|
|
1567
1617
|
if ( i == 0 )
|
|
@@ -1577,7 +1627,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1577
1627
|
if ( i == 0 ) {
|
|
1578
1628
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
1579
1629
|
if ( mirrorMode != -1 ) {
|
|
1580
|
-
const prevFFTData = binLo ? fftData[ binLo - 1 ]
|
|
1630
|
+
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
1581
1631
|
ctx.moveTo( initialX - lineWidth, analyzerBottom - prevFFTData );
|
|
1582
1632
|
}
|
|
1583
1633
|
else
|
|
@@ -1657,8 +1707,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1657
1707
|
ctx.globalAlpha = peak;
|
|
1658
1708
|
|
|
1659
1709
|
// render peak according to current mode / effect
|
|
1660
|
-
if ( isLedDisplay )
|
|
1661
|
-
|
|
1710
|
+
if ( isLedDisplay ) {
|
|
1711
|
+
const ledPeak = ledPosY( peak );
|
|
1712
|
+
if ( ledPeak >= ledHeight ) // avoid peak below zero level
|
|
1713
|
+
ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
1714
|
+
}
|
|
1662
1715
|
else if ( ! isRadial )
|
|
1663
1716
|
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, adjWidth, 2 );
|
|
1664
1717
|
else if ( mode != 10 ) // radial - no peaks for mode 10
|
|
@@ -1803,6 +1856,22 @@ export default class AudioMotionAnalyzer {
|
|
|
1803
1856
|
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1804
1857
|
}
|
|
1805
1858
|
|
|
1859
|
+
/**
|
|
1860
|
+
* Return scaled frequency according to the selected scale
|
|
1861
|
+
*/
|
|
1862
|
+
_freqScaling( freq ) {
|
|
1863
|
+
switch ( this._frequencyScale ) {
|
|
1864
|
+
case SCALE_LOG :
|
|
1865
|
+
return Math.log2( freq );
|
|
1866
|
+
case SCALE_BARK :
|
|
1867
|
+
return ( 26.81 * freq ) / ( 1960 + freq ) - .53;
|
|
1868
|
+
case SCALE_MEL :
|
|
1869
|
+
return Math.log2( 1 + freq / 700 );
|
|
1870
|
+
case SCALE_LINEAR :
|
|
1871
|
+
return freq;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1806
1875
|
/**
|
|
1807
1876
|
* Return the FFT data bin (array index) which represents a given frequency
|
|
1808
1877
|
*/
|
|
@@ -1984,7 +2053,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1984
2053
|
|
|
1985
2054
|
// detect fullscreen changes (for Safari)
|
|
1986
2055
|
if ( this._fsStatus !== undefined && this._fsStatus !== isFullscreen )
|
|
1987
|
-
reason =
|
|
2056
|
+
reason = REASON_FSCHANGE;
|
|
1988
2057
|
this._fsStatus = isFullscreen;
|
|
1989
2058
|
|
|
1990
2059
|
// call the callback function, if defined
|
|
@@ -2005,7 +2074,8 @@ export default class AudioMotionAnalyzer {
|
|
|
2005
2074
|
bgAlpha : 0.7,
|
|
2006
2075
|
fftSize : 8192,
|
|
2007
2076
|
fillAlpha : 1,
|
|
2008
|
-
|
|
2077
|
+
frequencyScale : SCALE_LOG,
|
|
2078
|
+
gradient : GRADIENT_CLASSIC,
|
|
2009
2079
|
ledBars : false,
|
|
2010
2080
|
linearAmplitude: false,
|
|
2011
2081
|
linearBoost : 1,
|
|
@@ -2038,7 +2108,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2038
2108
|
stereo : false,
|
|
2039
2109
|
useCanvas : true,
|
|
2040
2110
|
volume : 1,
|
|
2041
|
-
weightingFilter:
|
|
2111
|
+
weightingFilter: FILTER_NONE
|
|
2042
2112
|
};
|
|
2043
2113
|
|
|
2044
2114
|
// callback functions properties
|
package/src/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ type OnCanvasResizeFunction = (
|
|
|
4
4
|
instance: AudioMotionAnalyzer
|
|
5
5
|
) => unknown;
|
|
6
6
|
|
|
7
|
-
type CanvasResizeReason = "create" | "fschange" | "lores" | "resize" | "user";
|
|
7
|
+
export type CanvasResizeReason = "create" | "fschange" | "lores" | "resize" | "user";
|
|
8
8
|
|
|
9
9
|
export interface Options {
|
|
10
10
|
alphaBars?: boolean;
|
|
@@ -13,11 +13,12 @@ export interface Options {
|
|
|
13
13
|
bgAlpha?: number;
|
|
14
14
|
fftSize?: number;
|
|
15
15
|
fillAlpha?: number;
|
|
16
|
+
frequencyScale?: FrequencyScale;
|
|
16
17
|
gradient?: string;
|
|
17
18
|
height?: number;
|
|
18
19
|
ledBars?: boolean;
|
|
19
20
|
linearAmplitude?: boolean;
|
|
20
|
-
linearBoost?:
|
|
21
|
+
linearBoost?: number;
|
|
21
22
|
lineWidth?: number;
|
|
22
23
|
loRes?: boolean;
|
|
23
24
|
lumiBars?: boolean;
|
|
@@ -53,7 +54,7 @@ export interface Options {
|
|
|
53
54
|
width?: number;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
interface AnalyzerBarData {
|
|
57
|
+
export interface AnalyzerBarData {
|
|
57
58
|
posX: number;
|
|
58
59
|
freq: number;
|
|
59
60
|
freqLo: number;
|
|
@@ -63,18 +64,20 @@ interface AnalyzerBarData {
|
|
|
63
64
|
value: [ number, number? ];
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
interface ConstructorOptions extends Options {
|
|
67
|
+
export interface ConstructorOptions extends Options {
|
|
67
68
|
audioCtx?: AudioContext;
|
|
68
69
|
connectSpeakers?: boolean;
|
|
69
70
|
fsElement?: HTMLElement;
|
|
70
71
|
source?: HTMLMediaElement | AudioNode;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
type EnergyPreset = "peak" | "bass" | "lowMid" | "mid" | "highMid" | "treble";
|
|
74
|
+
export type EnergyPreset = "peak" | "bass" | "lowMid" | "mid" | "highMid" | "treble";
|
|
74
75
|
|
|
75
|
-
type
|
|
76
|
+
export type FrequencyScale = "bark" | "linear" | "log" | "mel";
|
|
76
77
|
|
|
77
|
-
type
|
|
78
|
+
export type GradientColorStop = string | { pos: number; color: string };
|
|
79
|
+
|
|
80
|
+
export type WeightingFilter = "" | "A" | "B" | "C" | "D" | "468";
|
|
78
81
|
|
|
79
82
|
type ArrayTwoOrMore<T> = {
|
|
80
83
|
0: T
|
|
@@ -124,6 +127,9 @@ declare class AudioMotionAnalyzer {
|
|
|
124
127
|
get fsHeight(): number;
|
|
125
128
|
get fsWidth(): number;
|
|
126
129
|
|
|
130
|
+
get frequencyScale(): FrequencyScale;
|
|
131
|
+
set frequencyScale(value: FrequencyScale);
|
|
132
|
+
|
|
127
133
|
get gradient(): string;
|
|
128
134
|
set gradient(value: string);
|
|
129
135
|
|
|
@@ -134,6 +140,7 @@ declare class AudioMotionAnalyzer {
|
|
|
134
140
|
set width(w: number);
|
|
135
141
|
|
|
136
142
|
get isAlphaBars(): boolean;
|
|
143
|
+
get isBandsMode(): boolean;
|
|
137
144
|
get isFullscreen(): boolean;
|
|
138
145
|
get isLedBars(): boolean;
|
|
139
146
|
get isLumiBars(): boolean;
|