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 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 scale with customizable range
32
- + Visualize discrete FFT frequencies or octave bands
33
- + Optional effects: vintage LEDs, luminance bars, mirroring and reflection, radial visualization
34
- + Customizable sensitivity, FFT size and time-smoothing constant
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
- [![demo-animation](demo/media/demo.gif)](https://audiomotion.dev/demo/)
42
+ [![demo-animation](img/demo.gif)](https://audiomotion.dev/demo/)
42
43
 
43
44
  ?> https://audiomotion.dev/demo/
44
45
 
@@ -121,11 +122,13 @@ options = {<br>
121
122
  &emsp;&emsp;[connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
122
123
  &emsp;&emsp;[fftSize](#fftsize-number): **8192**,<br>
123
124
  &emsp;&emsp;[fillAlpha](#fillalpha-number): **1**,<br>
125
+ &emsp;&emsp;[frequencyScale](#frequencyscale-string): **'log'**,<br>
124
126
  &emsp;&emsp;[fsElement](#fselement-htmlelement-object): *undefined*, // constructor only<br>
125
127
  &emsp;&emsp;[gradient](#gradient-string): **'classic'**,<br>
126
128
  &emsp;&emsp;[height](#height-number): *undefined*,<br>
127
129
  &emsp;&emsp;[ledBars](#ledbars-boolean): **false**,<br>
128
130
  &emsp;&emsp;[linearAmplitude](#linearamplitude-boolean): **false**,<br>
131
+ &emsp;&emsp;[linearBoost](#linearboost-number): **1**,<br>
129
132
  &emsp;&emsp;[lineWidth](#linewidth-number): **0**,<br>
130
133
  &emsp;&emsp;[loRes](#lores-boolean): **false**,<br>
131
134
  &emsp;&emsp;[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 **octave bands** [modes](#mode-number).
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 **Graph** [mode](#mode-number), or inner fill of bars in **octave bands** modes when [`outlineBars`](#outlinebars-boolean) is *true*.
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 octave bands modes, [`alphaBars`](#alphabars-boolean) set to *true* takes precedence over `fillAlpha`.
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
- Currently selected color gradient used for analyzer graphs.
397
+ Name of the color gradient used for analyzer graphs.
376
398
 
377
- It must be the name of a built-in or [registered](#registergradient-name-options-) gradient. Built-in gradients are *'classic'*, *'prism'* and *'rainbow'*.
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 with the [LED effect](#ledbars-boolean) on.
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 octave bands modes, in which case [`lumiBars`](#lumibars-boolean) must be set to *false* or [`radial`](#radial-boolean) must be set to *true*.
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., [`ledBars`](#ledBars-boolean) is set to *true* and [`mode`](#mode-number) is set to an octave bands mode and [`radial`](#radial-boolean) is *false*.
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., [`lumiBars`](#lumibars-boolean) is set to *true* and [`mode`](#mode-number) is set to an octave bands mode and [`radial`](#radial-boolean) is *false*.
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 [`mode`](#mode-number) is set to one of the octave bands modes.
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., [`outlineBars`](#outlinebars-boolean) is set to *true*, [`mode`](#mode-number) is set to
436
- one of the octave bands modes and both [`ledBars`](#ledbars-boolean) and [`lumiBars`](#lumibars-boolean) are set to *false*, or [`radial`](#radial-boolean) is set to *true*.
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 a vintage LEDs display effect. Only effective for **octave bands** [modes](#mode-number).
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 **Graph** [mode](#mode-number), or outline stroke in **octave bands** modes when [`outlineBars`](#outlinebars-boolean) is *true*.
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 **octave bands** [modes](#mode-number).
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
- Current visualization mode.
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 **octave bands** modes, draws outlined bars with customizable [`fillAlpha`](#fillalpha-number) and [`lineWidth`](#linewidth-number).
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
- | ![split-on](demo/media/splitGradient_on.png) | ![split-off](demo/media/splitGradient_off.png) |
783
+ | ![split-on](img/splitGradient_on.png) | ![split-off](img/splitGradient_off.png) |
754
784
 
755
- This option has no effect in horizontal gradients, or when [`stereo`](#stereo-boolean) is set to *false*.
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
- Select a [weighting filter](https://en.wikipedia.org/wiki/Weighting_filter) for spectrum visualization.
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
- ?> Selecting a weighting filter **does NOT** affect the audio output, only the visualization.
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
- **If called with no arguments or any invalid property, clears custom parameters previously set.**
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
- **audioMotion-analyzer** automatically unlocks the audio context on the first click on the page,
1190
- since browsers' autoplay policy requires audio output to be initiated on user gesture only.
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
- However, if you're using an `audio` or `video` element with the `controls` property, clicks on those native media
1193
- controls can't be detected, so the audio will only be enabled if/when the user clicks somewhere else.
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 'Play' and 'Stop' buttons. A very simple example:
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.1",
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.1
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.1';
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
- WEIGHTING_FILTERS = [ '', 'A', 'B', 'C', 'D', '468' ];
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
- // Gradient definitions
67
-
68
- this._gradients = {
69
- classic: {
70
- bgColor: GRADIENT_DEFAULT_BGCOLOR,
71
- colorStops: [
72
- 'hsl( 0, 100%, 50% )',
73
- { pos: .6, color: 'hsl( 60, 100%, 50% )' },
74
- 'hsl( 120, 100%, 50% )'
75
- ]
76
- },
77
- prism: {
78
- bgColor: GRADIENT_DEFAULT_BGCOLOR,
79
- colorStops: [
80
- 'hsl( 0, 100%, 50% )',
81
- 'hsl( 60, 100%, 50% )',
82
- 'hsl( 120, 100%, 50% )',
83
- 'hsl( 180, 100%, 50% )',
84
- 'hsl( 240, 100%, 50% )'
85
- ]
86
- },
87
- rainbow: {
88
- bgColor: GRADIENT_DEFAULT_BGCOLOR,
89
- dir: 'h',
90
- colorStops: [
91
- 'hsl( 0, 100%, 50% )',
92
- 'hsl( 60, 100%, 50% )',
93
- 'hsl( 120, 100%, 50% )',
94
- 'hsl( 180, 100%, 47% )',
95
- 'hsl( 240, 100%, 58% )',
96
- 'hsl( 300, 100%, 50% )',
97
- 'hsl( 360, 100%, 50% )'
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('resize');
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('fschange');
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('create');
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('user');
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('lores');
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('user');
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('user');
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._isOctaveBands = this._mode % 10 != 0;
899
- this._isLedDisplay = this._showLeds && this._isOctaveBands && ! isRadial;
900
- this._isLumiBars = this._lumiBars && this._isOctaveBands && ! isRadial;
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._isOctaveBands && ! this._isLumiBars && ! this._isLedDisplay;
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
- * Precalculate the actual X-coordinate on screen for each analyzer bar
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 minLog, logWidth;
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
- minLog = Math.log10( firstBar.freqLo );
1049
- logWidth = analyzerWidth / ( Math.log10( lastBar.freqHi ) - minLog );
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 minLog and logWidth, for the proper positioning of labels on the X-axis
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
- // Discrete frequencies modes
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
- minLog = Math.log10( minFreq );
1070
- logWidth = analyzerWidth / ( Math.log10( maxFreq ) - minLog );
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( logWidth * ( Math.log10( freq ) - minLog ) ); // avoid fractionary pixel values
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._minLog = minLog;
1097
- this._logWidth = logWidth;
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._isOctaveBands || ! this._ready )
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 && ! isNoteLabels )
1192
- freqLabels.push(16,31.5,63,125,250,500,1e3,2e3,4e3,8e3,16e3);
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 = `${ canvasX.height >> 1 }px ${FONT_FAMILY}`;
1243
- scaleR.font = `${ scaleHeight >> 1 }px ${FONT_FAMILY}`;
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._logWidth * ( Math.log10( freq ) - this._minLog ),
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 ? this._logWidth * ( isC ? .03 : .015 ) : 99;
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
- scaleX.fillText( label, initialX + x, y, maxW );
1257
- if ( x < analyzerWidth ) // avoid wrapping-around the last label and overlapping the first one
1258
- radialLabel( x, label );
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 ( isMirror ) {
1261
- scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
1262
- if ( x > 10 ) // avoid overlapping of first labels on mirror mode
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
- isOctaveBands = this._isOctaveBands,
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 'A' : // A-weighting https://en.wikipedia.org/wiki/A-weighting
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 'B' :
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 'C' :
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 'D' :
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 '468' : // ITU-R 468 https://en.wikipedia.org/wiki/ITU-R_468_noise_weighting
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 - ( ! isOctaveBands ? 0 : Math.max( isLedDisplay ? ledSpaceH : 0, this._barSpacePx ) );
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 lastBin = fftData.length - 1,
1489
- interpolate = ( bin, ratio ) => fftData[ bin ] + ( bin < lastBin ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
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
- // normalize barHeight
1543
- if ( isLedDisplay ) {
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 ] / 255 * maxBarHeight * ( ! isRadial || ! channel || - 1 ) + barHeight ) / 2;
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 ] / 255 * maxBarHeight : barHeight; // use previous FFT bin value, when available
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
- ctx.fillRect( posX, analyzerBottom - ledPosY( peak ), width, ledHeight );
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 = 'fschange';
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
- gradient : 'classic',
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?: boolean;
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 GradientColorStop = string | { pos: number; color: string };
76
+ export type FrequencyScale = "bark" | "linear" | "log" | "mel";
76
77
 
77
- type WeightingFilter = "" | "A" | "B" | "C" | "D" | "468";
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;