audiomotion-analyzer 4.1.0 → 4.2.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 +107 -49
- package/package.json +6 -2
- package/src/audioMotion-analyzer.js +481 -319
- package/src/index.d.ts +16 -2
|
@@ -2,17 +2,16 @@
|
|
|
2
2
|
* audioMotion-analyzer
|
|
3
3
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
4
4
|
*
|
|
5
|
-
* @version 4.
|
|
5
|
+
* @version 4.2.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.
|
|
10
|
+
const VERSION = '4.2.0';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
13
|
const TAU = 2 * Math.PI,
|
|
14
14
|
HALF_PI = Math.PI / 2,
|
|
15
|
-
RPM = TAU / 3600, // angle increment per frame for one revolution per minute @60fps
|
|
16
15
|
C_1 = 8.17579892; // frequency for C -1
|
|
17
16
|
|
|
18
17
|
const CANVAS_BACKGROUND_COLOR = '#000',
|
|
@@ -22,6 +21,10 @@ const CANVAS_BACKGROUND_COLOR = '#000',
|
|
|
22
21
|
COLOR_BAR_INDEX = 'bar-index',
|
|
23
22
|
COLOR_BAR_LEVEL = 'bar-level',
|
|
24
23
|
COLOR_GRADIENT = 'gradient',
|
|
24
|
+
DEBOUNCE_TIMEOUT = 60,
|
|
25
|
+
EVENT_CLICK = 'click',
|
|
26
|
+
EVENT_FULLSCREENCHANGE = 'fullscreenchange',
|
|
27
|
+
EVENT_RESIZE = 'resize',
|
|
25
28
|
GRADIENT_DEFAULT_BGCOLOR = '#111',
|
|
26
29
|
FILTER_NONE = '',
|
|
27
30
|
FILTER_A = 'A',
|
|
@@ -35,7 +38,7 @@ const CANVAS_BACKGROUND_COLOR = '#000',
|
|
|
35
38
|
REASON_CREATE = 'create',
|
|
36
39
|
REASON_FSCHANGE = 'fschange',
|
|
37
40
|
REASON_LORES = 'lores',
|
|
38
|
-
REASON_RESIZE =
|
|
41
|
+
REASON_RESIZE = EVENT_RESIZE,
|
|
39
42
|
REASON_USER = 'user',
|
|
40
43
|
SCALEX_BACKGROUND_COLOR = '#000c',
|
|
41
44
|
SCALEX_LABEL_COLOR = '#fff',
|
|
@@ -102,6 +105,21 @@ const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated.
|
|
|
102
105
|
// returns the validated value, or the first element of `list` if `value` is not found in the array
|
|
103
106
|
const validateFromList = ( value, list, modifier = 'toLowerCase' ) => list[ Math.max( 0, list.indexOf( ( '' + value )[ modifier ]() ) ) ];
|
|
104
107
|
|
|
108
|
+
// helper function - find the Y-coordinate of a point located between two other points, given its X-coordinate
|
|
109
|
+
const findY = ( x1, y1, x2, y2, x ) => y1 + ( y2 - y1 ) * ( x - x1 ) / ( x2 - x1 );
|
|
110
|
+
|
|
111
|
+
// Polyfill for Array.findLastIndex()
|
|
112
|
+
if ( ! Array.prototype.findLastIndex ) {
|
|
113
|
+
Array.prototype.findLastIndex = function( callback ) {
|
|
114
|
+
let index = this.length;
|
|
115
|
+
while ( index-- > 0 ) {
|
|
116
|
+
if ( callback( this[ index ] ) )
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
105
123
|
// AudioMotionAnalyzer class
|
|
106
124
|
|
|
107
125
|
export default class AudioMotionAnalyzer {
|
|
@@ -118,11 +136,18 @@ export default class AudioMotionAnalyzer {
|
|
|
118
136
|
this._ready = false;
|
|
119
137
|
|
|
120
138
|
// Initialize internal objects
|
|
121
|
-
this._aux = {};
|
|
122
|
-
this.
|
|
139
|
+
this._aux = {}; // auxiliary variables
|
|
140
|
+
this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
|
|
141
|
+
this._destroyed = false;
|
|
142
|
+
this._energy = { val: 0, peak: 0, hold: 0 };
|
|
143
|
+
this._flg = {}; // flags
|
|
144
|
+
this._fps = 0;
|
|
123
145
|
this._gradients = {}; // registered gradients
|
|
146
|
+
this._last = 0; // timestamp of last rendered frame
|
|
147
|
+
this._outNodes = []; // output nodes
|
|
148
|
+
this._ownContext = false;
|
|
124
149
|
this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
|
|
125
|
-
this.
|
|
150
|
+
this._sources = []; // input nodes
|
|
126
151
|
|
|
127
152
|
// Register built-in gradients
|
|
128
153
|
for ( const [ name, options ] of GRADIENTS )
|
|
@@ -148,6 +173,7 @@ export default class AudioMotionAnalyzer {
|
|
|
148
173
|
else {
|
|
149
174
|
try {
|
|
150
175
|
audioCtx = new ( window.AudioContext || window.webkitAudioContext )();
|
|
176
|
+
this._ownContext = true;
|
|
151
177
|
}
|
|
152
178
|
catch( err ) {
|
|
153
179
|
throw new AudioMotionError( ERR_AUDIO_CONTEXT_FAIL );
|
|
@@ -162,13 +188,13 @@ export default class AudioMotionAnalyzer {
|
|
|
162
188
|
Connection routing:
|
|
163
189
|
===================
|
|
164
190
|
|
|
165
|
-
for dual channel
|
|
191
|
+
for dual channel layouts: +---> analyzer[0] ---+
|
|
166
192
|
| |
|
|
167
193
|
(source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
|
|
168
194
|
| |
|
|
169
195
|
+---> analyzer[1] ---+
|
|
170
196
|
|
|
171
|
-
for single channel
|
|
197
|
+
for single channel layout:
|
|
172
198
|
|
|
173
199
|
(source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
|
|
174
200
|
|
|
@@ -181,8 +207,7 @@ export default class AudioMotionAnalyzer {
|
|
|
181
207
|
this._input = audioCtx.createGain();
|
|
182
208
|
this._output = audioCtx.createGain();
|
|
183
209
|
|
|
184
|
-
//
|
|
185
|
-
this._sources = [];
|
|
210
|
+
// connect audio source if provided in the options
|
|
186
211
|
if ( options.source )
|
|
187
212
|
this.connectInput( options.source );
|
|
188
213
|
|
|
@@ -194,17 +219,13 @@ export default class AudioMotionAnalyzer {
|
|
|
194
219
|
merger.connect( this._output );
|
|
195
220
|
|
|
196
221
|
// connect output -> destination (speakers)
|
|
197
|
-
this._outNodes = [];
|
|
198
222
|
if ( options.connectSpeakers !== false )
|
|
199
223
|
this.connectOutput();
|
|
200
224
|
|
|
201
|
-
// initialize object to save energy
|
|
202
|
-
this._energy = { val: 0, peak: 0, hold: 0 };
|
|
203
|
-
|
|
204
225
|
// create analyzer canvas
|
|
205
226
|
const canvas = document.createElement('canvas');
|
|
206
227
|
canvas.style = 'max-width: 100%;';
|
|
207
|
-
this.
|
|
228
|
+
this._ctx = canvas.getContext('2d');
|
|
208
229
|
|
|
209
230
|
// create auxiliary canvases for the X-axis and radial scale labels
|
|
210
231
|
for ( const ctx of [ '_scaleX', '_scaleR' ] )
|
|
@@ -231,21 +252,25 @@ export default class AudioMotionAnalyzer {
|
|
|
231
252
|
this._setCanvas( REASON_RESIZE );
|
|
232
253
|
this._fsTimeout = 0;
|
|
233
254
|
}
|
|
234
|
-
},
|
|
255
|
+
}, DEBOUNCE_TIMEOUT );
|
|
235
256
|
}
|
|
236
257
|
}
|
|
237
258
|
|
|
238
259
|
// if browser supports ResizeObserver, listen for resize on the container
|
|
239
260
|
if ( window.ResizeObserver ) {
|
|
240
|
-
|
|
241
|
-
|
|
261
|
+
this._observer = new ResizeObserver( onResize );
|
|
262
|
+
this._observer.observe( this._container );
|
|
242
263
|
}
|
|
243
264
|
|
|
265
|
+
// create an AbortController to remove event listeners on destroy()
|
|
266
|
+
this._controller = new AbortController();
|
|
267
|
+
const signal = this._controller.signal;
|
|
268
|
+
|
|
244
269
|
// listen for resize events on the window - required for fullscreen on iPadOS
|
|
245
|
-
window.addEventListener(
|
|
270
|
+
window.addEventListener( EVENT_RESIZE, onResize, { signal } );
|
|
246
271
|
|
|
247
272
|
// listen for fullscreenchange events on the canvas - not available on Safari
|
|
248
|
-
canvas.addEventListener(
|
|
273
|
+
canvas.addEventListener( EVENT_FULLSCREENCHANGE, () => {
|
|
249
274
|
// set flag to indicate a fullscreen change in progress
|
|
250
275
|
this._fsChanging = true;
|
|
251
276
|
|
|
@@ -260,16 +285,24 @@ export default class AudioMotionAnalyzer {
|
|
|
260
285
|
this._fsTimeout = window.setTimeout( () => {
|
|
261
286
|
this._fsChanging = false;
|
|
262
287
|
this._fsTimeout = 0;
|
|
263
|
-
},
|
|
264
|
-
});
|
|
288
|
+
}, DEBOUNCE_TIMEOUT );
|
|
289
|
+
}, { signal } );
|
|
265
290
|
|
|
266
291
|
// Resume audio context if in suspended state (browsers' autoplay policy)
|
|
267
292
|
const unlockContext = () => {
|
|
268
293
|
if ( audioCtx.state == 'suspended' )
|
|
269
294
|
audioCtx.resume();
|
|
270
|
-
window.removeEventListener(
|
|
295
|
+
window.removeEventListener( EVENT_CLICK, unlockContext );
|
|
271
296
|
}
|
|
272
|
-
window.addEventListener(
|
|
297
|
+
window.addEventListener( EVENT_CLICK, unlockContext );
|
|
298
|
+
|
|
299
|
+
// reset FPS-related variables when window becomes visible (avoid FPS drop due to frames not rendered while hidden)
|
|
300
|
+
document.addEventListener( 'visibilitychange', () => {
|
|
301
|
+
if ( document.visibilityState != 'hidden' ) {
|
|
302
|
+
this._frames = 0;
|
|
303
|
+
this._time = performance.now();
|
|
304
|
+
}
|
|
305
|
+
}, { signal } );
|
|
273
306
|
|
|
274
307
|
// Set configuration options and use defaults for any missing properties
|
|
275
308
|
this._setProps( options, true );
|
|
@@ -441,6 +474,13 @@ export default class AudioMotionAnalyzer {
|
|
|
441
474
|
this._analyzer[ i ].maxDecibels = value;
|
|
442
475
|
}
|
|
443
476
|
|
|
477
|
+
get maxFPS() {
|
|
478
|
+
return this._maxFPS;
|
|
479
|
+
}
|
|
480
|
+
set maxFPS( value ) {
|
|
481
|
+
this._maxFPS = value < 0 ? 0 : +value || 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
444
484
|
get maxFreq() {
|
|
445
485
|
return this._maxFreq;
|
|
446
486
|
}
|
|
@@ -512,6 +552,13 @@ export default class AudioMotionAnalyzer {
|
|
|
512
552
|
this._calcBars();
|
|
513
553
|
}
|
|
514
554
|
|
|
555
|
+
get peakLine() {
|
|
556
|
+
return this._peakLine;
|
|
557
|
+
}
|
|
558
|
+
set peakLine( value ) {
|
|
559
|
+
this._peakLine = !! value;
|
|
560
|
+
}
|
|
561
|
+
|
|
515
562
|
get radial() {
|
|
516
563
|
return this._radial;
|
|
517
564
|
}
|
|
@@ -613,10 +660,10 @@ export default class AudioMotionAnalyzer {
|
|
|
613
660
|
return this._input.context;
|
|
614
661
|
}
|
|
615
662
|
get canvas() {
|
|
616
|
-
return this.
|
|
663
|
+
return this._ctx.canvas;
|
|
617
664
|
}
|
|
618
665
|
get canvasCtx() {
|
|
619
|
-
return this.
|
|
666
|
+
return this._ctx;
|
|
620
667
|
}
|
|
621
668
|
get connectedSources() {
|
|
622
669
|
return this._sources;
|
|
@@ -639,8 +686,11 @@ export default class AudioMotionAnalyzer {
|
|
|
639
686
|
get isBandsMode() {
|
|
640
687
|
return this._flg.isBands;
|
|
641
688
|
}
|
|
689
|
+
get isDestroyed() {
|
|
690
|
+
return this._destroyed;
|
|
691
|
+
}
|
|
642
692
|
get isFullscreen() {
|
|
643
|
-
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
693
|
+
return this._fsEl && ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
644
694
|
}
|
|
645
695
|
get isLedBars() {
|
|
646
696
|
return this._flg.isLeds;
|
|
@@ -652,7 +702,7 @@ export default class AudioMotionAnalyzer {
|
|
|
652
702
|
return this._flg.isOctaves;
|
|
653
703
|
}
|
|
654
704
|
get isOn() {
|
|
655
|
-
return this._runId
|
|
705
|
+
return !! this._runId;
|
|
656
706
|
}
|
|
657
707
|
get isOutlineBars() {
|
|
658
708
|
return this._flg.isOutline;
|
|
@@ -717,12 +767,54 @@ export default class AudioMotionAnalyzer {
|
|
|
717
767
|
}
|
|
718
768
|
}
|
|
719
769
|
|
|
770
|
+
/**
|
|
771
|
+
* Destroys instance
|
|
772
|
+
*/
|
|
773
|
+
destroy() {
|
|
774
|
+
if ( ! this._ready )
|
|
775
|
+
return;
|
|
776
|
+
|
|
777
|
+
const { audioCtx, canvas, _controller, _input, _merger, _observer, _ownContext, _splitter } = this;
|
|
778
|
+
|
|
779
|
+
this._destroyed = true;
|
|
780
|
+
this._ready = false;
|
|
781
|
+
this.stop();
|
|
782
|
+
|
|
783
|
+
// remove event listeners
|
|
784
|
+
_controller.abort();
|
|
785
|
+
if ( _observer )
|
|
786
|
+
_observer.disconnect();
|
|
787
|
+
|
|
788
|
+
// clear callbacks and fullscreen element
|
|
789
|
+
this.onCanvasResize = null;
|
|
790
|
+
this.onCanvasDraw = null;
|
|
791
|
+
this._fsEl = null;
|
|
792
|
+
|
|
793
|
+
// disconnect audio nodes
|
|
794
|
+
this.disconnectInput();
|
|
795
|
+
this.disconnectOutput(); // also disconnects analyzer nodes
|
|
796
|
+
_input.disconnect();
|
|
797
|
+
_splitter.disconnect();
|
|
798
|
+
_merger.disconnect();
|
|
799
|
+
|
|
800
|
+
// if audio context is our own (not provided by the user), close it
|
|
801
|
+
if ( _ownContext )
|
|
802
|
+
audioCtx.close();
|
|
803
|
+
|
|
804
|
+
// remove canvas from the DOM
|
|
805
|
+
canvas.remove();
|
|
806
|
+
|
|
807
|
+
// reset flags
|
|
808
|
+
this._calcBars();
|
|
809
|
+
}
|
|
810
|
+
|
|
720
811
|
/**
|
|
721
812
|
* Disconnects audio sources from the analyzer
|
|
722
813
|
*
|
|
723
|
-
* @param [{object|array}] a connected AudioNode object or an array of such objects; if
|
|
814
|
+
* @param [{object|array}] a connected AudioNode object or an array of such objects; if falsy, all connected nodes are disconnected
|
|
815
|
+
* @param [{boolean}] if true, stops/releases audio tracks from disconnected media streams (e.g. microphone)
|
|
724
816
|
*/
|
|
725
|
-
disconnectInput( sources ) {
|
|
817
|
+
disconnectInput( sources, stopTracks ) {
|
|
726
818
|
if ( ! sources )
|
|
727
819
|
sources = Array.from( this._sources );
|
|
728
820
|
else if ( ! Array.isArray( sources ) )
|
|
@@ -730,6 +822,11 @@ export default class AudioMotionAnalyzer {
|
|
|
730
822
|
|
|
731
823
|
for ( const node of sources ) {
|
|
732
824
|
const idx = this._sources.indexOf( node );
|
|
825
|
+
if ( stopTracks && node.mediaStream ) {
|
|
826
|
+
for ( const track of node.mediaStream.getAudioTracks() ) {
|
|
827
|
+
track.stop();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
733
830
|
if ( idx >= 0 ) {
|
|
734
831
|
node.disconnect( this._input );
|
|
735
832
|
this._sources.splice( idx, 1 );
|
|
@@ -927,26 +1024,42 @@ export default class AudioMotionAnalyzer {
|
|
|
927
1024
|
}
|
|
928
1025
|
}
|
|
929
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* Start the analyzer
|
|
1029
|
+
*/
|
|
1030
|
+
start() {
|
|
1031
|
+
this.toggleAnalyzer( true );
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Stop the analyzer
|
|
1036
|
+
*/
|
|
1037
|
+
stop() {
|
|
1038
|
+
this.toggleAnalyzer( false );
|
|
1039
|
+
}
|
|
1040
|
+
|
|
930
1041
|
/**
|
|
931
1042
|
* Start / stop canvas animation
|
|
932
1043
|
*
|
|
933
|
-
* @param {boolean} [
|
|
934
|
-
* @returns {boolean} resulting
|
|
1044
|
+
* @param {boolean} [force] if undefined, inverts the current state
|
|
1045
|
+
* @returns {boolean} resulting state after the change
|
|
935
1046
|
*/
|
|
936
|
-
toggleAnalyzer(
|
|
937
|
-
const
|
|
1047
|
+
toggleAnalyzer( force ) {
|
|
1048
|
+
const hasStarted = this.isOn;
|
|
938
1049
|
|
|
939
|
-
if (
|
|
940
|
-
|
|
1050
|
+
if ( force === undefined )
|
|
1051
|
+
force = ! hasStarted;
|
|
941
1052
|
|
|
942
|
-
if
|
|
1053
|
+
// Stop the analyzer if it was already running and must be disabled
|
|
1054
|
+
if ( hasStarted && ! force ) {
|
|
943
1055
|
cancelAnimationFrame( this._runId );
|
|
944
|
-
this._runId =
|
|
1056
|
+
this._runId = 0;
|
|
945
1057
|
}
|
|
946
|
-
|
|
947
|
-
|
|
1058
|
+
// Start the analyzer if it was stopped and must be enabled
|
|
1059
|
+
else if ( ! hasStarted && force && ! this._destroyed ) {
|
|
1060
|
+
this._frames = 0;
|
|
948
1061
|
this._time = performance.now();
|
|
949
|
-
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1062
|
+
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) ); // arrow function preserves the scope of *this*
|
|
950
1063
|
}
|
|
951
1064
|
|
|
952
1065
|
return this.isOn;
|
|
@@ -964,6 +1077,8 @@ export default class AudioMotionAnalyzer {
|
|
|
964
1077
|
}
|
|
965
1078
|
else {
|
|
966
1079
|
const fsEl = this._fsEl;
|
|
1080
|
+
if ( ! fsEl )
|
|
1081
|
+
return;
|
|
967
1082
|
if ( fsEl.requestFullscreen )
|
|
968
1083
|
fsEl.requestFullscreen();
|
|
969
1084
|
else if ( fsEl.webkitRequestFullscreen )
|
|
@@ -992,8 +1107,10 @@ export default class AudioMotionAnalyzer {
|
|
|
992
1107
|
_calcBars() {
|
|
993
1108
|
const bars = this._bars = []; // initialize object property
|
|
994
1109
|
|
|
995
|
-
if ( ! this._ready )
|
|
1110
|
+
if ( ! this._ready ) {
|
|
1111
|
+
this._flg = { isAlpha: false, isBands: false, isLeds: false, isLumi: false, isOctaves: false, isOutline: false, isRound: false, noLedGap: false };
|
|
996
1112
|
return;
|
|
1113
|
+
}
|
|
997
1114
|
|
|
998
1115
|
const barSpace = this._barSpace,
|
|
999
1116
|
canvas = this.canvas,
|
|
@@ -1357,28 +1474,22 @@ export default class AudioMotionAnalyzer {
|
|
|
1357
1474
|
return;
|
|
1358
1475
|
|
|
1359
1476
|
const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
|
|
1360
|
-
canvas
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
canvasX = scaleX.canvas,
|
|
1364
|
-
canvasR = scaleR.canvas,
|
|
1477
|
+
{ canvas, _frequencyScale, _mirror, _noteLabels, _radial, _scaleX, _scaleR } = this,
|
|
1478
|
+
canvasX = _scaleX.canvas,
|
|
1479
|
+
canvasR = _scaleR.canvas,
|
|
1365
1480
|
freqLabels = [],
|
|
1366
|
-
frequencyScale= this._frequencyScale,
|
|
1367
|
-
isNoteLabels = this._noteLabels,
|
|
1368
|
-
isRadial = this._radial,
|
|
1369
1481
|
isVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
1370
|
-
mirror = this._mirror,
|
|
1371
1482
|
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1372
1483
|
scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
|
|
1373
1484
|
fontSizeX = canvasX.height >> 1,
|
|
1374
1485
|
fontSizeR = scaleHeight >> 1,
|
|
1375
|
-
labelWidthX = fontSizeX * (
|
|
1376
|
-
labelWidthR = fontSizeR * (
|
|
1486
|
+
labelWidthX = fontSizeX * ( _noteLabels ? .7 : 1.5 ),
|
|
1487
|
+
labelWidthR = fontSizeR * ( _noteLabels ? 1 : 2 ),
|
|
1377
1488
|
root12 = 2 ** ( 1 / 12 );
|
|
1378
1489
|
|
|
1379
|
-
if ( !
|
|
1490
|
+
if ( ! _noteLabels && ( this._ansiBands || _frequencyScale != SCALE_LOG ) ) {
|
|
1380
1491
|
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
1381
|
-
if (
|
|
1492
|
+
if ( _frequencyScale == SCALE_LINEAR )
|
|
1382
1493
|
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
|
|
1383
1494
|
else
|
|
1384
1495
|
freqLabels.push( 8e3, 16e3 );
|
|
@@ -1390,8 +1501,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1390
1501
|
if ( freq >= this._minFreq && freq <= this._maxFreq ) {
|
|
1391
1502
|
const pitch = scale[ note ],
|
|
1392
1503
|
isC = pitch == 'C';
|
|
1393
|
-
if ( ( pitch &&
|
|
1394
|
-
freqLabels.push(
|
|
1504
|
+
if ( ( pitch && _noteLabels && ! _mirror ) || isC )
|
|
1505
|
+
freqLabels.push( _noteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
|
|
1395
1506
|
}
|
|
1396
1507
|
freq *= root12;
|
|
1397
1508
|
}
|
|
@@ -1411,27 +1522,27 @@ export default class AudioMotionAnalyzer {
|
|
|
1411
1522
|
posX = radialY * Math.cos( adjAng ),
|
|
1412
1523
|
posY = radialY * Math.sin( adjAng );
|
|
1413
1524
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1525
|
+
_scaleR.save();
|
|
1526
|
+
_scaleR.translate( centerR + posX, centerR + posY );
|
|
1527
|
+
_scaleR.rotate( angle );
|
|
1528
|
+
_scaleR.fillText( label, 0, 0 );
|
|
1529
|
+
_scaleR.restore();
|
|
1419
1530
|
}
|
|
1420
1531
|
|
|
1421
1532
|
// clear scale canvas
|
|
1422
1533
|
canvasX.width |= 0;
|
|
1423
1534
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1535
|
+
_scaleX.fillStyle = _scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
|
|
1536
|
+
_scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1426
1537
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1538
|
+
_scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
|
|
1539
|
+
_scaleR.lineWidth = scaleHeight;
|
|
1540
|
+
_scaleR.stroke();
|
|
1430
1541
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1542
|
+
_scaleX.fillStyle = _scaleR.fillStyle = SCALEX_LABEL_COLOR;
|
|
1543
|
+
_scaleX.font = `${ fontSizeX }px ${FONT_FAMILY}`;
|
|
1544
|
+
_scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
|
|
1545
|
+
_scaleX.textAlign = _scaleR.textAlign = 'center';
|
|
1435
1546
|
|
|
1436
1547
|
let prevX = -labelWidthX / 4,
|
|
1437
1548
|
prevR = -labelWidthR;
|
|
@@ -1441,22 +1552,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1441
1552
|
x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
|
|
1442
1553
|
y = canvasX.height * .75,
|
|
1443
1554
|
isC = label[0] == 'C',
|
|
1444
|
-
maxW = fontSizeX * (
|
|
1555
|
+
maxW = fontSizeX * ( _noteLabels && ! _mirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1445
1556
|
|
|
1446
1557
|
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1447
|
-
|
|
1558
|
+
_scaleX.fillStyle = _scaleR.fillStyle = isC && ! _mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1448
1559
|
|
|
1449
1560
|
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1450
|
-
if (
|
|
1561
|
+
if ( _noteLabels ) {
|
|
1562
|
+
const isLog = _frequencyScale == SCALE_LOG,
|
|
1563
|
+
isLinear = _frequencyScale == SCALE_LINEAR;
|
|
1564
|
+
|
|
1451
1565
|
let allowedLabels = ['C'];
|
|
1452
|
-
|
|
1453
|
-
|
|
1566
|
+
|
|
1567
|
+
if ( isLog || freq > 2e3 || ( ! isLinear && freq > 250 ) ||
|
|
1568
|
+
( ( ! _radial || isVertical ) && ( ! isLinear && freq > 125 || freq > 1e3 ) ) )
|
|
1454
1569
|
allowedLabels.push('G');
|
|
1455
|
-
if (
|
|
1456
|
-
( ( !
|
|
1570
|
+
if ( isLog || freq > 4e3 || ( ! isLinear && freq > 500 ) ||
|
|
1571
|
+
( ( ! _radial || isVertical ) && ( ! isLinear && freq > 250 || freq > 2e3 ) ) )
|
|
1457
1572
|
allowedLabels.push('E');
|
|
1458
|
-
if (
|
|
1459
|
-
( ( !
|
|
1573
|
+
if ( isLinear && freq > 4e3 ||
|
|
1574
|
+
( ( ! _radial || isVertical ) && ( isLog || freq > 2e3 || ( ! isLinear && freq > 500 ) ) ) )
|
|
1460
1575
|
allowedLabels.push('D','F','A','B');
|
|
1461
1576
|
if ( ! allowedLabels.includes( label[0] ) )
|
|
1462
1577
|
continue; // skip this label
|
|
@@ -1464,16 +1579,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1464
1579
|
|
|
1465
1580
|
// linear scale
|
|
1466
1581
|
if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
|
|
1467
|
-
|
|
1468
|
-
if (
|
|
1469
|
-
|
|
1470
|
-
prevX = x + Math.min( maxW,
|
|
1582
|
+
_scaleX.fillText( label, initialX + x, y, maxW );
|
|
1583
|
+
if ( _mirror && ( x > labelWidthX || _mirror == 1 ) )
|
|
1584
|
+
_scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
|
|
1585
|
+
prevX = x + Math.min( maxW, _scaleX.measureText( label ).width ) / 2;
|
|
1471
1586
|
}
|
|
1472
1587
|
|
|
1473
1588
|
// radial scale
|
|
1474
1589
|
if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
|
|
1475
1590
|
radialLabel( x, label );
|
|
1476
|
-
if (
|
|
1591
|
+
if ( _mirror && ( x > labelWidthR || _mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
|
|
1477
1592
|
radialLabel( -x, label );
|
|
1478
1593
|
prevR = x;
|
|
1479
1594
|
}
|
|
@@ -1485,41 +1600,68 @@ export default class AudioMotionAnalyzer {
|
|
|
1485
1600
|
* this is called 60 times per second by requestAnimationFrame()
|
|
1486
1601
|
*/
|
|
1487
1602
|
_draw( timestamp ) {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1603
|
+
// schedule next canvas update
|
|
1604
|
+
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1605
|
+
|
|
1606
|
+
if ( this._maxFPS && ( timestamp - this._last < 1000 / this._maxFPS ) )
|
|
1607
|
+
return;
|
|
1608
|
+
|
|
1609
|
+
const { isAlpha,
|
|
1610
|
+
isBands,
|
|
1611
|
+
isLeds,
|
|
1612
|
+
isLumi,
|
|
1613
|
+
isOctaves,
|
|
1614
|
+
isOutline,
|
|
1615
|
+
isRound,
|
|
1616
|
+
noLedGap } = this._flg,
|
|
1617
|
+
|
|
1618
|
+
{ analyzerHeight,
|
|
1619
|
+
channelCoords,
|
|
1620
|
+
channelHeight,
|
|
1621
|
+
channelGap,
|
|
1622
|
+
initialX,
|
|
1623
|
+
radius } = this._aux,
|
|
1624
|
+
|
|
1625
|
+
{ _bars,
|
|
1626
|
+
canvas,
|
|
1627
|
+
_canvasGradients,
|
|
1628
|
+
_chLayout,
|
|
1629
|
+
_colorMode,
|
|
1630
|
+
_ctx,
|
|
1631
|
+
_energy,
|
|
1632
|
+
fillAlpha,
|
|
1633
|
+
_fps,
|
|
1634
|
+
_linearAmplitude,
|
|
1635
|
+
_lineWidth,
|
|
1636
|
+
maxDecibels,
|
|
1637
|
+
minDecibels,
|
|
1638
|
+
_mirror,
|
|
1639
|
+
_mode,
|
|
1640
|
+
overlay,
|
|
1641
|
+
_radial,
|
|
1642
|
+
showBgColor,
|
|
1643
|
+
showPeaks,
|
|
1644
|
+
useCanvas,
|
|
1645
|
+
_weightingFilter } = this,
|
|
1646
|
+
|
|
1492
1647
|
canvasX = this._scaleX.canvas,
|
|
1493
1648
|
canvasR = this._scaleR.canvas,
|
|
1494
|
-
canvasGradients= this._canvasGradients,
|
|
1495
1649
|
centerX = canvas.width >> 1,
|
|
1496
1650
|
centerY = canvas.height >> 1,
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
isRadial = this._radial,
|
|
1504
|
-
isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
|
|
1505
|
-
channelLayout = this._chLayout,
|
|
1506
|
-
lineWidth = this._lineWidth,
|
|
1507
|
-
mirrorMode = this._mirror,
|
|
1508
|
-
{ analyzerHeight, channelCoords,
|
|
1509
|
-
channelHeight, channelGap, initialX, radius } = this._aux,
|
|
1510
|
-
analyzerWidth = isRadial ? canvas.width : this._aux.analyzerWidth,
|
|
1651
|
+
holdFrames = _fps >> 1, // number of frames in half a second
|
|
1652
|
+
isDualVertical = _chLayout == CHANNEL_VERTICAL,
|
|
1653
|
+
isDualCombined = _chLayout == CHANNEL_COMBINED,
|
|
1654
|
+
isSingle = _chLayout == CHANNEL_SINGLE,
|
|
1655
|
+
isTrueLeds = isLeds && this._trueLeds && _colorMode == COLOR_GRADIENT,
|
|
1656
|
+
analyzerWidth = _radial ? canvas.width : this._aux.analyzerWidth,
|
|
1511
1657
|
finalX = initialX + analyzerWidth,
|
|
1512
|
-
|
|
1513
|
-
maxBarHeight =
|
|
1514
|
-
|
|
1515
|
-
mindB = this.minDecibels,
|
|
1516
|
-
dbRange = maxdB - mindB,
|
|
1517
|
-
useCanvas = this.useCanvas,
|
|
1518
|
-
weightingFilter= this._weightingFilter,
|
|
1658
|
+
showPeakLine = showPeaks && this._peakLine && _mode == 10,
|
|
1659
|
+
maxBarHeight = _radial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
|
|
1660
|
+
dbRange = maxDecibels - minDecibels,
|
|
1519
1661
|
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1520
1662
|
|
|
1521
|
-
if (
|
|
1522
|
-
this._spinAngle += this._spinSpeed * RPM
|
|
1663
|
+
if ( _energy.val > 0 )
|
|
1664
|
+
this._spinAngle += this._spinSpeed * TAU / ( 60 * _fps ); // spinSpeed * angle increment per frame for 1 RPM
|
|
1523
1665
|
|
|
1524
1666
|
/* HELPER FUNCTIONS */
|
|
1525
1667
|
|
|
@@ -1527,8 +1669,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1527
1669
|
const doReflex = channel => {
|
|
1528
1670
|
if ( this._reflexRatio > 0 && ! isLumi ) {
|
|
1529
1671
|
let posY, height;
|
|
1530
|
-
if ( this.reflexFit ||
|
|
1531
|
-
posY =
|
|
1672
|
+
if ( this.reflexFit || isDualVertical ) { // always fit reflex in vertical stereo mode
|
|
1673
|
+
posY = isDualVertical && channel == 0 ? channelHeight + channelGap : 0;
|
|
1532
1674
|
height = channelHeight - analyzerHeight;
|
|
1533
1675
|
}
|
|
1534
1676
|
else {
|
|
@@ -1536,34 +1678,34 @@ export default class AudioMotionAnalyzer {
|
|
|
1536
1678
|
height = analyzerHeight;
|
|
1537
1679
|
}
|
|
1538
1680
|
|
|
1539
|
-
|
|
1681
|
+
_ctx.save();
|
|
1540
1682
|
|
|
1541
1683
|
// set alpha and brightness for the reflection
|
|
1542
|
-
|
|
1684
|
+
_ctx.globalAlpha = this.reflexAlpha;
|
|
1543
1685
|
if ( this.reflexBright != 1 )
|
|
1544
|
-
|
|
1686
|
+
_ctx.filter = `brightness(${this.reflexBright})`;
|
|
1545
1687
|
|
|
1546
1688
|
// create the reflection
|
|
1547
|
-
|
|
1548
|
-
|
|
1689
|
+
_ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
|
|
1690
|
+
_ctx.drawImage( canvas, 0, channelCoords[ channel ].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
|
|
1549
1691
|
|
|
1550
|
-
|
|
1692
|
+
_ctx.restore();
|
|
1551
1693
|
}
|
|
1552
1694
|
}
|
|
1553
1695
|
|
|
1554
1696
|
// draw scale on X-axis
|
|
1555
1697
|
const drawScaleX = () => {
|
|
1556
1698
|
if ( this.showScaleX ) {
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
|
|
1699
|
+
if ( _radial ) {
|
|
1700
|
+
_ctx.save();
|
|
1701
|
+
_ctx.translate( centerX, centerY );
|
|
1560
1702
|
if ( this._spinSpeed )
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1703
|
+
_ctx.rotate( this._spinAngle + HALF_PI );
|
|
1704
|
+
_ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
|
|
1705
|
+
_ctx.restore();
|
|
1564
1706
|
}
|
|
1565
1707
|
else
|
|
1566
|
-
|
|
1708
|
+
_ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
|
|
1567
1709
|
}
|
|
1568
1710
|
}
|
|
1569
1711
|
|
|
@@ -1571,16 +1713,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1571
1713
|
const drawScaleY = channelTop => {
|
|
1572
1714
|
const scaleWidth = canvasX.height,
|
|
1573
1715
|
fontSize = scaleWidth >> 1,
|
|
1574
|
-
max =
|
|
1575
|
-
min =
|
|
1576
|
-
incr =
|
|
1716
|
+
max = _linearAmplitude ? 100 : maxDecibels,
|
|
1717
|
+
min = _linearAmplitude ? 0 : minDecibels,
|
|
1718
|
+
incr = _linearAmplitude ? 20 : 5,
|
|
1577
1719
|
interval = analyzerHeight / ( max - min );
|
|
1578
1720
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1721
|
+
_ctx.save();
|
|
1722
|
+
_ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1723
|
+
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1724
|
+
_ctx.textAlign = 'right';
|
|
1725
|
+
_ctx.lineWidth = 1;
|
|
1584
1726
|
|
|
1585
1727
|
for ( let val = max; val > min; val -= incr ) {
|
|
1586
1728
|
const posY = channelTop + ( max - val ) * interval,
|
|
@@ -1588,26 +1730,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1588
1730
|
|
|
1589
1731
|
if ( even ) {
|
|
1590
1732
|
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1591
|
-
if (
|
|
1592
|
-
|
|
1593
|
-
if (
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1733
|
+
if ( _mirror != -1 )
|
|
1734
|
+
_ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1735
|
+
if ( _mirror != 1 )
|
|
1736
|
+
_ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
|
|
1737
|
+
_ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1738
|
+
_ctx.setLineDash([2,4]);
|
|
1739
|
+
_ctx.lineDashOffset = 0;
|
|
1598
1740
|
}
|
|
1599
1741
|
else {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1742
|
+
_ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1743
|
+
_ctx.setLineDash([2,8]);
|
|
1744
|
+
_ctx.lineDashOffset = 1;
|
|
1603
1745
|
}
|
|
1604
1746
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1747
|
+
_ctx.beginPath();
|
|
1748
|
+
_ctx.moveTo( initialX + scaleWidth * even * ( _mirror != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1749
|
+
_ctx.lineTo( finalX - scaleWidth * even * ( _mirror != 1 ), ~~posY + .5 );
|
|
1750
|
+
_ctx.stroke();
|
|
1609
1751
|
}
|
|
1610
|
-
|
|
1752
|
+
_ctx.restore();
|
|
1611
1753
|
}
|
|
1612
1754
|
|
|
1613
1755
|
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
@@ -1620,7 +1762,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1620
1762
|
SQ12194 = 148693636,
|
|
1621
1763
|
linearTodB = value => 20 * Math.log10( value );
|
|
1622
1764
|
|
|
1623
|
-
switch (
|
|
1765
|
+
switch ( _weightingFilter ) {
|
|
1624
1766
|
case FILTER_A : // A-weighting https://en.wikipedia.org/wiki/A-weighting
|
|
1625
1767
|
const rA = ( SQ12194 * f2 ** 2 ) / ( ( f2 + SQ20_6 ) * Math.sqrt( ( f2 + SQ107_7 ) * ( f2 + SQ737_9 ) ) * ( f2 + SQ12194 ) );
|
|
1626
1768
|
return 2 + linearTodB( rA );
|
|
@@ -1650,19 +1792,19 @@ export default class AudioMotionAnalyzer {
|
|
|
1650
1792
|
|
|
1651
1793
|
// draws (stroke) a bar from x,y1 to x,y2
|
|
1652
1794
|
const strokeBar = ( x, y1, y2 ) => {
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1795
|
+
_ctx.beginPath();
|
|
1796
|
+
_ctx.moveTo( x, y1 );
|
|
1797
|
+
_ctx.lineTo( x, y2 );
|
|
1798
|
+
_ctx.stroke();
|
|
1657
1799
|
}
|
|
1658
1800
|
|
|
1659
1801
|
// conditionally strokes current path on canvas
|
|
1660
1802
|
const strokeIf = flag => {
|
|
1661
|
-
if ( flag &&
|
|
1662
|
-
const alpha =
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1803
|
+
if ( flag && _lineWidth ) {
|
|
1804
|
+
const alpha = _ctx.globalAlpha;
|
|
1805
|
+
_ctx.globalAlpha = 1;
|
|
1806
|
+
_ctx.stroke();
|
|
1807
|
+
_ctx.globalAlpha = alpha;
|
|
1666
1808
|
}
|
|
1667
1809
|
}
|
|
1668
1810
|
|
|
@@ -1670,7 +1812,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1670
1812
|
const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
|
|
1671
1813
|
|
|
1672
1814
|
// converts planar X,Y coordinates to radial coordinates
|
|
1673
|
-
const radialXY = ( x, y, dir ) => {
|
|
1815
|
+
const radialXY = ( x, y, dir = 1 ) => {
|
|
1674
1816
|
const height = radius + y,
|
|
1675
1817
|
angle = getAngle( x, dir );
|
|
1676
1818
|
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
@@ -1678,21 +1820,21 @@ export default class AudioMotionAnalyzer {
|
|
|
1678
1820
|
|
|
1679
1821
|
// draws a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
1680
1822
|
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1681
|
-
|
|
1682
|
-
for ( const dir of (
|
|
1823
|
+
_ctx.beginPath();
|
|
1824
|
+
for ( const dir of ( _mirror ? [1,-1] : [1] ) ) {
|
|
1683
1825
|
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1684
|
-
|
|
1685
|
-
|
|
1826
|
+
_ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1827
|
+
_ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1686
1828
|
if ( isRound )
|
|
1687
|
-
|
|
1829
|
+
_ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
|
|
1688
1830
|
else
|
|
1689
|
-
|
|
1690
|
-
|
|
1831
|
+
_ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1832
|
+
_ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1691
1833
|
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1692
|
-
|
|
1834
|
+
_ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
|
|
1693
1835
|
}
|
|
1694
1836
|
strokeIf( stroke );
|
|
1695
|
-
|
|
1837
|
+
_ctx.fill();
|
|
1696
1838
|
}
|
|
1697
1839
|
|
|
1698
1840
|
// converts a value in [0;1] range to a height in pixels that fits into the current LED elements
|
|
@@ -1700,35 +1842,36 @@ export default class AudioMotionAnalyzer {
|
|
|
1700
1842
|
|
|
1701
1843
|
// update energy information
|
|
1702
1844
|
const updateEnergy = newVal => {
|
|
1703
|
-
|
|
1704
|
-
if (
|
|
1705
|
-
|
|
1706
|
-
|
|
1845
|
+
_energy.val = newVal;
|
|
1846
|
+
if ( _energy.peak > 0 ) {
|
|
1847
|
+
_energy.hold--;
|
|
1848
|
+
if ( _energy.hold < 0 )
|
|
1849
|
+
_energy.peak += _energy.hold / ( holdFrames * holdFrames / 2 );
|
|
1707
1850
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
else if ( energy.peak > 0 )
|
|
1712
|
-
energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
|
|
1851
|
+
if ( newVal >= _energy.peak ) {
|
|
1852
|
+
_energy.peak = newVal;
|
|
1853
|
+
_energy.hold = holdFrames;
|
|
1713
1854
|
}
|
|
1714
1855
|
}
|
|
1715
1856
|
|
|
1716
1857
|
// calculate and display (if enabled) the current frame rate
|
|
1717
1858
|
const updateFPS = () => {
|
|
1718
|
-
this.
|
|
1719
|
-
|
|
1859
|
+
const elapsed = timestamp - this._time; // elapsed time since the last FPS computation
|
|
1860
|
+
|
|
1861
|
+
this._last = timestamp - ( this._maxFPS ? elapsed % ( 1000 / this._maxFPS ) : 0 ); // thanks https://stackoverflow.com/a/19772220/2370385
|
|
1862
|
+
this._frames++;
|
|
1720
1863
|
|
|
1721
1864
|
if ( elapsed >= 1000 ) {
|
|
1722
|
-
this._fps = this.
|
|
1723
|
-
this.
|
|
1865
|
+
this._fps = this._frames / ( elapsed / 1000 );
|
|
1866
|
+
this._frames = 0;
|
|
1724
1867
|
this._time = timestamp;
|
|
1725
1868
|
}
|
|
1726
1869
|
if ( this.showFPS ) {
|
|
1727
1870
|
const size = canvasX.height;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1871
|
+
_ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
1872
|
+
_ctx.fillStyle = FPS_COLOR;
|
|
1873
|
+
_ctx.textAlign = 'right';
|
|
1874
|
+
_ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
|
|
1732
1875
|
}
|
|
1733
1876
|
}
|
|
1734
1877
|
|
|
@@ -1736,9 +1879,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1736
1879
|
|
|
1737
1880
|
let currentEnergy = 0;
|
|
1738
1881
|
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1741
|
-
nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
|
|
1882
|
+
const nBars = _bars.length,
|
|
1883
|
+
nChannels = isSingle ? 1 : 2;
|
|
1742
1884
|
|
|
1743
1885
|
for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1744
1886
|
|
|
@@ -1746,8 +1888,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1746
1888
|
channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
1747
1889
|
colorStops = channelGradient.colorStops,
|
|
1748
1890
|
colorCount = colorStops.length,
|
|
1749
|
-
bgColor = ( ! showBgColor || isLeds && !
|
|
1750
|
-
mustClear = channel == 0 || !
|
|
1891
|
+
bgColor = ( ! showBgColor || isLeds && ! overlay ) ? '#000' : channelGradient.bgColor,
|
|
1892
|
+
mustClear = channel == 0 || ! _radial && ! isDualCombined,
|
|
1893
|
+
direction = channel && _radial && isDualVertical ? -1 : 1; // for radial dual vertical layout
|
|
1751
1894
|
|
|
1752
1895
|
// helper function for FFT data interpolation (uses fftData)
|
|
1753
1896
|
const interpolate = ( bin, ratio ) => {
|
|
@@ -1759,53 +1902,53 @@ export default class AudioMotionAnalyzer {
|
|
|
1759
1902
|
const setBarColor = ( value = 0, barIndex = 0 ) => {
|
|
1760
1903
|
let color;
|
|
1761
1904
|
// for mode 10, always use the channel gradient (ignore colorMode)
|
|
1762
|
-
if ( (
|
|
1763
|
-
color =
|
|
1905
|
+
if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode == 10 )
|
|
1906
|
+
color = _canvasGradients[ channel ];
|
|
1764
1907
|
else {
|
|
1765
|
-
const selectedIndex =
|
|
1908
|
+
const selectedIndex = _colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
|
|
1766
1909
|
color = colorStops[ selectedIndex ].color;
|
|
1767
1910
|
}
|
|
1768
|
-
|
|
1911
|
+
_ctx.fillStyle = _ctx.strokeStyle = color;
|
|
1769
1912
|
}
|
|
1770
1913
|
|
|
1771
1914
|
if ( useCanvas ) {
|
|
1772
1915
|
// clear the channel area, if in overlay mode
|
|
1773
1916
|
// this is done per channel to clear any residue below 0 off the top channel (especially in line graph mode with lineWidth > 1)
|
|
1774
|
-
if (
|
|
1775
|
-
|
|
1917
|
+
if ( overlay && mustClear )
|
|
1918
|
+
_ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
|
|
1776
1919
|
|
|
1777
1920
|
// fill the analyzer background if needed (not overlay or overlay + showBgColor)
|
|
1778
|
-
if ( !
|
|
1779
|
-
if (
|
|
1780
|
-
|
|
1921
|
+
if ( ! overlay || showBgColor ) {
|
|
1922
|
+
if ( overlay )
|
|
1923
|
+
_ctx.globalAlpha = this.bgAlpha;
|
|
1781
1924
|
|
|
1782
|
-
|
|
1925
|
+
_ctx.fillStyle = bgColor;
|
|
1783
1926
|
|
|
1784
1927
|
// exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
|
|
1785
1928
|
if ( mustClear )
|
|
1786
|
-
|
|
1929
|
+
_ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
|
|
1787
1930
|
|
|
1788
|
-
|
|
1931
|
+
_ctx.globalAlpha = 1;
|
|
1789
1932
|
}
|
|
1790
1933
|
|
|
1791
1934
|
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
1792
|
-
if ( this.showScaleY && ! isLumi && !
|
|
1935
|
+
if ( this.showScaleY && ! isLumi && ! _radial && ( channel == 0 || ! isDualCombined ) )
|
|
1793
1936
|
drawScaleY( channelTop );
|
|
1794
1937
|
|
|
1795
1938
|
// set line width and dash for LEDs effect
|
|
1796
1939
|
if ( isLeds ) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1940
|
+
_ctx.setLineDash( [ ledHeight, ledSpaceV ] );
|
|
1941
|
+
_ctx.lineWidth = _bars[0].width;
|
|
1799
1942
|
}
|
|
1800
1943
|
else // for outline effect ensure linewidth is not greater than half the bar width
|
|
1801
|
-
|
|
1944
|
+
_ctx.lineWidth = isOutline ? Math.min( _lineWidth, _bars[0].width / 2 ) : _lineWidth;
|
|
1802
1945
|
|
|
1803
1946
|
// set clip region
|
|
1804
|
-
|
|
1805
|
-
if ( !
|
|
1947
|
+
_ctx.save();
|
|
1948
|
+
if ( ! _radial ) {
|
|
1806
1949
|
const channelRegion = new Path2D();
|
|
1807
1950
|
channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
|
|
1808
|
-
|
|
1951
|
+
_ctx.clip( channelRegion );
|
|
1809
1952
|
}
|
|
1810
1953
|
} // if ( useCanvas )
|
|
1811
1954
|
|
|
@@ -1814,11 +1957,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1814
1957
|
this._analyzer[ channel ].getFloatFrequencyData( fftData );
|
|
1815
1958
|
|
|
1816
1959
|
// apply weighting
|
|
1817
|
-
if (
|
|
1960
|
+
if ( _weightingFilter )
|
|
1818
1961
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1819
1962
|
|
|
1820
1963
|
// start drawing path (for mode 10)
|
|
1821
|
-
|
|
1964
|
+
_ctx.beginPath();
|
|
1822
1965
|
|
|
1823
1966
|
// store line graph points to create mirror effect in radial mode
|
|
1824
1967
|
let points = [];
|
|
@@ -1827,7 +1970,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1827
1970
|
|
|
1828
1971
|
for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
|
|
1829
1972
|
|
|
1830
|
-
const bar =
|
|
1973
|
+
const bar = _bars[ barIndex ],
|
|
1831
1974
|
{ posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1832
1975
|
|
|
1833
1976
|
let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
|
|
@@ -1849,13 +1992,13 @@ export default class AudioMotionAnalyzer {
|
|
|
1849
1992
|
bar.hold[ channel ]--;
|
|
1850
1993
|
// if hold is negative, it becomes the "acceleration" for peak drop
|
|
1851
1994
|
if ( bar.hold[ channel ] < 0 )
|
|
1852
|
-
bar.peak[ channel ] += bar.hold[ channel ] /
|
|
1995
|
+
bar.peak[ channel ] += bar.hold[ channel ] / ( holdFrames * holdFrames / 2 );
|
|
1853
1996
|
}
|
|
1854
1997
|
|
|
1855
1998
|
// check if it's a new peak for this bar
|
|
1856
1999
|
if ( barValue >= bar.peak[ channel ] ) {
|
|
1857
2000
|
bar.peak[ channel ] = barValue;
|
|
1858
|
-
bar.hold[ channel ] =
|
|
2001
|
+
bar.hold[ channel ] = holdFrames;
|
|
1859
2002
|
}
|
|
1860
2003
|
|
|
1861
2004
|
// if not using the canvas, move earlier to the next bar
|
|
@@ -1864,71 +2007,67 @@ export default class AudioMotionAnalyzer {
|
|
|
1864
2007
|
|
|
1865
2008
|
// set opacity for bar effects
|
|
1866
2009
|
if ( isLumi || isAlpha )
|
|
1867
|
-
|
|
2010
|
+
_ctx.globalAlpha = barValue;
|
|
1868
2011
|
else if ( isOutline )
|
|
1869
|
-
|
|
2012
|
+
_ctx.globalAlpha = fillAlpha;
|
|
1870
2013
|
|
|
1871
2014
|
// set fillStyle and strokeStyle for the current bar
|
|
1872
2015
|
setBarColor( barValue, barIndex );
|
|
1873
2016
|
|
|
1874
2017
|
// compute actual bar height on screen
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
// invert bar for radial channel 1
|
|
1878
|
-
if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
|
|
1879
|
-
barHeight *= -1;
|
|
2018
|
+
const barHeight = ( isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0 ) * direction;
|
|
1880
2019
|
|
|
1881
2020
|
// Draw current bar or line segment
|
|
1882
2021
|
|
|
1883
|
-
if (
|
|
2022
|
+
if ( _mode == 10 ) {
|
|
1884
2023
|
// compute the average between the initial bar (barIndex==0) and the next one
|
|
1885
2024
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
1886
|
-
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[
|
|
2025
|
+
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ _bars[1].binLo ] ) * maxBarHeight * direction + barHeight ) / 2;
|
|
1887
2026
|
|
|
1888
|
-
if (
|
|
2027
|
+
if ( _radial ) {
|
|
1889
2028
|
if ( barIndex == 0 )
|
|
1890
|
-
|
|
2029
|
+
_ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ) ) );
|
|
1891
2030
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
1892
2031
|
if ( posX >= 0 ) {
|
|
1893
2032
|
const point = [ posX, barHeight ];
|
|
1894
|
-
|
|
2033
|
+
_ctx.lineTo( ...radialXY( ...point ) );
|
|
1895
2034
|
points.push( point );
|
|
1896
2035
|
}
|
|
1897
2036
|
}
|
|
1898
2037
|
else { // Linear
|
|
1899
2038
|
if ( barIndex == 0 ) {
|
|
1900
2039
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
1901
|
-
if (
|
|
2040
|
+
if ( _mirror != -1 ) {
|
|
1902
2041
|
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
1903
|
-
|
|
2042
|
+
_ctx.moveTo( initialX - _lineWidth, analyzerBottom - prevFFTData );
|
|
1904
2043
|
}
|
|
1905
2044
|
else
|
|
1906
|
-
|
|
2045
|
+
_ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
|
|
1907
2046
|
}
|
|
1908
2047
|
// draw line to the current point
|
|
1909
2048
|
// avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
|
|
1910
|
-
if (
|
|
1911
|
-
|
|
2049
|
+
if ( _mirror != -1 || posX >= initialX )
|
|
2050
|
+
_ctx.lineTo( posX, analyzerBottom - barHeight );
|
|
1912
2051
|
}
|
|
1913
2052
|
}
|
|
1914
2053
|
else {
|
|
1915
2054
|
if ( isLeds ) {
|
|
1916
2055
|
// draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
|
|
1917
|
-
if ( showBgColor && !
|
|
1918
|
-
const alpha =
|
|
1919
|
-
|
|
1920
|
-
|
|
2056
|
+
if ( showBgColor && ! overlay && ( channel == 0 || ! isDualCombined ) ) {
|
|
2057
|
+
const alpha = _ctx.globalAlpha;
|
|
2058
|
+
_ctx.strokeStyle = LEDS_UNLIT_COLOR;
|
|
2059
|
+
_ctx.globalAlpha = 1;
|
|
1921
2060
|
strokeBar( barCenter, channelTop, analyzerBottom );
|
|
1922
2061
|
// restore properties
|
|
1923
|
-
|
|
1924
|
-
|
|
2062
|
+
_ctx.strokeStyle = _ctx.fillStyle;
|
|
2063
|
+
_ctx.globalAlpha = alpha;
|
|
1925
2064
|
}
|
|
1926
2065
|
if ( isTrueLeds ) {
|
|
1927
2066
|
// ledPosY() is used below to fit one entire led height into the selected range
|
|
1928
2067
|
const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
|
|
1929
2068
|
let last = analyzerBottom;
|
|
1930
2069
|
for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
|
|
1931
|
-
|
|
2070
|
+
_ctx.strokeStyle = colorStops[ i ].color;
|
|
1932
2071
|
let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
|
|
1933
2072
|
strokeBar( barCenter, last, y );
|
|
1934
2073
|
last = y - ledSpaceV;
|
|
@@ -1938,53 +2077,53 @@ export default class AudioMotionAnalyzer {
|
|
|
1938
2077
|
strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
|
|
1939
2078
|
}
|
|
1940
2079
|
else if ( posX >= initialX ) {
|
|
1941
|
-
if (
|
|
2080
|
+
if ( _radial )
|
|
1942
2081
|
radialPoly( posX, 0, width, barHeight, isOutline );
|
|
1943
2082
|
else if ( isRound ) {
|
|
1944
2083
|
const halfWidth = width / 2,
|
|
1945
2084
|
y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
|
|
1946
2085
|
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2086
|
+
_ctx.beginPath();
|
|
2087
|
+
_ctx.moveTo( posX, y );
|
|
2088
|
+
_ctx.lineTo( posX, y - barHeight );
|
|
2089
|
+
_ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
|
|
2090
|
+
_ctx.lineTo( posX + width, y );
|
|
1952
2091
|
strokeIf( isOutline );
|
|
1953
|
-
|
|
2092
|
+
_ctx.fill();
|
|
1954
2093
|
}
|
|
1955
2094
|
else {
|
|
1956
|
-
const offset = isOutline ?
|
|
1957
|
-
|
|
1958
|
-
|
|
2095
|
+
const offset = isOutline ? _ctx.lineWidth : 0;
|
|
2096
|
+
_ctx.beginPath();
|
|
2097
|
+
_ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
|
|
1959
2098
|
strokeIf( isOutline );
|
|
1960
|
-
|
|
2099
|
+
_ctx.fill();
|
|
1961
2100
|
}
|
|
1962
2101
|
}
|
|
1963
2102
|
}
|
|
1964
2103
|
|
|
1965
2104
|
// Draw peak
|
|
1966
2105
|
const peak = bar.peak[ channel ];
|
|
1967
|
-
if ( peak > 0 &&
|
|
2106
|
+
if ( peak > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
|
|
1968
2107
|
// set opacity
|
|
1969
|
-
if ( isOutline &&
|
|
1970
|
-
|
|
2108
|
+
if ( isOutline && _lineWidth > 0 )
|
|
2109
|
+
_ctx.globalAlpha = 1;
|
|
1971
2110
|
else if ( isAlpha )
|
|
1972
|
-
|
|
2111
|
+
_ctx.globalAlpha = peak;
|
|
1973
2112
|
|
|
1974
2113
|
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
1975
|
-
if (
|
|
2114
|
+
if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
|
|
1976
2115
|
setBarColor( peak );
|
|
1977
2116
|
|
|
1978
2117
|
// render peak according to current mode / effect
|
|
1979
2118
|
if ( isLeds ) {
|
|
1980
2119
|
const ledPeak = ledPosY( peak );
|
|
1981
2120
|
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
1982
|
-
|
|
2121
|
+
_ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
1983
2122
|
}
|
|
1984
|
-
else if ( !
|
|
1985
|
-
|
|
1986
|
-
else if (
|
|
1987
|
-
radialPoly( posX, peak * maxBarHeight *
|
|
2123
|
+
else if ( ! _radial )
|
|
2124
|
+
_ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
|
|
2125
|
+
else if ( _mode != 10 ) // radial - peaks for mode 10 are done by the peak line code
|
|
2126
|
+
radialPoly( posX, peak * maxBarHeight * direction, width, -2 );
|
|
1988
2127
|
}
|
|
1989
2128
|
|
|
1990
2129
|
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
@@ -1993,41 +2132,72 @@ export default class AudioMotionAnalyzer {
|
|
|
1993
2132
|
if ( ! useCanvas )
|
|
1994
2133
|
continue;
|
|
1995
2134
|
|
|
1996
|
-
|
|
2135
|
+
_ctx.restore(); // restore clip region
|
|
1997
2136
|
|
|
1998
2137
|
// restore global alpha
|
|
1999
|
-
|
|
2138
|
+
_ctx.globalAlpha = 1;
|
|
2000
2139
|
|
|
2001
2140
|
// Fill/stroke drawing path for mode 10
|
|
2002
|
-
if (
|
|
2141
|
+
if ( _mode == 10 ) {
|
|
2003
2142
|
setBarColor(); // select channel gradient
|
|
2004
2143
|
|
|
2005
|
-
if (
|
|
2006
|
-
if (
|
|
2144
|
+
if ( _radial ) {
|
|
2145
|
+
if ( _mirror ) {
|
|
2007
2146
|
let p;
|
|
2008
2147
|
while ( p = points.pop() )
|
|
2009
|
-
|
|
2148
|
+
_ctx.lineTo( ...radialXY( ...p, -1 ) );
|
|
2010
2149
|
}
|
|
2011
|
-
|
|
2150
|
+
_ctx.closePath();
|
|
2012
2151
|
}
|
|
2013
2152
|
|
|
2014
|
-
if (
|
|
2015
|
-
|
|
2153
|
+
if ( _lineWidth > 0 )
|
|
2154
|
+
_ctx.stroke();
|
|
2016
2155
|
|
|
2017
2156
|
if ( fillAlpha > 0 ) {
|
|
2018
|
-
if (
|
|
2157
|
+
if ( _radial ) {
|
|
2019
2158
|
// exclude the center circle from the fill area
|
|
2020
|
-
|
|
2021
|
-
|
|
2159
|
+
_ctx.moveTo( centerX + radius, centerY );
|
|
2160
|
+
_ctx.arc( centerX, centerY, radius, 0, TAU, true );
|
|
2022
2161
|
}
|
|
2023
2162
|
else { // close the fill area
|
|
2024
|
-
|
|
2025
|
-
|
|
2163
|
+
_ctx.lineTo( finalX, analyzerBottom );
|
|
2164
|
+
_ctx.lineTo( initialX, analyzerBottom );
|
|
2026
2165
|
}
|
|
2027
2166
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2167
|
+
_ctx.globalAlpha = fillAlpha;
|
|
2168
|
+
_ctx.fill();
|
|
2169
|
+
_ctx.globalAlpha = 1;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// draw peak line (and standard peaks on radial)
|
|
2173
|
+
if ( showPeakLine || ( _radial && showPeaks ) ) {
|
|
2174
|
+
points = []; // for mirror line on radial
|
|
2175
|
+
_ctx.beginPath();
|
|
2176
|
+
_bars.forEach( ( b, i ) => {
|
|
2177
|
+
let x = b.posX,
|
|
2178
|
+
h = b.peak[ channel ],
|
|
2179
|
+
m = i ? 'lineTo' : 'moveTo';
|
|
2180
|
+
if ( _radial && x < 0 ) {
|
|
2181
|
+
const nextBar = _bars[ i + 1 ];
|
|
2182
|
+
h = findY( x, h, nextBar.posX, nextBar.peak[ channel ], 0 );
|
|
2183
|
+
x = 0;
|
|
2184
|
+
}
|
|
2185
|
+
h *= maxBarHeight * direction;
|
|
2186
|
+
if ( showPeakLine ) {
|
|
2187
|
+
_ctx[ m ]( ...( _radial ? radialXY( x, h ) : [ x, analyzerBottom - h ] ) );
|
|
2188
|
+
if ( _radial && _mirror )
|
|
2189
|
+
points.push( [ x, h ] );
|
|
2190
|
+
}
|
|
2191
|
+
else if ( h )
|
|
2192
|
+
radialPoly( x, h, 1, -2 * direction ); // standard peaks (also does mirror)
|
|
2193
|
+
});
|
|
2194
|
+
if ( showPeakLine ) {
|
|
2195
|
+
let p;
|
|
2196
|
+
while ( p = points.pop() )
|
|
2197
|
+
_ctx.lineTo( ...radialXY( ...p, -1 ) ); // mirror line points
|
|
2198
|
+
_ctx.lineWidth = 1;
|
|
2199
|
+
_ctx.stroke(); // stroke peak line
|
|
2200
|
+
}
|
|
2031
2201
|
}
|
|
2032
2202
|
}
|
|
2033
2203
|
|
|
@@ -2040,14 +2210,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2040
2210
|
|
|
2041
2211
|
if ( useCanvas ) {
|
|
2042
2212
|
// Mirror effect
|
|
2043
|
-
if (
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2213
|
+
if ( _mirror && ! _radial ) {
|
|
2214
|
+
_ctx.setTransform( -1, 0, 0, 1, canvas.width - initialX, 0 );
|
|
2215
|
+
_ctx.drawImage( canvas, initialX, 0, centerX, canvas.height, 0, 0, centerX, canvas.height );
|
|
2216
|
+
_ctx.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
2047
2217
|
}
|
|
2048
2218
|
|
|
2049
2219
|
// restore solid lines
|
|
2050
|
-
|
|
2220
|
+
_ctx.setLineDash([]);
|
|
2051
2221
|
|
|
2052
2222
|
// draw frequency scale (X-axis)
|
|
2053
2223
|
drawScaleX();
|
|
@@ -2058,14 +2228,11 @@ export default class AudioMotionAnalyzer {
|
|
|
2058
2228
|
|
|
2059
2229
|
// call callback function, if defined
|
|
2060
2230
|
if ( this.onCanvasDraw ) {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
this.onCanvasDraw( this, { timestamp,
|
|
2064
|
-
|
|
2231
|
+
_ctx.save();
|
|
2232
|
+
_ctx.fillStyle = _ctx.strokeStyle = _canvasGradients[0];
|
|
2233
|
+
this.onCanvasDraw( this, { timestamp, _canvasGradients } );
|
|
2234
|
+
_ctx.restore();
|
|
2065
2235
|
}
|
|
2066
|
-
|
|
2067
|
-
// schedule next canvas update
|
|
2068
|
-
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
2069
2236
|
}
|
|
2070
2237
|
|
|
2071
2238
|
/**
|
|
@@ -2098,24 +2265,19 @@ export default class AudioMotionAnalyzer {
|
|
|
2098
2265
|
* Generate currently selected gradient
|
|
2099
2266
|
*/
|
|
2100
2267
|
_makeGrad() {
|
|
2101
|
-
|
|
2102
2268
|
if ( ! this._ready )
|
|
2103
2269
|
return;
|
|
2104
2270
|
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
channelLayout = this._chLayout,
|
|
2271
|
+
const { canvas, _ctx, _radial, _reflexRatio } = this,
|
|
2272
|
+
{ analyzerWidth, initialX, radius } = this._aux,
|
|
2108
2273
|
{ isLumi } = this._flg,
|
|
2109
|
-
|
|
2110
|
-
|
|
2274
|
+
isDualVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
2275
|
+
analyzerRatio = 1 - _reflexRatio,
|
|
2276
|
+
centerX = canvas.width >> 1,
|
|
2277
|
+
centerY = canvas.height >> 1,
|
|
2278
|
+
maxRadius = Math.min( centerX, centerY ),
|
|
2279
|
+
gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - _reflexRatio * ( ! isDualVertical ) ) | 0;
|
|
2111
2280
|
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
2112
|
-
analyzerRatio = 1 - this._reflexRatio,
|
|
2113
|
-
{ analyzerWidth, initialX, radius } = this._aux;
|
|
2114
|
-
|
|
2115
|
-
// for radial mode
|
|
2116
|
-
const centerX = canvas.width >> 1,
|
|
2117
|
-
centerY = canvas.height >> 1,
|
|
2118
|
-
maxRadius = Math.min( centerX, centerY );
|
|
2119
2281
|
|
|
2120
2282
|
for ( const channel of [0,1] ) {
|
|
2121
2283
|
const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
@@ -2124,13 +2286,13 @@ export default class AudioMotionAnalyzer {
|
|
|
2124
2286
|
|
|
2125
2287
|
let grad;
|
|
2126
2288
|
|
|
2127
|
-
if (
|
|
2128
|
-
grad =
|
|
2289
|
+
if ( _radial )
|
|
2290
|
+
grad = _ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * isDualVertical );
|
|
2129
2291
|
else
|
|
2130
|
-
grad =
|
|
2292
|
+
grad = _ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
|
|
2131
2293
|
|
|
2132
2294
|
if ( colorStops ) {
|
|
2133
|
-
const dual =
|
|
2295
|
+
const dual = isDualVertical && ! this._splitGradient && ( ! isHorizontal || _radial );
|
|
2134
2296
|
|
|
2135
2297
|
for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
|
|
2136
2298
|
const maxIndex = colorStops.length - 1;
|
|
@@ -2143,17 +2305,17 @@ export default class AudioMotionAnalyzer {
|
|
|
2143
2305
|
offset /= 2;
|
|
2144
2306
|
|
|
2145
2307
|
// constrain the offset within the useful analyzer areas (avoid reflex areas)
|
|
2146
|
-
if (
|
|
2308
|
+
if ( isDualVertical && ! isLumi && ! _radial && ! isHorizontal ) {
|
|
2147
2309
|
offset *= analyzerRatio;
|
|
2148
2310
|
// skip the first reflex area in split mode
|
|
2149
2311
|
if ( ! dual && offset > .5 * analyzerRatio )
|
|
2150
|
-
offset += .5 *
|
|
2312
|
+
offset += .5 * _reflexRatio;
|
|
2151
2313
|
}
|
|
2152
2314
|
|
|
2153
2315
|
// only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
|
|
2154
2316
|
if ( channelArea == 1 ) {
|
|
2155
2317
|
// add colors in reverse order if radial or lumi are active
|
|
2156
|
-
if (
|
|
2318
|
+
if ( _radial || isLumi ) {
|
|
2157
2319
|
const revIndex = maxIndex - index;
|
|
2158
2320
|
colorStop = colorStops[ revIndex ];
|
|
2159
2321
|
offset = 1 - colorStop.pos / 2;
|
|
@@ -2171,7 +2333,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2171
2333
|
grad.addColorStop( offset, colorStop.color );
|
|
2172
2334
|
|
|
2173
2335
|
// create additional color stop at the end of first channel to prevent bleeding
|
|
2174
|
-
if (
|
|
2336
|
+
if ( isDualVertical && index == maxIndex && offset < .5 )
|
|
2175
2337
|
grad.addColorStop( .5, colorStop.color );
|
|
2176
2338
|
});
|
|
2177
2339
|
} // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
|
|
@@ -2206,12 +2368,10 @@ export default class AudioMotionAnalyzer {
|
|
|
2206
2368
|
* Internal function to change canvas dimensions on demand
|
|
2207
2369
|
*/
|
|
2208
2370
|
_setCanvas( reason ) {
|
|
2209
|
-
// if initialization is not finished, quit
|
|
2210
2371
|
if ( ! this._ready )
|
|
2211
2372
|
return;
|
|
2212
2373
|
|
|
2213
|
-
const
|
|
2214
|
-
canvas = ctx.canvas,
|
|
2374
|
+
const { canvas, _ctx } = this,
|
|
2215
2375
|
canvasX = this._scaleX.canvas,
|
|
2216
2376
|
pixelRatio = window.devicePixelRatio / ( this._loRes + 1 );
|
|
2217
2377
|
|
|
@@ -2242,12 +2402,12 @@ export default class AudioMotionAnalyzer {
|
|
|
2242
2402
|
|
|
2243
2403
|
// if not in overlay mode, paint the canvas black
|
|
2244
2404
|
if ( ! this.overlay ) {
|
|
2245
|
-
|
|
2246
|
-
|
|
2405
|
+
_ctx.fillStyle = '#000';
|
|
2406
|
+
_ctx.fillRect( 0, 0, newWidth, newHeight );
|
|
2247
2407
|
}
|
|
2248
2408
|
|
|
2249
2409
|
// set lineJoin property for area fill mode (this is reset whenever the canvas size changes)
|
|
2250
|
-
|
|
2410
|
+
_ctx.lineJoin = 'bevel';
|
|
2251
2411
|
|
|
2252
2412
|
// update dimensions of the scale canvas
|
|
2253
2413
|
canvasX.width = newWidth;
|
|
@@ -2312,6 +2472,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2312
2472
|
loRes : false,
|
|
2313
2473
|
lumiBars : false,
|
|
2314
2474
|
maxDecibels : -25,
|
|
2475
|
+
maxFPS : 0,
|
|
2315
2476
|
maxFreq : 22000,
|
|
2316
2477
|
minDecibels : -85,
|
|
2317
2478
|
minFreq : 20,
|
|
@@ -2320,6 +2481,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2320
2481
|
noteLabels : false,
|
|
2321
2482
|
outlineBars : false,
|
|
2322
2483
|
overlay : false,
|
|
2484
|
+
peakLine : false,
|
|
2323
2485
|
radial : false,
|
|
2324
2486
|
reflexAlpha : 0.15,
|
|
2325
2487
|
reflexBright : 1,
|