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 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 music player called [**audioMotion**](https://audiomotion.me), but I later decided
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), \~25kB minified
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
- ### Native ES6 module (ESM)
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
- ### npm project
93
+ ### In the browser using global variable
74
94
 
75
- Install as a dependency:
95
+ Load from Unpkg CDN:
76
96
 
77
- ```console
78
- $ npm i audiomotion-analyzer
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
  &emsp;&emsp;[channelLayout](#channellayout-string): **'single'**,<br>
134
152
  &emsp;&emsp;[colorMode](#colormode-string): **'gradient'**,<br>
135
153
  &emsp;&emsp;[connectSpeakers](#connectspeakers-boolean): **true**, // constructor only<br>
154
+ &emsp;&emsp;[fadePeaks](#fadepeaks-boolean): **false**,<br>
136
155
  &emsp;&emsp;[fftSize](#fftsize-number): **8192**,<br>
137
156
  &emsp;&emsp;[fillAlpha](#fillalpha-number): **1**,<br>
138
157
  &emsp;&emsp;[frequencyScale](#frequencyscale-string): **'log'**,<br>
@@ -140,7 +159,7 @@ options = {<br>
140
159
  &emsp;&emsp;[gradient](#gradient-string): **'classic'**,<br>
141
160
  &emsp;&emsp;[gradientLeft](#gradientleft-string): *undefined*,<br>
142
161
  &emsp;&emsp;[gradientRight](#gradientright-string): *undefined*,<br>
143
- &emsp;&emsp;[gravity](#gravity-number): **1**,<br>
162
+ &emsp;&emsp;[gravity](#gravity-number): **3.8**,<br>
144
163
  &emsp;&emsp;[height](#height-number): *undefined*,<br>
145
164
  &emsp;&emsp;[ledBars](#ledbars-boolean): **false**,<br>
146
165
  &emsp;&emsp;[linearAmplitude](#linearamplitude-boolean): **false**,<br>
@@ -160,6 +179,8 @@ options = {<br>
160
179
  &emsp;&emsp;[onCanvasResize](#oncanvasresize-function): *undefined*,<br>
161
180
  &emsp;&emsp;[outlineBars](#outlinebars-boolean): **false**,<br>
162
181
  &emsp;&emsp;[overlay](#overlay-boolean): **false**,<br>
182
+ &emsp;&emsp;[peakFadeTime](#peakfadetime-number): **750**,<br>
183
+ &emsp;&emsp;[peakHoldTime](#peakholdtime-number): **500**,<br>
163
184
  &emsp;&emsp;[peakLine](#peakline-boolean): **false**,<br>
164
185
  &emsp;&emsp;[radial](#radial-boolean): **false**,<br>
165
186
  &emsp;&emsp;[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
- Controls the acceleration of [peaks](#showpeaks-boolean) falling down.
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
- Defaults to **1**.
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-2024 [Henrique Avila Vianna](https://henriquevianna.com)<br>
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.0-beta.1
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.0-beta.1';
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: 1,
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: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
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
- holdFrames = _fps >> 1,
1742
- // number of frames in half a second
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 / (holdFrames * holdFrames / _gravity);
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, it becomes the "acceleration" for peak drop
2073
- if (bar.hold[channel] < 0) bar.peak[channel] += bar.hold[channel] / (holdFrames * holdFrames / _gravity);
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
- if (isLumi || isAlpha) _ctx.globalAlpha = barValue;else if (isOutline) _ctx.globalAlpha = fillAlpha;
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 peak = bar.peak[channel];
2171
- if (peak > 0 && showPeaks && !showPeakLine && !isLumi && posX >= initialX && posX < finalX) {
2172
- // set opacity
2173
- if (isOutline && _lineWidth > 0) _ctx.globalAlpha = 1;else if (isAlpha) _ctx.globalAlpha = peak;
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(peak);
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(peak);
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 - peak * maxBarHeight, width, 2);else if (_mode != MODE_GRAPH) {
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 = peak * maxBarHeight;
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.0-beta.1",
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.24.1",
47
- "@babel/core": "^7.24.3",
48
- "@babel/plugin-transform-modules-umd": "^7.24.1",
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.0-beta.1
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.0-beta.1';
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 : 1,
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: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
1294
- const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
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
- holdFrames = _fps >> 1, // number of frames in half a second
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 / ( holdFrames * holdFrames / _gravity );
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, it becomes the "acceleration" for peak drop
2152
- if ( bar.hold[ channel ] < 0 )
2153
- bar.peak[ channel ] += bar.hold[ channel ] / ( holdFrames * holdFrames / _gravity );
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
- if ( isLumi || isAlpha )
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 peak = bar.peak[ channel ];
2267
- if ( peak > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
2268
- // set opacity
2269
- if ( isOutline && _lineWidth > 0 )
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 = peak;
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( peak );
2330
+ setBarColor( peakValue );
2277
2331
 
2278
2332
  // render peak according to current mode / effect
2279
2333
  if ( isLeds ) {
2280
- const ledPeak = ledPosY( peak );
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 - peak * maxBarHeight, width, 2 );
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 = peak * maxBarHeight;
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: string;
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