audiomotion-analyzer 4.5.0-beta.1 → 4.5.0

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
 
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.0
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.0';
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,
@@ -443,6 +446,12 @@
443
446
  set colorMode(value) {
444
447
  this._colorMode = validateFromList(value, [COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL]);
445
448
  }
449
+ get fadePeaks() {
450
+ return this._fadePeaks;
451
+ }
452
+ set fadePeaks(value) {
453
+ this._fadePeaks = !!value;
454
+ }
446
455
  get fftSize() {
447
456
  return this._analyzer[0].fftSize;
448
457
  }
@@ -599,6 +608,18 @@
599
608
  this._outlineBars = !!value;
600
609
  this._calcBars();
601
610
  }
611
+ get peakFadeTime() {
612
+ return this._peakFadeTime;
613
+ }
614
+ set peakFadeTime(value) {
615
+ this._peakFadeTime = value >= 0 ? +value : this._peakFadeTime || DEFAULT_SETTINGS.peakFadeTime;
616
+ }
617
+ get peakHoldTime() {
618
+ return this._peakHoldTime;
619
+ }
620
+ set peakHoldTime(value) {
621
+ this._peakHoldTime = +value || 0;
622
+ }
602
623
  get peakLine() {
603
624
  return this._peakLine;
604
625
  }
@@ -1223,12 +1244,27 @@
1223
1244
  * unitWidth
1224
1245
  */
1225
1246
 
1226
- // helper function
1227
- // bar object: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
1247
+ // helper function to add a bar to the bars array
1248
+ // bar object format:
1249
+ // {
1250
+ // posX,
1251
+ // freq,
1252
+ // freqLo,
1253
+ // freqHi,
1254
+ // binLo,
1255
+ // binHi,
1256
+ // ratioLo,
1257
+ // ratioHi,
1258
+ // peak, // peak value
1259
+ // hold, // peak hold frames (negative value indicates peak falling / fading)
1260
+ // alpha, // peak alpha (used by fadePeaks)
1261
+ // value // current bar value
1262
+ // }
1228
1263
  const barsPush = args => bars.push({
1229
1264
  ...args,
1230
1265
  peak: [0, 0],
1231
1266
  hold: [0],
1267
+ alpha: [0],
1232
1268
  value: [0]
1233
1269
  });
1234
1270
 
@@ -1720,9 +1756,9 @@
1720
1756
  _colorMode,
1721
1757
  _ctx,
1722
1758
  _energy,
1759
+ _fadePeaks,
1723
1760
  fillAlpha,
1724
1761
  _fps,
1725
- _gravity,
1726
1762
  _linearAmplitude,
1727
1763
  _lineWidth,
1728
1764
  maxDecibels,
@@ -1738,8 +1774,10 @@
1738
1774
  } = this,
1739
1775
  canvasX = this._scaleX.canvas,
1740
1776
  canvasR = this._scaleR.canvas,
1741
- holdFrames = _fps >> 1,
1742
- // number of frames in half a second
1777
+ fadeFrames = _fps * this._peakFadeTime / 1e3,
1778
+ fpsSquared = _fps ** 2,
1779
+ gravity = this._gravity * 1e3,
1780
+ holdFrames = _fps * this._peakHoldTime / 1e3,
1743
1781
  isDualCombined = _chLayout == CHANNEL_COMBINED,
1744
1782
  isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
1745
1783
  isDualVertical = _chLayout == CHANNEL_VERTICAL,
@@ -1749,6 +1787,8 @@
1749
1787
  finalX = initialX + analyzerWidth,
1750
1788
  showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
1751
1789
  maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
1790
+ nominalMaxHeight = maxBarHeight / this._pixelRatio,
1791
+ // for consistent gravity on lo-res or hi-dpi
1752
1792
  dbRange = maxDecibels - minDecibels,
1753
1793
  [ledCount, ledSpaceH, ledSpaceV, ledHeight] = this._leds || [];
1754
1794
  if (_energy.val > 0 && _fps > 0) this._spinAngle += this._spinSpeed * TAU / 60 / _fps; // spinSpeed * angle increment per frame for 1 RPM
@@ -1853,7 +1893,8 @@
1853
1893
  _energy.val = newVal;
1854
1894
  if (_energy.peak > 0) {
1855
1895
  _energy.hold--;
1856
- if (_energy.hold < 0) _energy.peak += _energy.hold / (holdFrames * holdFrames / _gravity);
1896
+ if (_energy.hold < 0) _energy.peak += _energy.hold * gravity / fpsSquared / canvas.height * this._pixelRatio;
1897
+ // TO-DO: replace `canvas.height * this._pixelRatio` with `maxNominalHeight` when implementing dual-channel energy
1857
1898
  }
