audiomotion-analyzer 4.5.0-beta.1 → 4.5.1
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 -18
- package/dist/index.js +77 -23
- package/package.json +5 -6
- package/src/audioMotion-analyzer.js +83 -29
- package/src/index.d.ts +13 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
**audioMotion-analyzer** is a high-resolution real-time audio spectrum analyzer built upon **Web Audio** and **Canvas** JavaScript APIs.
|
|
5
5
|
|
|
6
|
-
It was originally conceived as part of my full-featured
|
|
6
|
+
It was originally conceived as part of my full-featured media player called [**audioMotion**](https://audiomotion.app), but I later decided
|
|
7
7
|
to make the spectrum analyzer available as a self-contained module, so other developers could use it in their own JS projects.
|
|
8
8
|
|
|
9
9
|
My goal is to make this the best looking, most accurate and customizable spectrum analyzer around, in a small-footprint and high-performance package.
|
|
@@ -35,7 +35,7 @@ What users are saying:
|
|
|
35
35
|
+ Additional effects: LED bars, luminance bars, mirroring and reflection, radial spectrum
|
|
36
36
|
+ Choose from 5 built-in color gradients or easily add your own!
|
|
37
37
|
+ Fullscreen support, ready for retina / HiDPI displays
|
|
38
|
-
+ Zero-dependency native ES6+ module (ESM), \~
|
|
38
|
+
+ Zero-dependency native ES6+ module (ESM), \~30kB minified
|
|
39
39
|
|
|
40
40
|
## Online demos
|
|
41
41
|
|
|
@@ -57,7 +57,27 @@ What users are saying:
|
|
|
57
57
|
|
|
58
58
|
## Usage
|
|
59
59
|
|
|
60
|
-
###
|
|
60
|
+
### Node.js project
|
|
61
|
+
|
|
62
|
+
Install via npm:
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
npm i audiomotion-analyzer
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use ES6 import:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Or CommonJS require:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const { AudioMotionAnalyzer } = require('audioMotion-analyzer');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### In the browser using native ES6 module (ESM)
|
|
61
81
|
|
|
62
82
|
Load from Skypack CDN:
|
|
63
83
|
|
|
@@ -70,19 +90,17 @@ Load from Skypack CDN:
|
|
|
70
90
|
|
|
71
91
|
Or download the [latest version](https://github.com/hvianna/audioMotion-analyzer/releases) and copy the `audioMotion-analyzer.js` file from the `src/` folder into your project folder.
|
|
72
92
|
|
|
73
|
-
###
|
|
93
|
+
### In the browser using global variable
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
Load from Unpkg CDN:
|
|
76
96
|
|
|
77
|
-
```
|
|
78
|
-
|
|
97
|
+
```html
|
|
98
|
+
<script src="https://unpkg.com/audiomotion-analyzer/dist"></script>
|
|
99
|
+
<script>
|
|
100
|
+
// available as AudioMotionAnalyzer global
|
|
101
|
+
</script>
|
|
79
102
|
```
|
|
80
103
|
|
|
81
|
-
Use ES6 import syntax:
|
|
82
|
-
|
|
83
|
-
```js
|
|
84
|
-
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
|
85
|
-
```
|
|
86
104
|
|
|
87
105
|
## Constructor
|
|
88
106
|
|
|
@@ -133,6 +151,7 @@ options = {<br>
|
|
|
133
151
|
  [channelLayout](#channellayout-string): **'single'**,<br>
|
|
134
152
|
  [colorMode](#colormode-string): **'gradient'**,<br>
|
|
135
153
|
  [connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
|
|
154
|
+
  [fadePeaks](#fadepeaks-boolean): **false**,<br>
|
|
136
155
|
  [fftSize](#fftsize-number): **8192**,<br>
|
|
137
156
|
  [fillAlpha](#fillalpha-number): **1**,<br>
|
|
138
157
|
  [frequencyScale](#frequencyscale-string): **'log'**,<br>
|
|
@@ -140,7 +159,7 @@ options = {<br>
|
|
|
140
159
|
  [gradient](#gradient-string): **'classic'**,<br>
|
|
141
160
|
  [gradientLeft](#gradientleft-string): *undefined*,<br>
|
|
142
161
|
  [gradientRight](#gradientright-string): *undefined*,<br>
|
|
143
|
-
  [gravity](#gravity-number): **
|
|
162
|
+
  [gravity](#gravity-number): **3.8**,<br>
|
|
144
163
|
  [height](#height-number): *undefined*,<br>
|
|
145
164
|
  [ledBars](#ledbars-boolean): **false**,<br>
|
|
146
165
|
  [linearAmplitude](#linearamplitude-boolean): **false**,<br>
|
|
@@ -160,6 +179,8 @@ options = {<br>
|
|
|
160
179
|
  [onCanvasResize](#oncanvasresize-function): *undefined*,<br>
|
|
161
180
|
  [outlineBars](#outlinebars-boolean): **false**,<br>
|
|
162
181
|
  [overlay](#overlay-boolean): **false**,<br>
|
|
182
|
+
  [peakFadeTime](#peakfadetime-number): **750**,<br>
|
|
183
|
+
  [peakHoldTime](#peakholdtime-number): **500**,<br>
|
|
163
184
|
  [peakLine](#peakline-boolean): **false**,<br>
|
|
164
185
|
  [radial](#radial-boolean): **false**,<br>
|
|
165
186
|
  [radialInvert](#radialinvert-boolean): **false**,<br>
|
|
@@ -401,6 +422,18 @@ By default, **audioMotion-analyzer** is connected to the *AudioContext* `destina
|
|
|
401
422
|
|
|
402
423
|
See also [`connectOutput()`](#connectoutput-node-).
|
|
403
424
|
|
|
425
|
+
### `fadePeaks` *boolean*
|
|
426
|
+
|
|
427
|
+
*Available since v4.5.0*
|
|
428
|
+
|
|
429
|
+
When *true*, peaks fade out instead of falling down. It has no effect when [`peakLine`](#peakline-boolean) is active.
|
|
430
|
+
|
|
431
|
+
Fade time can be customized via [`peakFadeTime`](#peakfadetime-number).
|
|
432
|
+
|
|
433
|
+
See also [`peakHoldTime`](#peakholdtime-number) and [`showPeaks`](#showpeaks-boolean).
|
|
434
|
+
|
|
435
|
+
Defaults to **false**.
|
|
436
|
+
|
|
404
437
|
### `fftSize` *number*
|
|
405
438
|
|
|
406
439
|
Number of samples used for the FFT performed by the [*AnalyzerNode*](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode).
|
|
@@ -503,11 +536,17 @@ See also [`gradient`](#gradient-string) and [`splitGradient`](#splitgradient-boo
|
|
|
503
536
|
|
|
504
537
|
*Available since v4.5.0*
|
|
505
538
|
|
|
506
|
-
|
|
539
|
+
Customize the acceleration of falling peaks.
|
|
507
540
|
|
|
508
|
-
It must be a number greater than zero. Invalid values are ignored and no error is thrown.
|
|
541
|
+
It must be a number **greater than zero,** representing _thousands of pixels per second squared_. Invalid values are ignored and no error is thrown.
|
|
509
542
|
|
|
510
|
-
|
|
543
|
+
With the default value and analyzer height of 1080px, a peak at maximum amplitude takes approximately 750ms to fall to zero.
|
|
544
|
+
|
|
545
|
+
You can use the [peak drop analysis tool](/tools/peak-drop.html) to see the decay curve for different values of gravity.
|
|
546
|
+
|
|
547
|
+
See also [`peakHoldTime`](#peakholdtime-number) and [`showPeaks`](#showpeaks-boolean).
|
|
548
|
+
|
|
549
|
+
Defaults to **3.8**.
|
|
511
550
|
|
|
512
551
|
### `height` *number*
|
|
513
552
|
### `width` *number*
|
|
@@ -779,6 +818,30 @@ Defaults to **false**.
|
|
|
779
818
|
|
|
780
819
|
?> In order to keep elements other than the canvas visible in fullscreen, you'll need to set the [`fsElement`](#fselement-htmlelement-object) property in the [constructor](#constructor) options.
|
|
781
820
|
|
|
821
|
+
### `peakFadeTime` *number*
|
|
822
|
+
|
|
823
|
+
*Available since v4.5.0*
|
|
824
|
+
|
|
825
|
+
Time in milliseconds for peaks to completely fade out, when [`fadePeaks`](#fadepeaks-boolean) is active.
|
|
826
|
+
|
|
827
|
+
It must be a number greater than or equal to zero. Invalid values are ignored and no error is thrown.
|
|
828
|
+
|
|
829
|
+
See also [`peakHoldTime`](#peakholdtime-number) and [`showPeaks`](#showpeaks-boolean).
|
|
830
|
+
|
|
831
|
+
Defaults to **750**.
|
|
832
|
+
|
|
833
|
+
### `peakHoldTime` *number*
|
|
834
|
+
|
|
835
|
+
*Available since v4.5.0*
|
|
836
|
+
|
|
837
|
+
Time in milliseconds for peaks to hold their value before they begin to fall or fade.
|
|
838
|
+
|
|
839
|
+
It must be a number greater than or equal to zero. Invalid values are ignored and no error is thrown.
|
|
840
|
+
|
|
841
|
+
See also [`fadePeaks`](#fadepeaks-boolean), [`gravity`](#gravity-number), [`peakFadeTime`](#peakfadetime-number) and [`showPeaks`](#showpeaks-boolean).
|
|
842
|
+
|
|
843
|
+
Defaults to **500**.
|
|
844
|
+
|
|
782
845
|
### `peakLine` *boolean*
|
|
783
846
|
|
|
784
847
|
*Available since v4.2.0*
|
|
@@ -919,7 +982,7 @@ and setting `showBgColor` to ***true*** will make the "unlit" LEDs visible inste
|
|
|
919
982
|
|
|
920
983
|
*true* to show amplitude peaks.
|
|
921
984
|
|
|
922
|
-
See also [`gravity`](#gravity-number) and [`peakLine`](#peakline-boolean).
|
|
985
|
+
See also [`gravity`](#gravity-number), [`peakFadeTime`](#peakfadetime-number), [`peakHoldTime`](#peakholdtime-number) and [`peakLine`](#peakline-boolean).
|
|
923
986
|
|
|
924
987
|
Defaults to **true**.
|
|
925
988
|
|
|
@@ -1551,5 +1614,5 @@ And if you're feeling generous, maybe:
|
|
|
1551
1614
|
|
|
1552
1615
|
## License
|
|
1553
1616
|
|
|
1554
|
-
audioMotion-analyzer copyright (c) 2018-
|
|
1617
|
+
audioMotion-analyzer copyright (c) 2018-2025 [Henrique Avila Vianna](https://henriquevianna.com)<br>
|
|
1555
1618
|
Licensed under the [GNU Affero General Public License, version 3 or later](https://www.gnu.org/licenses/agpl.html).
|
package/dist/index.js
CHANGED
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
* audioMotion-analyzer
|
|
22
22
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
23
23
|
*
|
|
24
|
-
* @version 4.5.
|
|
24
|
+
* @version 4.5.1
|
|
25
25
|
* @author Henrique Avila Vianna <hvianna@gmail.com> <https://henriquevianna.com>
|
|
26
26
|
* @license AGPL-3.0-or-later
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
const VERSION = '4.5.
|
|
29
|
+
const VERSION = '4.5.1';
|
|
30
30
|
|
|
31
31
|
// internal constants
|
|
32
32
|
const PI = Math.PI,
|
|
@@ -104,11 +104,12 @@
|
|
|
104
104
|
bgAlpha: 0.7,
|
|
105
105
|
channelLayout: CHANNEL_SINGLE,
|
|
106
106
|
colorMode: COLOR_GRADIENT,
|
|
107
|
+
fadePeaks: false,
|
|
107
108
|
fftSize: 8192,
|
|
108
109
|
fillAlpha: 1,
|
|
109
110
|
frequencyScale: SCALE_LOG,
|
|
110
111
|
gradient: GRADIENTS[0][0],
|
|
111
|
-
gravity:
|
|
112
|
+
gravity: 3.8,
|
|
112
113
|
height: undefined,
|
|
113
114
|
ledBars: false,
|
|
114
115
|
linearAmplitude: false,
|
|
@@ -126,6 +127,8 @@
|
|
|
126
127
|
noteLabels: false,
|
|
127
128
|
outlineBars: false,
|
|
128
129
|
overlay: false,
|
|
130
|
+
peakFadeTime: 750,
|
|
131
|
+
peakHoldTime: 500,
|
|
129
132
|
peakLine: false,
|
|
130
133
|
radial: false,
|
|
131
134
|
radialInvert: false,
|
|
@@ -366,8 +369,7 @@
|
|
|
366
369
|
|
|
367
370
|
// Resume audio context if in suspended state (browsers' autoplay policy)
|
|
368
371
|
const unlockContext = () => {
|
|
369
|
-
if (audioCtx.state == 'suspended') audioCtx.resume();
|
|
370
|
-
window.removeEventListener(EVENT_CLICK, unlockContext);
|
|
372
|
+
if (audioCtx.state == 'suspended') audioCtx.resume().then(() => window.removeEventListener(EVENT_CLICK, unlockContext));
|
|
371
373
|
};
|
|
372
374
|
window.addEventListener(EVENT_CLICK, unlockContext);
|
|
373
375
|
|
|
@@ -443,6 +445,12 @@
|
|
|
443
445
|
set colorMode(value) {
|
|
444
446
|
this._colorMode = validateFromList(value, [COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL]);
|
|
445
447
|
}
|
|
448
|
+
get fadePeaks() {
|
|
449
|
+
return this._fadePeaks;
|
|
450
|
+
}
|
|
451
|
+
set fadePeaks(value) {
|
|
452
|
+
this._fadePeaks = !!value;
|
|
453
|
+
}
|
|
446
454
|
get fftSize() {
|
|
447
455
|
return this._analyzer[0].fftSize;
|
|
448
456
|
}
|
|
@@ -599,6 +607,18 @@
|
|
|
599
607
|
this._outlineBars = !!value;
|
|
600
608
|
this._calcBars();
|
|
601
609
|
}
|
|
610
|
+
get peakFadeTime() {
|
|
611
|
+
return this._peakFadeTime;
|
|
612
|
+
}
|
|
613
|
+
set peakFadeTime(value) {
|
|
614
|
+
this._peakFadeTime = value >= 0 ? +value : this._peakFadeTime || DEFAULT_SETTINGS.peakFadeTime;
|
|
615
|
+
}
|
|
616
|
+
get peakHoldTime() {
|
|
617
|
+
return this._peakHoldTime;
|
|
618
|
+
}
|
|
619
|
+
set peakHoldTime(value) {
|
|
620
|
+
this._peakHoldTime = +value || 0;
|
|
621
|
+
}
|
|
602
622
|
get peakLine() {
|
|
603
623
|
return this._peakLine;
|
|
604
624
|
}
|
|
@@ -1223,12 +1243,27 @@
|
|
|
1223
1243
|
* unitWidth
|
|
1224
1244
|
*/
|
|
1225
1245
|
|
|
1226
|
-
// helper function
|
|
1227
|
-
// bar object:
|
|
1246
|
+
// helper function to add a bar to the bars array
|
|
1247
|
+
// bar object format:
|
|
1248
|
+
// {
|
|
1249
|
+
// posX,
|
|
1250
|
+
// freq,
|
|
1251
|
+
// freqLo,
|
|
1252
|
+
// freqHi,
|
|
1253
|
+
// binLo,
|
|
1254
|
+
// binHi,
|
|
1255
|
+
// ratioLo,
|
|
1256
|
+
// ratioHi,
|
|
1257
|
+
// peak, // peak value
|
|
1258
|
+
// hold, // peak hold frames (negative value indicates peak falling / fading)
|
|
1259
|
+
// alpha, // peak alpha (used by fadePeaks)
|
|
1260
|
+
// value // current bar value
|
|
1261
|
+
// }
|
|
1228
1262
|
const barsPush = args => bars.push({
|
|
1229
1263
|
...args,
|
|
1230
1264
|
peak: [0, 0],
|
|
1231
1265
|
hold: [0],
|
|
1266
|
+
alpha: [0],
|
|
1232
1267
|
value: [0]
|
|
1233
1268
|
});
|
|
1234
1269
|
|
|
@@ -1720,9 +1755,9 @@
|
|
|
1720
1755
|
_colorMode,
|
|
1721
1756
|
_ctx,
|
|
1722
1757
|
_energy,
|
|
1758
|
+
_fadePeaks,
|
|
1723
1759
|
fillAlpha,
|
|
1724
1760
|
_fps,
|
|
1725
|
-
_gravity,
|
|
1726
1761
|
_linearAmplitude,
|
|
1727
1762
|
_lineWidth,
|
|
1728
1763
|
maxDecibels,
|
|
@@ -1738,8 +1773,10 @@
|
|
|
1738
1773
|
} = this,
|
|
1739
1774
|
canvasX = this._scaleX.canvas,
|
|
1740
1775
|
canvasR = this._scaleR.canvas,
|
|
1741
|
-
|
|
1742
|
-
|
|
1776
|
+
fadeFrames = _fps * this._peakFadeTime / 1e3,
|
|
1777
|
+
fpsSquared = _fps ** 2,
|
|
1778
|
+
gravity = this._gravity * 1e3,
|
|
1779
|
+
holdFrames = _fps * this._peakHoldTime / 1e3,
|
|
1743
1780
|
isDualCombined = _chLayout == CHANNEL_COMBINED,
|
|
1744
1781
|
isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
|
|
1745
1782
|
isDualVertical = _chLayout == CHANNEL_VERTICAL,
|
|
@@ -1749,6 +1786,8 @@
|
|
|
1749
1786
|
finalX = initialX + analyzerWidth,
|
|
1750
1787
|
showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
|
|
1751
1788
|
maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
|
|
1789
|
+
nominalMaxHeight = maxBarHeight / this._pixelRatio,
|
|
1790
|
+
// for consistent gravity on lo-res or hi-dpi
|
|
1752
1791
|
dbRange = maxDecibels - minDecibels,
|
|
1753
1792
|
[ledCount, ledSpaceH, ledSpaceV, ledHeight] = this._leds || [];
|
|
1754
1793
|
if (_energy.val > 0 && _fps > 0) this._spinAngle += this._spinSpeed * TAU / 60 / _fps; // spinSpeed * angle increment per frame for 1 RPM
|
|
@@ -1853,7 +1892,8 @@
|
|
|
1853
1892
|
_energy.val = newVal;
|
|
1854
1893
|
if (_energy.peak > 0) {
|
|
1855
1894
|
_energy.hold--;
|
|
1856
|
-
if (_energy.hold < 0) _energy.peak += _energy.hold
|
|
1895
|
+
if (_energy.hold < 0) _energy.peak += _energy.hold * gravity / fpsSquared / canvas.height * this._pixelRatio;
|
|
1896
|
+
// TO-DO: replace `canvas.height * this._pixelRatio` with `maxNominalHeight` when implementing dual-channel energy
|
|
1857
1897
|
}
|
|
1858
1898
|
if (newVal >= _energy.peak) {
|
|
1859
1899
|
_energy.peak = newVal;
|
|
@@ -2067,23 +2107,32 @@
|
|
|
2067
2107
|
currentEnergy += barValue;
|
|
2068
2108
|
|
|
2069
2109
|
// update bar peak
|
|
2070
|
-
if (bar.peak[channel] > 0) {
|
|
2110
|
+
if (bar.peak[channel] > 0 && bar.alpha[channel] > 0) {
|
|
2071
2111
|
bar.hold[channel]--;
|
|
2072
|
-
// if hold is negative,
|
|
2073
|
-
if (bar.hold[channel] < 0)
|
|
2112
|
+
// if hold is negative, start peak drop or fade out
|
|
2113
|
+
if (bar.hold[channel] < 0) {
|
|
2114
|
+
if (_fadePeaks && !showPeakLine) {
|
|
2115
|
+
const initialAlpha = !isAlpha || isOutline && _lineWidth > 0 ? 1 : isAlpha ? bar.peak[channel] : fillAlpha;
|
|
2116
|
+
bar.alpha[channel] = initialAlpha * (1 + bar.hold[channel] / fadeFrames); // hold is negative, so this is <= 1
|
|
2117
|
+
} else bar.peak[channel] += bar.hold[channel] * gravity / fpsSquared / nominalMaxHeight;
|
|
2118
|
+
// make sure the peak value is reset when using fadePeaks
|
|
2119
|
+
if (bar.alpha[channel] <= 0) bar.peak[channel] = 0;
|
|
2120
|
+
}
|
|
2074
2121
|
}
|
|
2075
2122
|
|
|
2076
2123
|
// check if it's a new peak for this bar
|
|
2077
2124
|
if (barValue >= bar.peak[channel]) {
|
|
2078
2125
|
bar.peak[channel] = barValue;
|
|
2079
2126
|
bar.hold[channel] = holdFrames;
|
|
2127
|
+
// check whether isAlpha or isOutline are active to start the peak alpha with the proper value
|
|
2128
|
+
bar.alpha[channel] = !isAlpha || isOutline && _lineWidth > 0 ? 1 : isAlpha ? barValue : fillAlpha;
|
|
2080
2129
|
}
|
|
2081
2130
|
|
|
2082
2131
|
// if not using the canvas, move earlier to the next bar
|
|
2083
2132
|
if (!useCanvas) continue;
|
|
2084
2133
|
|
|
2085
2134
|
// set opacity for bar effects
|
|
2086
|
-
|
|
2135
|
+
_ctx.globalAlpha = isLumi || isAlpha ? barValue : isOutline ? fillAlpha : 1;
|
|
2087
2136
|
|
|
2088
2137
|
// set fillStyle and strokeStyle for the current bar
|
|
2089
2138
|
setBarColor(barValue, barIndex);
|
|
@@ -2167,23 +2216,28 @@
|
|
|
2167
2216
|
}
|
|
2168
2217
|
|
|
2169
2218
|
// Draw peak
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2219
|
+
const peakValue = bar.peak[channel],
|
|
2220
|
+
peakAlpha = bar.alpha[channel];
|
|
2221
|
+
if (peakValue > 0 && peakAlpha > 0 && showPeaks && !showPeakLine && !isLumi && posX >= initialX && posX < finalX) {
|
|
2222
|
+
// set opacity for peak
|
|
2223
|
+
if (_fadePeaks) _ctx.globalAlpha = peakAlpha;else if (isOutline && _lineWidth > 0)
|
|
2224
|
+
// when lineWidth == 0 ctx.globalAlpha remains set to `fillAlpha`
|
|
2225
|
+
_ctx.globalAlpha = 1;else if (isAlpha)
|
|
2226
|
+
// isAlpha (alpha based on peak value) supersedes fillAlpha if lineWidth == 0
|
|
2227
|
+
_ctx.globalAlpha = peakValue;
|
|
2174
2228
|
|
|
2175
2229
|
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
2176
|
-
if (_colorMode == COLOR_BAR_LEVEL || isTrueLeds) setBarColor(
|
|
2230
|
+
if (_colorMode == COLOR_BAR_LEVEL || isTrueLeds) setBarColor(peakValue);
|
|
2177
2231
|
|
|
2178
2232
|
// render peak according to current mode / effect
|
|
2179
2233
|
if (isLeds) {
|
|
2180
|
-
const ledPeak = ledPosY(
|
|
2234
|
+
const ledPeak = ledPosY(peakValue);
|
|
2181
2235
|
if (ledPeak >= ledSpaceV)
|
|
2182
2236
|
// avoid peak below first led
|
|
2183
2237
|
_ctx.fillRect(posX, analyzerBottom - ledPeak, width, ledHeight);
|
|
2184
|
-
} else if (!_radial) _ctx.fillRect(posX, analyzerBottom -
|
|
2238
|
+
} else if (!_radial) _ctx.fillRect(posX, analyzerBottom - peakValue * maxBarHeight, width, 2);else if (_mode != MODE_GRAPH) {
|
|
2185
2239
|
// radial (peaks for graph mode are done by the peakLine code)
|
|
2186
|
-
const y =
|
|
2240
|
+
const y = peakValue * maxBarHeight;
|
|
2187
2241
|
radialPoly(posX, y, width, !this._radialInvert || isDualVertical || y + innerRadius >= 2 ? -2 : 2);
|
|
2188
2242
|
}
|
|
2189
2243
|
}
|
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.5.
|
|
4
|
+
"version": "4.5.1",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./src/audioMotion-analyzer.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/hvianna/audioMotion-analyzer"
|
|
30
|
+
"url": "git+https://github.com/hvianna/audioMotion-analyzer.git"
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"spectrum analyzer",
|
|
@@ -43,9 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://audiomotion.dev",
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@babel/cli": "^7.
|
|
47
|
-
"@babel/core": "^7.
|
|
48
|
-
"@babel/plugin-transform-modules-umd": "^7.
|
|
49
|
-
"@babel/preset-env": "^7.24.3"
|
|
46
|
+
"@babel/cli": "^7.27.2",
|
|
47
|
+
"@babel/core": "^7.27.4",
|
|
48
|
+
"@babel/plugin-transform-modules-umd": "^7.27.1"
|
|
50
49
|
}
|
|
51
50
|
}
|
|
@@ -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.5.
|
|
5
|
+
* @version 4.5.1
|
|
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.5.
|
|
10
|
+
const VERSION = '4.5.1';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
13
|
const PI = Math.PI,
|
|
@@ -88,11 +88,12 @@ const DEFAULT_SETTINGS = {
|
|
|
88
88
|
bgAlpha : 0.7,
|
|
89
89
|
channelLayout : CHANNEL_SINGLE,
|
|
90
90
|
colorMode : COLOR_GRADIENT,
|
|
91
|
+
fadePeaks : false,
|
|
91
92
|
fftSize : 8192,
|
|
92
93
|
fillAlpha : 1,
|
|
93
94
|
frequencyScale : SCALE_LOG,
|
|
94
95
|
gradient : GRADIENTS[0][0],
|
|
95
|
-
gravity :
|
|
96
|
+
gravity : 3.8,
|
|
96
97
|
height : undefined,
|
|
97
98
|
ledBars : false,
|
|
98
99
|
linearAmplitude: false,
|
|
@@ -110,6 +111,8 @@ const DEFAULT_SETTINGS = {
|
|
|
110
111
|
noteLabels : false,
|
|
111
112
|
outlineBars : false,
|
|
112
113
|
overlay : false,
|
|
114
|
+
peakFadeTime : 750,
|
|
115
|
+
peakHoldTime : 500,
|
|
113
116
|
peakLine : false,
|
|
114
117
|
radial : false,
|
|
115
118
|
radialInvert : false,
|
|
@@ -365,8 +368,7 @@ class AudioMotionAnalyzer {
|
|
|
365
368
|
// Resume audio context if in suspended state (browsers' autoplay policy)
|
|
366
369
|
const unlockContext = () => {
|
|
367
370
|
if ( audioCtx.state == 'suspended' )
|
|
368
|
-
audioCtx.resume();
|
|
369
|
-
window.removeEventListener( EVENT_CLICK, unlockContext );
|
|
371
|
+
audioCtx.resume().then( () => window.removeEventListener( EVENT_CLICK, unlockContext ) );
|
|
370
372
|
}
|
|
371
373
|
window.addEventListener( EVENT_CLICK, unlockContext );
|
|
372
374
|
|
|
@@ -446,6 +448,13 @@ class AudioMotionAnalyzer {
|
|
|
446
448
|
this._colorMode = validateFromList( value, [ COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL ] );
|
|
447
449
|
}
|
|
448
450
|
|
|
451
|
+
get fadePeaks() {
|
|
452
|
+
return this._fadePeaks;
|
|
453
|
+
}
|
|
454
|
+
set fadePeaks( value ) {
|
|
455
|
+
this._fadePeaks = !! value;
|
|
456
|
+
}
|
|
457
|
+
|
|
449
458
|
get fftSize() {
|
|
450
459
|
return this._analyzer[0].fftSize;
|
|
451
460
|
}
|
|
@@ -633,6 +642,20 @@ class AudioMotionAnalyzer {
|
|
|
633
642
|
this._calcBars();
|
|
634
643
|
}
|
|
635
644
|
|
|
645
|
+
get peakFadeTime() {
|
|
646
|
+
return this._peakFadeTime;
|
|
647
|
+
}
|
|
648
|
+
set peakFadeTime( value ) {
|
|
649
|
+
this._peakFadeTime = value >= 0 ? +value : this._peakFadeTime || DEFAULT_SETTINGS.peakFadeTime;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
get peakHoldTime() {
|
|
653
|
+
return this._peakHoldTime;
|
|
654
|
+
}
|
|
655
|
+
set peakHoldTime( value ) {
|
|
656
|
+
this._peakHoldTime = +value || 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
636
659
|
get peakLine() {
|
|
637
660
|
return this._peakLine;
|
|
638
661
|
}
|
|
@@ -1289,9 +1312,23 @@ class AudioMotionAnalyzer {
|
|
|
1289
1312
|
* unitWidth
|
|
1290
1313
|
*/
|
|
1291
1314
|
|
|
1292
|
-
// helper function
|
|
1293
|
-
// bar object:
|
|
1294
|
-
|
|
1315
|
+
// helper function to add a bar to the bars array
|
|
1316
|
+
// bar object format:
|
|
1317
|
+
// {
|
|
1318
|
+
// posX,
|
|
1319
|
+
// freq,
|
|
1320
|
+
// freqLo,
|
|
1321
|
+
// freqHi,
|
|
1322
|
+
// binLo,
|
|
1323
|
+
// binHi,
|
|
1324
|
+
// ratioLo,
|
|
1325
|
+
// ratioHi,
|
|
1326
|
+
// peak, // peak value
|
|
1327
|
+
// hold, // peak hold frames (negative value indicates peak falling / fading)
|
|
1328
|
+
// alpha, // peak alpha (used by fadePeaks)
|
|
1329
|
+
// value // current bar value
|
|
1330
|
+
// }
|
|
1331
|
+
const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], alpha: [0], value: [0] } );
|
|
1295
1332
|
|
|
1296
1333
|
/*
|
|
1297
1334
|
A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
|
|
@@ -1773,9 +1810,9 @@ class AudioMotionAnalyzer {
|
|
|
1773
1810
|
_colorMode,
|
|
1774
1811
|
_ctx,
|
|
1775
1812
|
_energy,
|
|
1813
|
+
_fadePeaks,
|
|
1776
1814
|
fillAlpha,
|
|
1777
1815
|
_fps,
|
|
1778
|
-
_gravity,
|
|
1779
1816
|
_linearAmplitude,
|
|
1780
1817
|
_lineWidth,
|
|
1781
1818
|
maxDecibels,
|
|
@@ -1791,7 +1828,10 @@ class AudioMotionAnalyzer {
|
|
|
1791
1828
|
|
|
1792
1829
|
canvasX = this._scaleX.canvas,
|
|
1793
1830
|
canvasR = this._scaleR.canvas,
|
|
1794
|
-
|
|
1831
|
+
fadeFrames = _fps * this._peakFadeTime / 1e3,
|
|
1832
|
+
fpsSquared = _fps ** 2,
|
|
1833
|
+
gravity = this._gravity * 1e3,
|
|
1834
|
+
holdFrames = _fps * this._peakHoldTime / 1e3,
|
|
1795
1835
|
isDualCombined = _chLayout == CHANNEL_COMBINED,
|
|
1796
1836
|
isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
|
|
1797
1837
|
isDualVertical = _chLayout == CHANNEL_VERTICAL,
|
|
@@ -1801,6 +1841,7 @@ class AudioMotionAnalyzer {
|
|
|
1801
1841
|
finalX = initialX + analyzerWidth,
|
|
1802
1842
|
showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
|
|
1803
1843
|
maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
|
|
1844
|
+
nominalMaxHeight = maxBarHeight / this._pixelRatio, // for consistent gravity on lo-res or hi-dpi
|
|
1804
1845
|
dbRange = maxDecibels - minDecibels,
|
|
1805
1846
|
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1806
1847
|
|
|
@@ -1918,7 +1959,8 @@ class AudioMotionAnalyzer {
|
|
|
1918
1959
|
if ( _energy.peak > 0 ) {
|
|
1919
1960
|
_energy.hold--;
|
|
1920
1961
|
if ( _energy.hold < 0 )
|
|
1921
|
-
_energy.peak += _energy.hold
|
|
1962
|
+
_energy.peak += _energy.hold * gravity / fpsSquared / canvas.height * this._pixelRatio;
|
|
1963
|
+
// TO-DO: replace `canvas.height * this._pixelRatio` with `maxNominalHeight` when implementing dual-channel energy
|
|
1922
1964
|
}
|
|
1923
1965
|
if ( newVal >= _energy.peak ) {
|
|
1924
1966
|
_energy.peak = newVal;
|
|
@@ -2146,17 +2188,28 @@ class AudioMotionAnalyzer {
|
|
|
2146
2188
|
currentEnergy += barValue;
|
|
2147
2189
|
|
|
2148
2190
|
// update bar peak
|
|
2149
|
-
if ( bar.peak[ channel ] > 0 ) {
|
|
2191
|
+
if ( bar.peak[ channel ] > 0 && bar.alpha[ channel ] > 0 ) {
|
|
2150
2192
|
bar.hold[ channel ]--;
|
|
2151
|
-
// if hold is negative,
|
|
2152
|
-
if ( bar.hold[ channel ] < 0 )
|
|
2153
|
-
|
|
2193
|
+
// if hold is negative, start peak drop or fade out
|
|
2194
|
+
if ( bar.hold[ channel ] < 0 ) {
|
|
2195
|
+
if ( _fadePeaks && ! showPeakLine ) {
|
|
2196
|
+
const initialAlpha = ! isAlpha || ( isOutline && _lineWidth > 0 ) ? 1 : isAlpha ? bar.peak[ channel ] : fillAlpha;
|
|
2197
|
+
bar.alpha[ channel ] = initialAlpha * ( 1 + bar.hold[ channel ] / fadeFrames ); // hold is negative, so this is <= 1
|
|
2198
|
+
}
|
|
2199
|
+
else
|
|
2200
|
+
bar.peak[ channel ] += bar.hold[ channel ] * gravity / fpsSquared / nominalMaxHeight;
|
|
2201
|
+
// make sure the peak value is reset when using fadePeaks
|
|
2202
|
+
if ( bar.alpha[ channel ] <= 0 )
|
|
2203
|
+
bar.peak[ channel ] = 0;
|
|
2204
|
+
}
|
|
2154
2205
|
}
|
|
2155
2206
|
|
|
2156
2207
|
// check if it's a new peak for this bar
|
|
2157
2208
|
if ( barValue >= bar.peak[ channel ] ) {
|
|
2158
2209
|
bar.peak[ channel ] = barValue;
|
|
2159
2210
|
bar.hold[ channel ] = holdFrames;
|
|
2211
|
+
// check whether isAlpha or isOutline are active to start the peak alpha with the proper value
|
|
2212
|
+
bar.alpha[ channel ] = ! isAlpha || ( isOutline && _lineWidth > 0 ) ? 1 : isAlpha ? barValue : fillAlpha;
|
|
2160
2213
|
}
|
|
2161
2214
|
|
|
2162
2215
|
// if not using the canvas, move earlier to the next bar
|
|
@@ -2164,10 +2217,7 @@ class AudioMotionAnalyzer {
|
|
|
2164
2217
|
continue;
|
|
2165
2218
|
|
|
2166
2219
|
// set opacity for bar effects
|
|
2167
|
-
|
|
2168
|
-
_ctx.globalAlpha = barValue;
|
|
2169
|
-
else if ( isOutline )
|
|
2170
|
-
_ctx.globalAlpha = fillAlpha;
|
|
2220
|
+
_ctx.globalAlpha = ( isLumi || isAlpha ) ? barValue : ( isOutline ) ? fillAlpha : 1;
|
|
2171
2221
|
|
|
2172
2222
|
// set fillStyle and strokeStyle for the current bar
|
|
2173
2223
|
setBarColor( barValue, barIndex );
|
|
@@ -2263,28 +2313,32 @@ class AudioMotionAnalyzer {
|
|
|
2263
2313
|
}
|
|
2264
2314
|
|
|
2265
2315
|
// Draw peak
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2316
|
+
const peakValue = bar.peak[ channel ],
|
|
2317
|
+
peakAlpha = bar.alpha[ channel ];
|
|
2318
|
+
|
|
2319
|
+
if ( peakValue > 0 && peakAlpha > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
|
|
2320
|
+
// set opacity for peak
|
|
2321
|
+
if ( _fadePeaks )
|
|
2322
|
+
_ctx.globalAlpha = peakAlpha;
|
|
2323
|
+
else if ( isOutline && _lineWidth > 0 ) // when lineWidth == 0 ctx.globalAlpha remains set to `fillAlpha`
|
|
2270
2324
|
_ctx.globalAlpha = 1;
|
|
2271
|
-
else if ( isAlpha )
|
|
2272
|
-
_ctx.globalAlpha =
|
|
2325
|
+
else if ( isAlpha ) // isAlpha (alpha based on peak value) supersedes fillAlpha if lineWidth == 0
|
|
2326
|
+
_ctx.globalAlpha = peakValue;
|
|
2273
2327
|
|
|
2274
2328
|
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
2275
2329
|
if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
|
|
2276
|
-
setBarColor(
|
|
2330
|
+
setBarColor( peakValue );
|
|
2277
2331
|
|
|
2278
2332
|
// render peak according to current mode / effect
|
|
2279
2333
|
if ( isLeds ) {
|
|
2280
|
-
const ledPeak = ledPosY(
|
|
2334
|
+
const ledPeak = ledPosY( peakValue );
|
|
2281
2335
|
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
2282
2336
|
_ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
2283
2337
|
}
|
|
2284
2338
|
else if ( ! _radial )
|
|
2285
|
-
_ctx.fillRect( posX, analyzerBottom -
|
|
2339
|
+
_ctx.fillRect( posX, analyzerBottom - peakValue * maxBarHeight, width, 2 );
|
|
2286
2340
|
else if ( _mode != MODE_GRAPH ) { // radial (peaks for graph mode are done by the peakLine code)
|
|
2287
|
-
const y =
|
|
2341
|
+
const y = peakValue * maxBarHeight;
|
|
2288
2342
|
radialPoly( posX, y, width, ! this._radialInvert || isDualVertical || y + innerRadius >= 2 ? -2 : 2 );
|
|
2289
2343
|
}
|
|
2290
2344
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface Options {
|
|
|
22
22
|
bgAlpha?: number;
|
|
23
23
|
channelLayout?: ChannelLayout;
|
|
24
24
|
colorMode?: ColorMode;
|
|
25
|
+
fadePeaks?: boolean;
|
|
25
26
|
fftSize?: number;
|
|
26
27
|
fillAlpha?: number;
|
|
27
28
|
frequencyScale?: FrequencyScale;
|
|
@@ -48,6 +49,8 @@ export interface Options {
|
|
|
48
49
|
onCanvasResize?: OnCanvasResizeFunction;
|
|
49
50
|
outlineBars?: boolean;
|
|
50
51
|
overlay?: boolean;
|
|
52
|
+
peakFadeTime?: number;
|
|
53
|
+
peakHoldTime?: number;
|
|
51
54
|
peakLine?: boolean;
|
|
52
55
|
radial?: boolean;
|
|
53
56
|
radialInvert?: boolean;
|
|
@@ -105,7 +108,7 @@ export type GradientColorStop = string | { pos?: number; color: string; level?:
|
|
|
105
108
|
export type WeightingFilter = "" | "A" | "B" | "C" | "D" | "468";
|
|
106
109
|
|
|
107
110
|
export interface GradientOptions {
|
|
108
|
-
bgColor
|
|
111
|
+
bgColor?: string;
|
|
109
112
|
dir?: "h";
|
|
110
113
|
colorStops: GradientColorStop[];
|
|
111
114
|
}
|
|
@@ -144,6 +147,9 @@ declare class AudioMotionAnalyzer {
|
|
|
144
147
|
get connectedSources(): AudioNode[];
|
|
145
148
|
get connectedTo(): AudioNode[];
|
|
146
149
|
|
|
150
|
+
get fadePeaks(): boolean;
|
|
151
|
+
set fadePeaks(value: boolean);
|
|
152
|
+
|
|
147
153
|
get fftSize(): number;
|
|
148
154
|
set fftSize(value: number);
|
|
149
155
|
|
|
@@ -234,6 +240,12 @@ declare class AudioMotionAnalyzer {
|
|
|
234
240
|
|
|
235
241
|
public overlay: boolean;
|
|
236
242
|
|
|
243
|
+
get peakFadeTime(): number;
|
|
244
|
+
set peakFadeTime(value: number);
|
|
245
|
+
|
|
246
|
+
get peakHoldTime(): number;
|
|
247
|
+
set peakHoldTime(value: number);
|
|
248
|
+
|
|
237
249
|
get peakLine(): boolean;
|
|
238
250
|
set peakLine(value: boolean);
|
|
239
251
|
|