1858
1899
  if (newVal >= _energy.peak) {
1859
1900
  _energy.peak = newVal;
@@ -2067,23 +2108,32 @@
2067
2108
  currentEnergy += barValue;
2068
2109
 
2069
2110
  // update bar peak
2070
- if (bar.peak[channel] > 0) {
2111
+ if (bar.peak[channel] > 0 && bar.alpha[channel] > 0) {
2071
2112
  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);
2113
+ // if hold is negative, start peak drop or fade out
2114
+ if (bar.hold[channel] < 0) {
2115
+ if (_fadePeaks && !showPeakLine) {
2116
+ const initialAlpha = !isAlpha || isOutline && _lineWidth > 0 ? 1 : isAlpha ? bar.peak[channel] : fillAlpha;
2117
+ bar.alpha[channel] = initialAlpha * (1 + bar.hold[channel] / fadeFrames); // hold is negative, so this is <= 1
2118
+ } else bar.peak[channel] += bar.hold[channel] * gravity / fpsSquared / nominalMaxHeight;
2119
+ // make sure the peak value is reset when using fadePeaks
2120
+ if (bar.alpha[channel] <= 0) bar.peak[channel] = 0;
2121
+ }
2074
2122
  }
2075
2123
 
2076
2124
  // check if it's a new peak for this bar
2077
2125
  if (barValue >= bar.peak[channel]) {
2078
2126
  bar.peak[channel] = barValue;
2079
2127
  bar.hold[channel] = holdFrames;
2128
+ // check whether isAlpha or isOutline are active to start the peak alpha with the proper value
2129
+ bar.alpha[channel] = !isAlpha || isOutline && _lineWidth > 0 ? 1 : isAlpha ? barValue : fillAlpha;
2080
2130
  }
2081
2131
 
2082
2132
  // if not using the canvas, move earlier to the next bar
2083
2133
  if (!useCanvas) continue;
2084
2134
 
2085
2135
  // set opacity for bar effects
2086
- if (isLumi || isAlpha) _ctx.globalAlpha = barValue;else if (isOutline) _ctx.globalAlpha = fillAlpha;
2136
+ _ctx.globalAlpha = isLumi || isAlpha ? barValue : isOutline ? fillAlpha : 1;
2087
2137
 
2088
2138
  // set fillStyle and strokeStyle for the current bar
2089
2139
  setBarColor(barValue, barIndex);
@@ -2167,23 +2217,28 @@
2167
2217
  }
2168
2218
 
2169
2219
  // 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;
2220
+ const peakValue = bar.peak[channel],
2221
+ peakAlpha = bar.alpha[channel];
2222
+ if (peakValue > 0 && peakAlpha > 0 && showPeaks && !showPeakLine && !isLumi && posX >= initialX && posX < finalX) {
2223
+ // set opacity for peak
2224
+ if (_fadePeaks) _ctx.globalAlpha = peakAlpha;else if (isOutline && _lineWidth > 0)
2225
+ // when lineWidth == 0 ctx.globalAlpha remains set to `fillAlpha`
2226
+ _ctx.globalAlpha = 1;else if (isAlpha)
2227
+ // isAlpha (alpha based on peak value) supersedes fillAlpha if lineWidth == 0
2228
+ _ctx.globalAlpha = peakValue;
2174
2229
 
2175
2230
  // select the peak color for 'bar-level' colorMode or 'trueLeds'
2176
- if (_colorMode == COLOR_BAR_LEVEL || isTrueLeds) setBarColor(peak);
2231
+ if (_colorMode == COLOR_BAR_LEVEL || isTrueLeds) setBarColor(peakValue);
2177
2232
 
2178
2233
  // render peak according to current mode / effect
2179
2234
  if (isLeds) {
2180
- const ledPeak = ledPosY(peak);
2235
+ const ledPeak = ledPosY(peakValue);
2181
2236
  if (ledPeak >= ledSpaceV)
2182
2237
  // avoid peak below first led
2183
2238
  _ctx.fillRect(posX, analyzerBottom - ledPeak, width, ledHeight);
2184
- } else if (!_radial) _ctx.fillRect(posX, analyzerBottom - peak * maxBarHeight, width, 2);else if (_mode != MODE_GRAPH) {
2239
+ } else if (!_radial) _ctx.fillRect(posX, analyzerBottom - peakValue * maxBarHeight, width, 2);else if (_mode != MODE_GRAPH) {
2185
2240
  // radial (peaks for graph mode are done by the peakLine code)
2186
- const y = peak * maxBarHeight;
2241
+ const y = peakValue * maxBarHeight;
2187
2242
  radialPoly(posX, y, width, !this._radialInvert || isDualVertical || y + innerRadius >= 2 ? -2 : 2);
2188
2243
  }
2189
2244
  }
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.0",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./src/audioMotion-analyzer.js",
7
7
  "types": "./src/index.d.ts",
@@ -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.24.5",
47
+ "@babel/core": "^7.24.5",
48
+ "@babel/plugin-transform-modules-umd": "^7.24.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.0
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.0';
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,
@@ -446,6 +449,13 @@ class AudioMotionAnalyzer {
446
449
  this._colorMode = validateFromList( value, [ COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL ] );
447
450
  }
448
451
 
452
+ get fadePeaks() {
453
+ return this._fadePeaks;
454
+ }
455
+ set fadePeaks( value ) {
456
+ this._fadePeaks = !! value;
457
+ }
458
+
449
459
  get fftSize() {
450
460
  return this._analyzer[0].fftSize;
451
461
  }
@@ -633,6 +643,20 @@ class AudioMotionAnalyzer {
633
643
  this._calcBars();
634
644
  }
635
645
 
646
+ get peakFadeTime() {
647
+ return this._peakFadeTime;
648
+ }
649
+ set peakFadeTime( value ) {
650
+ this._peakFadeTime = value >= 0 ? +value : this._peakFadeTime || DEFAULT_SETTINGS.peakFadeTime;
651
+ }
652
+
653
+ get peakHoldTime() {
654
+ return this._peakHoldTime;
655
+ }
656
+ set peakHoldTime( value ) {
657
+ this._peakHoldTime = +value || 0;
658
+ }
659
+
636
660
  get peakLine() {
637
661
  return this._peakLine;
638
662
  }
@@ -1289,9 +1313,23 @@ class AudioMotionAnalyzer {
1289
1313
  * unitWidth
1290
1314
  */
1291
1315
 
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] } );
1316
+ // helper function to add a bar to the bars array
1317
+ // bar object format:
1318
+ // {
1319
+ // posX,
1320
+ // freq,
1321
+ // freqLo,
1322
+ // freqHi,
1323
+ // binLo,
1324
+ // binHi,
1325
+ // ratioLo,
1326
+ // ratioHi,
1327
+ // peak, // peak value
1328
+ // hold, // peak hold frames (negative value indicates peak falling / fading)
1329
+ // alpha, // peak alpha (used by fadePeaks)
1330
+ // value // current bar value
1331
+ // }
1332
+ const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], alpha: [0], value: [0] } );
1295
1333
 
1296
1334
  /*
1297
1335
  A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
@@ -1773,9 +1811,9 @@ class AudioMotionAnalyzer {
1773
1811
  _colorMode,
1774
1812
  _ctx,
1775
1813
  _energy,
1814
+ _fadePeaks,
1776
1815
  fillAlpha,
1777
1816
  _fps,
1778
- _gravity,
1779
1817
  _linearAmplitude,
1780
1818
  _lineWidth,
1781
1819
  maxDecibels,
@@ -1791,7 +1829,10 @@ class AudioMotionAnalyzer {
1791
1829
 
1792
1830
  canvasX = this._scaleX.canvas,
1793
1831
  canvasR = this._scaleR.canvas,
1794
- holdFrames = _fps >> 1, // number of frames in half a second
1832
+ fadeFrames = _fps * this._peakFadeTime / 1e3,
1833
+ fpsSquared = _fps ** 2,
1834
+ gravity = this._gravity * 1e3,
1835
+ holdFrames = _fps * this._peakHoldTime / 1e3,
1795
1836
  isDualCombined = _chLayout == CHANNEL_COMBINED,
1796
1837
  isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
1797
1838
  isDualVertical = _chLayout == CHANNEL_VERTICAL,
@@ -1801,6 +1842,7 @@ class AudioMotionAnalyzer {
1801
1842
  finalX = initialX + analyzerWidth,
1802
1843
  showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
1803
1844
  maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
1845
+ nominalMaxHeight = maxBarHeight / this._pixelRatio, // for consistent gravity on lo-res or hi-dpi
1804
1846
  dbRange = maxDecibels - minDecibels,
1805
1847
  [ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
1806
1848
 
@@ -1918,7 +1960,8 @@ class AudioMotionAnalyzer {
1918
1960
  if ( _energy.peak > 0 ) {
1919
1961
  _energy.hold--;
1920
1962
  if ( _energy.hold < 0 )
1921
- _energy.peak += _energy.hold / ( holdFrames * holdFrames / _gravity );
1963
+ _energy.peak += _energy.hold * gravity / fpsSquared / canvas.height * this._pixelRatio;
1964
+ // TO-DO: replace `canvas.height * this._pixelRatio` with `maxNominalHeight` when implementing dual-channel energy
1922
1965
  }
1923
1966
  if ( newVal >= _energy.peak ) {
1924
1967
  _energy.peak = newVal;
@@ -2146,17 +2189,28 @@ class AudioMotionAnalyzer {
2146
2189
  currentEnergy += barValue;
2147
2190
 
2148
2191
  // update bar peak
2149
- if ( bar.peak[ channel ] > 0 ) {
2192
+ if ( bar.peak[ channel ] > 0 && bar.alpha[ channel ] > 0 ) {
2150
2193
  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 );
2194
+ // if hold is negative, start peak drop or fade out
2195
+ if ( bar.hold[ channel ] < 0 ) {
2196
+ if ( _fadePeaks && ! showPeakLine ) {
2197
+ const initialAlpha = ! isAlpha || ( isOutline && _lineWidth > 0 ) ? 1 : isAlpha ? bar.peak[ channel ] : fillAlpha;
2198
+ bar.alpha[ channel ] = initialAlpha * ( 1 + bar.hold[ channel ] / fadeFrames ); // hold is negative, so this is <= 1
2199
+ }
2200
+ else
2201
+ bar.peak[ channel ] += bar.hold[ channel ] * gravity / fpsSquared / nominalMaxHeight;
2202
+ // make sure the peak value is reset when using fadePeaks
2203
+ if ( bar.alpha[ channel ] <= 0 )
2204
+ bar.peak[ channel ] = 0;
2205
+ }
2154
2206
  }
2155
2207
 
2156
2208
  // check if it's a new peak for this bar
2157
2209
  if ( barValue >= bar.peak[ channel ] ) {
2158
2210
  bar.peak[ channel ] = barValue;
2159
2211
  bar.hold[ channel ] = holdFrames;
2212
+ // check whether isAlpha or isOutline are active to start the peak alpha with the proper value
2213
+ bar.alpha[ channel ] = ! isAlpha || ( isOutline && _lineWidth > 0 ) ? 1 : isAlpha ? barValue : fillAlpha;
2160
2214
  }
2161
2215
 
2162
2216
  // if not using the canvas, move earlier to the next bar
@@ -2164,10 +2218,7 @@ class AudioMotionAnalyzer {
2164
2218
  continue;
2165
2219
 
2166
2220
  // set opacity for bar effects
2167
- if ( isLumi || isAlpha )
2168
- _ctx.globalAlpha = barValue;
2169
- else if ( isOutline )
2170
- _ctx.globalAlpha = fillAlpha;
2221
+ _ctx.globalAlpha = ( isLumi || isAlpha ) ? barValue : ( isOutline ) ? fillAlpha : 1;
2171
2222
 
2172
2223
  // set fillStyle and strokeStyle for the current bar
2173
2224
  setBarColor( barValue, barIndex );
@@ -2263,28 +2314,32 @@ class AudioMotionAnalyzer {
2263
2314
  }
2264
2315
 
2265
2316
  // 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 )
2317
+ const peakValue = bar.peak[ channel ],
2318
+ peakAlpha = bar.alpha[ channel ];
2319
+
2320
+ if ( peakValue > 0 && peakAlpha > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
2321
+ // set opacity for peak
2322
+ if ( _fadePeaks )
2323
+ _ctx.globalAlpha = peakAlpha;
2324
+ else if ( isOutline && _lineWidth > 0 ) // when lineWidth == 0 ctx.globalAlpha remains set to `fillAlpha`
2270
2325
  _ctx.globalAlpha = 1;
2271
- else if ( isAlpha )
2272
- _ctx.globalAlpha = peak;
2326
+ else if ( isAlpha ) // isAlpha (alpha based on peak value) supersedes fillAlpha if lineWidth == 0
2327
+ _ctx.globalAlpha = peakValue;
2273
2328
 
2274
2329
  // select the peak color for 'bar-level' colorMode or 'trueLeds'
2275
2330
  if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
2276
- setBarColor( peak );
2331
+ setBarColor( peakValue );
2277
2332
 
2278
2333
  // render peak according to current mode / effect
2279
2334
  if ( isLeds ) {
2280
- const ledPeak = ledPosY( peak );
2335
+ const ledPeak = ledPosY( peakValue );
2281
2336
  if ( ledPeak >= ledSpaceV ) // avoid peak below first led
2282
2337
  _ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
2283
2338
  }
2284
2339
  else if ( ! _radial )
2285
- _ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
2340
+ _ctx.fillRect( posX, analyzerBottom - peakValue * maxBarHeight, width, 2 );
2286
2341
  else if ( _mode != MODE_GRAPH ) { // radial (peaks for graph mode are done by the peakLine code)
2287
- const y = peak * maxBarHeight;
2342
+ const y = peakValue * maxBarHeight;
2288
2343
  radialPoly( posX, y, width, ! this._radialInvert || isDualVertical || y + innerRadius >= 2 ? -2 : 2 );
2289
2344
  }
2290
2345
  }
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;
@@ -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