audiomotion-analyzer 4.1.1 → 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 +105 -47
- package/package.json +6 -2
- package/src/audioMotion-analyzer.js +469 -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,9 @@ 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
|
+
|
|
105
111
|
// Polyfill for Array.findLastIndex()
|
|
106
112
|
if ( ! Array.prototype.findLastIndex ) {
|
|
107
113
|
Array.prototype.findLastIndex = function( callback ) {
|
|
@@ -130,11 +136,18 @@ export default class AudioMotionAnalyzer {
|
|
|
130
136
|
this._ready = false;
|
|
131
137
|
|
|
132
138
|
// Initialize internal objects
|
|
133
|
-
this._aux = {};
|
|
134
|
-
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;
|
|
135
145
|
this._gradients = {}; // registered gradients
|
|
146
|
+
this._last = 0; // timestamp of last rendered frame
|
|
147
|
+
this._outNodes = []; // output nodes
|
|
148
|
+
this._ownContext = false;
|
|
136
149
|
this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
|
|
137
|
-
this.
|
|
150
|
+
this._sources = []; // input nodes
|
|
138
151
|
|
|
139
152
|
// Register built-in gradients
|
|
140
153
|
for ( const [ name, options ] of GRADIENTS )
|
|
@@ -160,6 +173,7 @@ export default class AudioMotionAnalyzer {
|
|
|
160
173
|
else {
|
|
161
174
|
try {
|
|
162
175
|
audioCtx = new ( window.AudioContext || window.webkitAudioContext )();
|
|
176
|
+
this._ownContext = true;
|
|
163
177
|
}
|
|
164
178
|
catch( err ) {
|
|
165
179
|
throw new AudioMotionError( ERR_AUDIO_CONTEXT_FAIL );
|
|
@@ -174,13 +188,13 @@ export default class AudioMotionAnalyzer {
|
|
|
174
188
|
Connection routing:
|
|
175
189
|
===================
|
|
176
190
|
|
|
177
|
-
for dual channel
|
|
191
|
+
for dual channel layouts: +---> analyzer[0] ---+
|
|
178
192
|
| |
|
|
179
193
|
(source) ---> input ---> splitter ---+ +---> merger ---> output ---> (destination)
|
|
180
194
|
| |
|
|
181
195
|
+---> analyzer[1] ---+
|
|
182
196
|
|
|
183
|
-
for single channel
|
|
197
|
+
for single channel layout:
|
|
184
198
|
|
|
185
199
|
(source) ---> input -----------------------> analyzer[0] ---------------------> output ---> (destination)
|
|
186
200
|
|
|
@@ -193,8 +207,7 @@ export default class AudioMotionAnalyzer {
|
|
|
193
207
|
this._input = audioCtx.createGain();
|
|
194
208
|
this._output = audioCtx.createGain();
|
|
195
209
|
|
|
196
|
-
//
|
|
197
|
-
this._sources = [];
|
|
210
|
+
// connect audio source if provided in the options
|
|
198
211
|
if ( options.source )
|
|
199
212
|
this.connectInput( options.source );
|
|
200
213
|
|
|
@@ -206,17 +219,13 @@ export default class AudioMotionAnalyzer {
|
|
|
206
219
|
merger.connect( this._output );
|
|
207
220
|
|
|
208
221
|
// connect output -> destination (speakers)
|
|
209
|
-
this._outNodes = [];
|
|
210
222
|
if ( options.connectSpeakers !== false )
|
|
211
223
|
this.connectOutput();
|
|
212
224
|
|
|
213
|
-
// initialize object to save energy
|
|
214
|
-
this._energy = { val: 0, peak: 0, hold: 0 };
|
|
215
|
-
|
|
216
225
|
// create analyzer canvas
|
|
217
226
|
const canvas = document.createElement('canvas');
|
|
218
227
|
canvas.style = 'max-width: 100%;';
|
|
219
|
-
this.
|
|
228
|
+
this._ctx = canvas.getContext('2d');
|
|
220
229
|
|
|
221
230
|
// create auxiliary canvases for the X-axis and radial scale labels
|
|
222
231
|
for ( const ctx of [ '_scaleX', '_scaleR' ] )
|
|
@@ -243,21 +252,25 @@ export default class AudioMotionAnalyzer {
|
|
|
243
252
|
this._setCanvas( REASON_RESIZE );
|
|
244
253
|
this._fsTimeout = 0;
|
|
245
254
|
}
|
|
246
|
-
},
|
|
255
|
+
}, DEBOUNCE_TIMEOUT );
|
|
247
256
|
}
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
// if browser supports ResizeObserver, listen for resize on the container
|
|
251
260
|
if ( window.ResizeObserver ) {
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
this._observer = new ResizeObserver( onResize );
|
|
262
|
+
this._observer.observe( this._container );
|
|
254
263
|
}
|
|
255
264
|
|
|
265
|
+
// create an AbortController to remove event listeners on destroy()
|
|
266
|
+
this._controller = new AbortController();
|
|
267
|
+
const signal = this._controller.signal;
|
|
268
|
+
|
|
256
269
|
// listen for resize events on the window - required for fullscreen on iPadOS
|
|
257
|
-
window.addEventListener(
|
|
270
|
+
window.addEventListener( EVENT_RESIZE, onResize, { signal } );
|
|
258
271
|
|
|
259
272
|
// listen for fullscreenchange events on the canvas - not available on Safari
|
|
260
|
-
canvas.addEventListener(
|
|
273
|
+
canvas.addEventListener( EVENT_FULLSCREENCHANGE, () => {
|
|
261
274
|
// set flag to indicate a fullscreen change in progress
|
|
262
275
|
this._fsChanging = true;
|
|
263
276
|
|
|
@@ -272,16 +285,24 @@ export default class AudioMotionAnalyzer {
|
|
|
272
285
|
this._fsTimeout = window.setTimeout( () => {
|
|
273
286
|
this._fsChanging = false;
|
|
274
287
|
this._fsTimeout = 0;
|
|
275
|
-
},
|
|
276
|
-
});
|
|
288
|
+
}, DEBOUNCE_TIMEOUT );
|
|
289
|
+
}, { signal } );
|
|
277
290
|
|
|
278
291
|
// Resume audio context if in suspended state (browsers' autoplay policy)
|
|
279
292
|
const unlockContext = () => {
|
|
280
293
|
if ( audioCtx.state == 'suspended' )
|
|
281
294
|
audioCtx.resume();
|
|
282
|
-
window.removeEventListener(
|
|
295
|
+
window.removeEventListener( EVENT_CLICK, unlockContext );
|
|
283
296
|
}
|
|
284
|
-
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 } );
|
|
285
306
|
|
|
286
307
|
// Set configuration options and use defaults for any missing properties
|
|
287
308
|
this._setProps( options, true );
|
|
@@ -453,6 +474,13 @@ export default class AudioMotionAnalyzer {
|
|
|
453
474
|
this._analyzer[ i ].maxDecibels = value;
|
|
454
475
|
}
|
|
455
476
|
|
|
477
|
+
get maxFPS() {
|
|
478
|
+
return this._maxFPS;
|
|
479
|
+
}
|
|
480
|
+
set maxFPS( value ) {
|
|
481
|
+
this._maxFPS = value < 0 ? 0 : +value || 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
456
484
|
get maxFreq() {
|
|
457
485
|
return this._maxFreq;
|
|
458
486
|
}
|
|
@@ -524,6 +552,13 @@ export default class AudioMotionAnalyzer {
|
|
|
524
552
|
this._calcBars();
|
|
525
553
|
}
|
|
526
554
|
|
|
555
|
+
get peakLine() {
|
|
556
|
+
return this._peakLine;
|
|
557
|
+
}
|
|
558
|
+
set peakLine( value ) {
|
|
559
|
+
this._peakLine = !! value;
|
|
560
|
+
}
|
|
561
|
+
|
|
527
562
|
get radial() {
|
|
528
563
|
return this._radial;
|
|
529
564
|
}
|
|
@@ -625,10 +660,10 @@ export default class AudioMotionAnalyzer {
|
|
|
625
660
|
return this._input.context;
|
|
626
661
|
}
|
|
627
662
|
get canvas() {
|
|
628
|
-
return this.
|
|
663
|
+
return this._ctx.canvas;
|
|
629
664
|
}
|
|
630
665
|
get canvasCtx() {
|
|
631
|
-
return this.
|
|
666
|
+
return this._ctx;
|
|
632
667
|
}
|
|
633
668
|
get connectedSources() {
|
|
634
669
|
return this._sources;
|
|
@@ -651,8 +686,11 @@ export default class AudioMotionAnalyzer {
|
|
|
651
686
|
get isBandsMode() {
|
|
652
687
|
return this._flg.isBands;
|
|
653
688
|
}
|
|
689
|
+
get isDestroyed() {
|
|
690
|
+
return this._destroyed;
|
|
691
|
+
}
|
|
654
692
|
get isFullscreen() {
|
|
655
|
-
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
693
|
+
return this._fsEl && ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
656
694
|
}
|
|
657
695
|
get isLedBars() {
|
|
658
696
|
return this._flg.isLeds;
|
|
@@ -664,7 +702,7 @@ export default class AudioMotionAnalyzer {
|
|
|
664
702
|
return this._flg.isOctaves;
|
|
665
703
|
}
|
|
666
704
|
get isOn() {
|
|
667
|
-
return this._runId
|
|
705
|
+
return !! this._runId;
|
|
668
706
|
}
|
|
669
707
|
get isOutlineBars() {
|
|
670
708
|
return this._flg.isOutline;
|
|
@@ -729,12 +767,54 @@ export default class AudioMotionAnalyzer {
|
|
|
729
767
|
}
|
|
730
768
|
}
|
|
731
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
|
+
|
|
732
811
|
/**
|
|
733
812
|
* Disconnects audio sources from the analyzer
|
|
734
813
|
*
|
|
735
|
-
* @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)
|
|
736
816
|
*/
|
|
737
|
-
disconnectInput( sources ) {
|
|
817
|
+
disconnectInput( sources, stopTracks ) {
|
|
738
818
|
if ( ! sources )
|
|
739
819
|
sources = Array.from( this._sources );
|
|
740
820
|
else if ( ! Array.isArray( sources ) )
|
|
@@ -742,6 +822,11 @@ export default class AudioMotionAnalyzer {
|
|
|
742
822
|
|
|
743
823
|
for ( const node of sources ) {
|
|
744
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
|
+
}
|
|
745
830
|
if ( idx >= 0 ) {
|
|
746
831
|
node.disconnect( this._input );
|
|
747
832
|
this._sources.splice( idx, 1 );
|
|
@@ -939,26 +1024,42 @@ export default class AudioMotionAnalyzer {
|
|
|
939
1024
|
}
|
|
940
1025
|
}
|
|
941
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
|
+
|
|
942
1041
|
/**
|
|
943
1042
|
* Start / stop canvas animation
|
|
944
1043
|
*
|
|
945
|
-
* @param {boolean} [
|
|
946
|
-
* @returns {boolean} resulting
|
|
1044
|
+
* @param {boolean} [force] if undefined, inverts the current state
|
|
1045
|
+
* @returns {boolean} resulting state after the change
|
|
947
1046
|
*/
|
|
948
|
-
toggleAnalyzer(
|
|
949
|
-
const
|
|
1047
|
+
toggleAnalyzer( force ) {
|
|
1048
|
+
const hasStarted = this.isOn;
|
|
950
1049
|
|
|
951
|
-
if (
|
|
952
|
-
|
|
1050
|
+
if ( force === undefined )
|
|
1051
|
+
force = ! hasStarted;
|
|
953
1052
|
|
|
954
|
-
if
|
|
1053
|
+
// Stop the analyzer if it was already running and must be disabled
|
|
1054
|
+
if ( hasStarted && ! force ) {
|
|
955
1055
|
cancelAnimationFrame( this._runId );
|
|
956
|
-
this._runId =
|
|
1056
|
+
this._runId = 0;
|
|
957
1057
|
}
|
|
958
|
-
|
|
959
|
-
|
|
1058
|
+
// Start the analyzer if it was stopped and must be enabled
|
|
1059
|
+
else if ( ! hasStarted && force && ! this._destroyed ) {
|
|
1060
|
+
this._frames = 0;
|
|
960
1061
|
this._time = performance.now();
|
|
961
|
-
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1062
|
+
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) ); // arrow function preserves the scope of *this*
|
|
962
1063
|
}
|
|
963
1064
|
|
|
964
1065
|
return this.isOn;
|
|
@@ -976,6 +1077,8 @@ export default class AudioMotionAnalyzer {
|
|
|
976
1077
|
}
|
|
977
1078
|
else {
|
|
978
1079
|
const fsEl = this._fsEl;
|
|
1080
|
+
if ( ! fsEl )
|
|
1081
|
+
return;
|
|
979
1082
|
if ( fsEl.requestFullscreen )
|
|
980
1083
|
fsEl.requestFullscreen();
|
|
981
1084
|
else if ( fsEl.webkitRequestFullscreen )
|
|
@@ -1004,8 +1107,10 @@ export default class AudioMotionAnalyzer {
|
|
|
1004
1107
|
_calcBars() {
|
|
1005
1108
|
const bars = this._bars = []; // initialize object property
|
|
1006
1109
|
|
|
1007
|
-
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 };
|
|
1008
1112
|
return;
|
|
1113
|
+
}
|
|
1009
1114
|
|
|
1010
1115
|
const barSpace = this._barSpace,
|
|
1011
1116
|
canvas = this.canvas,
|
|
@@ -1369,28 +1474,22 @@ export default class AudioMotionAnalyzer {
|
|
|
1369
1474
|
return;
|
|
1370
1475
|
|
|
1371
1476
|
const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
|
|
1372
|
-
canvas
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
canvasX = scaleX.canvas,
|
|
1376
|
-
canvasR = scaleR.canvas,
|
|
1477
|
+
{ canvas, _frequencyScale, _mirror, _noteLabels, _radial, _scaleX, _scaleR } = this,
|
|
1478
|
+
canvasX = _scaleX.canvas,
|
|
1479
|
+
canvasR = _scaleR.canvas,
|
|
1377
1480
|
freqLabels = [],
|
|
1378
|
-
frequencyScale= this._frequencyScale,
|
|
1379
|
-
isNoteLabels = this._noteLabels,
|
|
1380
|
-
isRadial = this._radial,
|
|
1381
1481
|
isVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
1382
|
-
mirror = this._mirror,
|
|
1383
1482
|
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1384
1483
|
scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
|
|
1385
1484
|
fontSizeX = canvasX.height >> 1,
|
|
1386
1485
|
fontSizeR = scaleHeight >> 1,
|
|
1387
|
-
labelWidthX = fontSizeX * (
|
|
1388
|
-
labelWidthR = fontSizeR * (
|
|
1486
|
+
labelWidthX = fontSizeX * ( _noteLabels ? .7 : 1.5 ),
|
|
1487
|
+
labelWidthR = fontSizeR * ( _noteLabels ? 1 : 2 ),
|
|
1389
1488
|
root12 = 2 ** ( 1 / 12 );
|
|
1390
1489
|
|
|
1391
|
-
if ( !
|
|
1490
|
+
if ( ! _noteLabels && ( this._ansiBands || _frequencyScale != SCALE_LOG ) ) {
|
|
1392
1491
|
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
1393
|
-
if (
|
|
1492
|
+
if ( _frequencyScale == SCALE_LINEAR )
|
|
1394
1493
|
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
|
|
1395
1494
|
else
|
|
1396
1495
|
freqLabels.push( 8e3, 16e3 );
|
|
@@ -1402,8 +1501,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1402
1501
|
if ( freq >= this._minFreq && freq <= this._maxFreq ) {
|
|
1403
1502
|
const pitch = scale[ note ],
|
|
1404
1503
|
isC = pitch == 'C';
|
|
1405
|
-
if ( ( pitch &&
|
|
1406
|
-
freqLabels.push(
|
|
1504
|
+
if ( ( pitch && _noteLabels && ! _mirror ) || isC )
|
|
1505
|
+
freqLabels.push( _noteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
|
|
1407
1506
|
}
|
|
1408
1507
|
freq *= root12;
|
|
1409
1508
|
}
|
|
@@ -1423,27 +1522,27 @@ export default class AudioMotionAnalyzer {
|
|
|
1423
1522
|
posX = radialY * Math.cos( adjAng ),
|
|
1424
1523
|
posY = radialY * Math.sin( adjAng );
|
|
1425
1524
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1525
|
+
_scaleR.save();
|
|
1526
|
+
_scaleR.translate( centerR + posX, centerR + posY );
|
|
1527
|
+
_scaleR.rotate( angle );
|
|
1528
|
+
_scaleR.fillText( label, 0, 0 );
|
|
1529
|
+
_scaleR.restore();
|
|
1431
1530
|
}
|
|
1432
1531
|
|
|
1433
1532
|
// clear scale canvas
|
|
1434
1533
|
canvasX.width |= 0;
|
|
1435
1534
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1535
|
+
_scaleX.fillStyle = _scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
|
|
1536
|
+
_scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1438
1537
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1538
|
+
_scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
|
|
1539
|
+
_scaleR.lineWidth = scaleHeight;
|
|
1540
|
+
_scaleR.stroke();
|
|
1442
1541
|
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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';
|
|
1447
1546
|
|
|
1448
1547
|
let prevX = -labelWidthX / 4,
|
|
1449
1548
|
prevR = -labelWidthR;
|
|
@@ -1453,22 +1552,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1453
1552
|
x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
|
|
1454
1553
|
y = canvasX.height * .75,
|
|
1455
1554
|
isC = label[0] == 'C',
|
|
1456
|
-
maxW = fontSizeX * (
|
|
1555
|
+
maxW = fontSizeX * ( _noteLabels && ! _mirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1457
1556
|
|
|
1458
1557
|
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1459
|
-
|
|
1558
|
+
_scaleX.fillStyle = _scaleR.fillStyle = isC && ! _mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1460
1559
|
|
|
1461
1560
|
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1462
|
-
if (
|
|
1561
|
+
if ( _noteLabels ) {
|
|
1562
|
+
const isLog = _frequencyScale == SCALE_LOG,
|
|
1563
|
+
isLinear = _frequencyScale == SCALE_LINEAR;
|
|
1564
|
+
|
|
1463
1565
|
let allowedLabels = ['C'];
|
|
1464
|
-
|
|
1465
|
-
|
|
1566
|
+
|
|
1567
|
+
if ( isLog || freq > 2e3 || ( ! isLinear && freq > 250 ) ||
|
|
1568
|
+
( ( ! _radial || isVertical ) && ( ! isLinear && freq > 125 || freq > 1e3 ) ) )
|
|
1466
1569
|
allowedLabels.push('G');
|
|
1467
|
-
if (
|
|
1468
|
-
( ( !
|
|
1570
|
+
if ( isLog || freq > 4e3 || ( ! isLinear && freq > 500 ) ||
|
|
1571
|
+
( ( ! _radial || isVertical ) && ( ! isLinear && freq > 250 || freq > 2e3 ) ) )
|
|
1469
1572
|
allowedLabels.push('E');
|
|
1470
|
-
if (
|
|
1471
|
-
( ( !
|
|
1573
|
+
if ( isLinear && freq > 4e3 ||
|
|
1574
|
+
( ( ! _radial || isVertical ) && ( isLog || freq > 2e3 || ( ! isLinear && freq > 500 ) ) ) )
|
|
1472
1575
|
allowedLabels.push('D','F','A','B');
|
|
1473
1576
|
if ( ! allowedLabels.includes( label[0] ) )
|
|
1474
1577
|
continue; // skip this label
|
|
@@ -1476,16 +1579,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1476
1579
|
|
|
1477
1580
|
// linear scale
|
|
1478
1581
|
if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
|
|
1479
|
-
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1482
|
-
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;
|
|
1483
1586
|
}
|
|
1484
1587
|
|
|
1485
1588
|
// radial scale
|
|
1486
1589
|
if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
|
|
1487
1590
|
radialLabel( x, label );
|
|
1488
|
-
if (
|
|
1591
|
+
if ( _mirror && ( x > labelWidthR || _mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
|
|
1489
1592
|
radialLabel( -x, label );
|
|
1490
1593
|
prevR = x;
|
|
1491
1594
|
}
|
|
@@ -1497,41 +1600,68 @@ export default class AudioMotionAnalyzer {
|
|
|
1497
1600
|
* this is called 60 times per second by requestAnimationFrame()
|
|
1498
1601
|
*/
|
|
1499
1602
|
_draw( timestamp ) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
+
|
|
1504
1647
|
canvasX = this._scaleX.canvas,
|
|
1505
1648
|
canvasR = this._scaleR.canvas,
|
|
1506
|
-
canvasGradients= this._canvasGradients,
|
|
1507
1649
|
centerX = canvas.width >> 1,
|
|
1508
1650
|
centerY = canvas.height >> 1,
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
isRadial = this._radial,
|
|
1516
|
-
isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
|
|
1517
|
-
channelLayout = this._chLayout,
|
|
1518
|
-
lineWidth = this._lineWidth,
|
|
1519
|
-
mirrorMode = this._mirror,
|
|
1520
|
-
{ analyzerHeight, channelCoords,
|
|
1521
|
-
channelHeight, channelGap, initialX, radius } = this._aux,
|
|
1522
|
-
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,
|
|
1523
1657
|
finalX = initialX + analyzerWidth,
|
|
1524
|
-
|
|
1525
|
-
maxBarHeight =
|
|
1526
|
-
|
|
1527
|
-
mindB = this.minDecibels,
|
|
1528
|
-
dbRange = maxdB - mindB,
|
|
1529
|
-
useCanvas = this.useCanvas,
|
|
1530
|
-
weightingFilter= this._weightingFilter,
|
|
1658
|
+
showPeakLine = showPeaks && this._peakLine && _mode == 10,
|
|
1659
|
+
maxBarHeight = _radial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
|
|
1660
|
+
dbRange = maxDecibels - minDecibels,
|
|
1531
1661
|
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1532
1662
|
|
|
1533
|
-
if (
|
|
1534
|
-
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
|
|
1535
1665
|
|
|
1536
1666
|
/* HELPER FUNCTIONS */
|
|
1537
1667
|
|
|
@@ -1539,8 +1669,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1539
1669
|
const doReflex = channel => {
|
|
1540
1670
|
if ( this._reflexRatio > 0 && ! isLumi ) {
|
|
1541
1671
|
let posY, height;
|
|
1542
|
-
if ( this.reflexFit ||
|
|
1543
|
-
posY =
|
|
1672
|
+
if ( this.reflexFit || isDualVertical ) { // always fit reflex in vertical stereo mode
|
|
1673
|
+
posY = isDualVertical && channel == 0 ? channelHeight + channelGap : 0;
|
|
1544
1674
|
height = channelHeight - analyzerHeight;
|
|
1545
1675
|
}
|
|
1546
1676
|
else {
|
|
@@ -1548,34 +1678,34 @@ export default class AudioMotionAnalyzer {
|
|
|
1548
1678
|
height = analyzerHeight;
|
|
1549
1679
|
}
|
|
1550
1680
|
|
|
1551
|
-
|
|
1681
|
+
_ctx.save();
|
|
1552
1682
|
|
|
1553
1683
|
// set alpha and brightness for the reflection
|
|
1554
|
-
|
|
1684
|
+
_ctx.globalAlpha = this.reflexAlpha;
|
|
1555
1685
|
if ( this.reflexBright != 1 )
|
|
1556
|
-
|
|
1686
|
+
_ctx.filter = `brightness(${this.reflexBright})`;
|
|
1557
1687
|
|
|
1558
1688
|
// create the reflection
|
|
1559
|
-
|
|
1560
|
-
|
|
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 );
|
|
1561
1691
|
|
|
1562
|
-
|
|
1692
|
+
_ctx.restore();
|
|
1563
1693
|
}
|
|
1564
1694
|
}
|
|
1565
1695
|
|
|
1566
1696
|
// draw scale on X-axis
|
|
1567
1697
|
const drawScaleX = () => {
|
|
1568
1698
|
if ( this.showScaleX ) {
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1699
|
+
if ( _radial ) {
|
|
1700
|
+
_ctx.save();
|
|
1701
|
+
_ctx.translate( centerX, centerY );
|
|
1572
1702
|
if ( this._spinSpeed )
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1703
|
+
_ctx.rotate( this._spinAngle + HALF_PI );
|
|
1704
|
+
_ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
|
|
1705
|
+
_ctx.restore();
|
|
1576
1706
|
}
|
|
1577
1707
|
else
|
|
1578
|
-
|
|
1708
|
+
_ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
|
|
1579
1709
|
}
|
|
1580
1710
|
}
|
|
1581
1711
|
|
|
@@ -1583,16 +1713,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1583
1713
|
const drawScaleY = channelTop => {
|
|
1584
1714
|
const scaleWidth = canvasX.height,
|
|
1585
1715
|
fontSize = scaleWidth >> 1,
|
|
1586
|
-
max =
|
|
1587
|
-
min =
|
|
1588
|
-
incr =
|
|
1716
|
+
max = _linearAmplitude ? 100 : maxDecibels,
|
|
1717
|
+
min = _linearAmplitude ? 0 : minDecibels,
|
|
1718
|
+
incr = _linearAmplitude ? 20 : 5,
|
|
1589
1719
|
interval = analyzerHeight / ( max - min );
|
|
1590
1720
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1721
|
+
_ctx.save();
|
|
1722
|
+
_ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1723
|
+
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1724
|
+
_ctx.textAlign = 'right';
|
|
1725
|
+
_ctx.lineWidth = 1;
|
|
1596
1726
|
|
|
1597
1727
|
for ( let val = max; val > min; val -= incr ) {
|
|
1598
1728
|
const posY = channelTop + ( max - val ) * interval,
|
|
@@ -1600,26 +1730,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1600
1730
|
|
|
1601
1731
|
if ( even ) {
|
|
1602
1732
|
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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;
|
|
1610
1740
|
}
|
|
1611
1741
|
else {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1742
|
+
_ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1743
|
+
_ctx.setLineDash([2,8]);
|
|
1744
|
+
_ctx.lineDashOffset = 1;
|
|
1615
1745
|
}
|
|
1616
1746
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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();
|
|
1621
1751
|
}
|
|
1622
|
-
|
|
1752
|
+
_ctx.restore();
|
|
1623
1753
|
}
|
|
1624
1754
|
|
|
1625
1755
|
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
@@ -1632,7 +1762,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1632
1762
|
SQ12194 = 148693636,
|
|
1633
1763
|
linearTodB = value => 20 * Math.log10( value );
|
|
1634
1764
|
|
|
1635
|
-
switch (
|
|
1765
|
+
switch ( _weightingFilter ) {
|
|
1636
1766
|
case FILTER_A : // A-weighting https://en.wikipedia.org/wiki/A-weighting
|
|
1637
1767
|
const rA = ( SQ12194 * f2 ** 2 ) / ( ( f2 + SQ20_6 ) * Math.sqrt( ( f2 + SQ107_7 ) * ( f2 + SQ737_9 ) ) * ( f2 + SQ12194 ) );
|
|
1638
1768
|
return 2 + linearTodB( rA );
|
|
@@ -1662,19 +1792,19 @@ export default class AudioMotionAnalyzer {
|
|
|
1662
1792
|
|
|
1663
1793
|
// draws (stroke) a bar from x,y1 to x,y2
|
|
1664
1794
|
const strokeBar = ( x, y1, y2 ) => {
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1795
|
+
_ctx.beginPath();
|
|
1796
|
+
_ctx.moveTo( x, y1 );
|
|
1797
|
+
_ctx.lineTo( x, y2 );
|
|
1798
|
+
_ctx.stroke();
|
|
1669
1799
|
}
|
|
1670
1800
|
|
|
1671
1801
|
// conditionally strokes current path on canvas
|
|
1672
1802
|
const strokeIf = flag => {
|
|
1673
|
-
if ( flag &&
|
|
1674
|
-
const alpha =
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1803
|
+
if ( flag && _lineWidth ) {
|
|
1804
|
+
const alpha = _ctx.globalAlpha;
|
|
1805
|
+
_ctx.globalAlpha = 1;
|
|
1806
|
+
_ctx.stroke();
|
|
1807
|
+
_ctx.globalAlpha = alpha;
|
|
1678
1808
|
}
|
|
1679
1809
|
}
|
|
1680
1810
|
|
|
@@ -1682,7 +1812,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1682
1812
|
const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
|
|
1683
1813
|
|
|
1684
1814
|
// converts planar X,Y coordinates to radial coordinates
|
|
1685
|
-
const radialXY = ( x, y, dir ) => {
|
|
1815
|
+
const radialXY = ( x, y, dir = 1 ) => {
|
|
1686
1816
|
const height = radius + y,
|
|
1687
1817
|
angle = getAngle( x, dir );
|
|
1688
1818
|
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
@@ -1690,21 +1820,21 @@ export default class AudioMotionAnalyzer {
|
|
|
1690
1820
|
|
|
1691
1821
|
// draws a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
1692
1822
|
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1693
|
-
|
|
1694
|
-
for ( const dir of (
|
|
1823
|
+
_ctx.beginPath();
|
|
1824
|
+
for ( const dir of ( _mirror ? [1,-1] : [1] ) ) {
|
|
1695
1825
|
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1696
|
-
|
|
1697
|
-
|
|
1826
|
+
_ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1827
|
+
_ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1698
1828
|
if ( isRound )
|
|
1699
|
-
|
|
1829
|
+
_ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
|
|
1700
1830
|
else
|
|
1701
|
-
|
|
1702
|
-
|
|
1831
|
+
_ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1832
|
+
_ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1703
1833
|
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1704
|
-
|
|
1834
|
+
_ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
|
|
1705
1835
|
}
|
|
1706
1836
|
strokeIf( stroke );
|
|
1707
|
-
|
|
1837
|
+
_ctx.fill();
|
|
1708
1838
|
}
|
|
1709
1839
|
|
|
1710
1840
|
// converts a value in [0;1] range to a height in pixels that fits into the current LED elements
|
|
@@ -1712,35 +1842,36 @@ export default class AudioMotionAnalyzer {
|
|
|
1712
1842
|
|
|
1713
1843
|
// update energy information
|
|
1714
1844
|
const updateEnergy = newVal => {
|
|
1715
|
-
|
|
1716
|
-
if (
|
|
1717
|
-
|
|
1718
|
-
|
|
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 );
|
|
1719
1850
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
else if ( energy.peak > 0 )
|
|
1724
|
-
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;
|
|
1725
1854
|
}
|
|
1726
1855
|
}
|
|
1727
1856
|
|
|
1728
1857
|
// calculate and display (if enabled) the current frame rate
|
|
1729
1858
|
const updateFPS = () => {
|
|
1730
|
-
this.
|
|
1731
|
-
|
|
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++;
|
|
1732
1863
|
|
|
1733
1864
|
if ( elapsed >= 1000 ) {
|
|
1734
|
-
this._fps = this.
|
|
1735
|
-
this.
|
|
1865
|
+
this._fps = this._frames / ( elapsed / 1000 );
|
|
1866
|
+
this._frames = 0;
|
|
1736
1867
|
this._time = timestamp;
|
|
1737
1868
|
}
|
|
1738
1869
|
if ( this.showFPS ) {
|
|
1739
1870
|
const size = canvasX.height;
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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 );
|
|
1744
1875
|
}
|
|
1745
1876
|
}
|
|
1746
1877
|
|
|
@@ -1748,9 +1879,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1748
1879
|
|
|
1749
1880
|
let currentEnergy = 0;
|
|
1750
1881
|
|
|
1751
|
-
const
|
|
1752
|
-
|
|
1753
|
-
nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
|
|
1882
|
+
const nBars = _bars.length,
|
|
1883
|
+
nChannels = isSingle ? 1 : 2;
|
|
1754
1884
|
|
|
1755
1885
|
for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1756
1886
|
|
|
@@ -1758,8 +1888,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1758
1888
|
channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
1759
1889
|
colorStops = channelGradient.colorStops,
|
|
1760
1890
|
colorCount = colorStops.length,
|
|
1761
|
-
bgColor = ( ! showBgColor || isLeds && !
|
|
1762
|
-
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
|
|
1763
1894
|
|
|
1764
1895
|
// helper function for FFT data interpolation (uses fftData)
|
|
1765
1896
|
const interpolate = ( bin, ratio ) => {
|
|
@@ -1771,53 +1902,53 @@ export default class AudioMotionAnalyzer {
|
|
|
1771
1902
|
const setBarColor = ( value = 0, barIndex = 0 ) => {
|
|
1772
1903
|
let color;
|
|
1773
1904
|
// for mode 10, always use the channel gradient (ignore colorMode)
|
|
1774
|
-
if ( (
|
|
1775
|
-
color =
|
|
1905
|
+
if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode == 10 )
|
|
1906
|
+
color = _canvasGradients[ channel ];
|
|
1776
1907
|
else {
|
|
1777
|
-
const selectedIndex =
|
|
1908
|
+
const selectedIndex = _colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
|
|
1778
1909
|
color = colorStops[ selectedIndex ].color;
|
|
1779
1910
|
}
|
|
1780
|
-
|
|
1911
|
+
_ctx.fillStyle = _ctx.strokeStyle = color;
|
|
1781
1912
|
}
|
|
1782
1913
|
|
|
1783
1914
|
if ( useCanvas ) {
|
|
1784
1915
|
// clear the channel area, if in overlay mode
|
|
1785
1916
|
// this is done per channel to clear any residue below 0 off the top channel (especially in line graph mode with lineWidth > 1)
|
|
1786
|
-
if (
|
|
1787
|
-
|
|
1917
|
+
if ( overlay && mustClear )
|
|
1918
|
+
_ctx.clearRect( 0, channelTop - channelGap, canvas.width, channelHeight + channelGap );
|
|
1788
1919
|
|
|
1789
1920
|
// fill the analyzer background if needed (not overlay or overlay + showBgColor)
|
|
1790
|
-
if ( !
|
|
1791
|
-
if (
|
|
1792
|
-
|
|
1921
|
+
if ( ! overlay || showBgColor ) {
|
|
1922
|
+
if ( overlay )
|
|
1923
|
+
_ctx.globalAlpha = this.bgAlpha;
|
|
1793
1924
|
|
|
1794
|
-
|
|
1925
|
+
_ctx.fillStyle = bgColor;
|
|
1795
1926
|
|
|
1796
1927
|
// exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
|
|
1797
1928
|
if ( mustClear )
|
|
1798
|
-
|
|
1929
|
+
_ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
|
|
1799
1930
|
|
|
1800
|
-
|
|
1931
|
+
_ctx.globalAlpha = 1;
|
|
1801
1932
|
}
|
|
1802
1933
|
|
|
1803
1934
|
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
1804
|
-
if ( this.showScaleY && ! isLumi && !
|
|
1935
|
+
if ( this.showScaleY && ! isLumi && ! _radial && ( channel == 0 || ! isDualCombined ) )
|
|
1805
1936
|
drawScaleY( channelTop );
|
|
1806
1937
|
|
|
1807
1938
|
// set line width and dash for LEDs effect
|
|
1808
1939
|
if ( isLeds ) {
|
|
1809
|
-
|
|
1810
|
-
|
|
1940
|
+
_ctx.setLineDash( [ ledHeight, ledSpaceV ] );
|
|
1941
|
+
_ctx.lineWidth = _bars[0].width;
|
|
1811
1942
|
}
|
|
1812
1943
|
else // for outline effect ensure linewidth is not greater than half the bar width
|
|
1813
|
-
|
|
1944
|
+
_ctx.lineWidth = isOutline ? Math.min( _lineWidth, _bars[0].width / 2 ) : _lineWidth;
|
|
1814
1945
|
|
|
1815
1946
|
// set clip region
|
|
1816
|
-
|
|
1817
|
-
if ( !
|
|
1947
|
+
_ctx.save();
|
|
1948
|
+
if ( ! _radial ) {
|
|
1818
1949
|
const channelRegion = new Path2D();
|
|
1819
1950
|
channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
|
|
1820
|
-
|
|
1951
|
+
_ctx.clip( channelRegion );
|
|
1821
1952
|
}
|
|
1822
1953
|
} // if ( useCanvas )
|
|
1823
1954
|
|
|
@@ -1826,11 +1957,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1826
1957
|
this._analyzer[ channel ].getFloatFrequencyData( fftData );
|
|
1827
1958
|
|
|
1828
1959
|
// apply weighting
|
|
1829
|
-
if (
|
|
1960
|
+
if ( _weightingFilter )
|
|
1830
1961
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1831
1962
|
|
|
1832
1963
|
// start drawing path (for mode 10)
|
|
1833
|
-
|
|
1964
|
+
_ctx.beginPath();
|
|
1834
1965
|
|
|
1835
1966
|
// store line graph points to create mirror effect in radial mode
|
|
1836
1967
|
let points = [];
|
|
@@ -1839,7 +1970,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1839
1970
|
|
|
1840
1971
|
for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
|
|
1841
1972
|
|
|
1842
|
-
const bar =
|
|
1973
|
+
const bar = _bars[ barIndex ],
|
|
1843
1974
|
{ posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1844
1975
|
|
|
1845
1976
|
let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
|
|
@@ -1861,13 +1992,13 @@ export default class AudioMotionAnalyzer {
|
|
|
1861
1992
|
bar.hold[ channel ]--;
|
|
1862
1993
|
// if hold is negative, it becomes the "acceleration" for peak drop
|
|
1863
1994
|
if ( bar.hold[ channel ] < 0 )
|
|
1864
|
-
bar.peak[ channel ] += bar.hold[ channel ] /
|
|
1995
|
+
bar.peak[ channel ] += bar.hold[ channel ] / ( holdFrames * holdFrames / 2 );
|
|
1865
1996
|
}
|
|
1866
1997
|
|
|
1867
1998
|
// check if it's a new peak for this bar
|
|
1868
1999
|
if ( barValue >= bar.peak[ channel ] ) {
|
|
1869
2000
|
bar.peak[ channel ] = barValue;
|
|
1870
|
-
bar.hold[ channel ] =
|
|
2001
|
+
bar.hold[ channel ] = holdFrames;
|
|
1871
2002
|
}
|
|
1872
2003
|
|
|
1873
2004
|
// if not using the canvas, move earlier to the next bar
|
|
@@ -1876,71 +2007,67 @@ export default class AudioMotionAnalyzer {
|
|
|
1876
2007
|
|
|
1877
2008
|
// set opacity for bar effects
|
|
1878
2009
|
if ( isLumi || isAlpha )
|
|
1879
|
-
|
|
2010
|
+
_ctx.globalAlpha = barValue;
|
|
1880
2011
|
else if ( isOutline )
|
|
1881
|
-
|
|
2012
|
+
_ctx.globalAlpha = fillAlpha;
|
|
1882
2013
|
|
|
1883
2014
|
// set fillStyle and strokeStyle for the current bar
|
|
1884
2015
|
setBarColor( barValue, barIndex );
|
|
1885
2016
|
|
|
1886
2017
|
// compute actual bar height on screen
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
// invert bar for radial channel 1
|
|
1890
|
-
if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
|
|
1891
|
-
barHeight *= -1;
|
|
2018
|
+
const barHeight = ( isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0 ) * direction;
|
|
1892
2019
|
|
|
1893
2020
|
// Draw current bar or line segment
|
|
1894
2021
|
|
|
1895
|
-
if (
|
|
2022
|
+
if ( _mode == 10 ) {
|
|
1896
2023
|
// compute the average between the initial bar (barIndex==0) and the next one
|
|
1897
2024
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
1898
|
-
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[
|
|
2025
|
+
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ _bars[1].binLo ] ) * maxBarHeight * direction + barHeight ) / 2;
|
|
1899
2026
|
|
|
1900
|
-
if (
|
|
2027
|
+
if ( _radial ) {
|
|
1901
2028
|
if ( barIndex == 0 )
|
|
1902
|
-
|
|
2029
|
+
_ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ) ) );
|
|
1903
2030
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
1904
2031
|
if ( posX >= 0 ) {
|
|
1905
2032
|
const point = [ posX, barHeight ];
|
|
1906
|
-
|
|
2033
|
+
_ctx.lineTo( ...radialXY( ...point ) );
|
|
1907
2034
|
points.push( point );
|
|
1908
2035
|
}
|
|
1909
2036
|
}
|
|
1910
2037
|
else { // Linear
|
|
1911
2038
|
if ( barIndex == 0 ) {
|
|
1912
2039
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
1913
|
-
if (
|
|
2040
|
+
if ( _mirror != -1 ) {
|
|
1914
2041
|
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
1915
|
-
|
|
2042
|
+
_ctx.moveTo( initialX - _lineWidth, analyzerBottom - prevFFTData );
|
|
1916
2043
|
}
|
|
1917
2044
|
else
|
|
1918
|
-
|
|
2045
|
+
_ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
|
|
1919
2046
|
}
|
|
1920
2047
|
// draw line to the current point
|
|
1921
2048
|
// avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
|
|
1922
|
-
if (
|
|
1923
|
-
|
|
2049
|
+
if ( _mirror != -1 || posX >= initialX )
|
|
2050
|
+
_ctx.lineTo( posX, analyzerBottom - barHeight );
|
|
1924
2051
|
}
|
|
1925
2052
|
}
|
|
1926
2053
|
else {
|
|
1927
2054
|
if ( isLeds ) {
|
|
1928
2055
|
// draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
|
|
1929
|
-
if ( showBgColor && !
|
|
1930
|
-
const alpha =
|
|
1931
|
-
|
|
1932
|
-
|
|
2056
|
+
if ( showBgColor && ! overlay && ( channel == 0 || ! isDualCombined ) ) {
|
|
2057
|
+
const alpha = _ctx.globalAlpha;
|
|
2058
|
+
_ctx.strokeStyle = LEDS_UNLIT_COLOR;
|
|
2059
|
+
_ctx.globalAlpha = 1;
|
|
1933
2060
|
strokeBar( barCenter, channelTop, analyzerBottom );
|
|
1934
2061
|
// restore properties
|
|
1935
|
-
|
|
1936
|
-
|
|
2062
|
+
_ctx.strokeStyle = _ctx.fillStyle;
|
|
2063
|
+
_ctx.globalAlpha = alpha;
|
|
1937
2064
|
}
|
|
1938
2065
|
if ( isTrueLeds ) {
|
|
1939
2066
|
// ledPosY() is used below to fit one entire led height into the selected range
|
|
1940
2067
|
const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
|
|
1941
2068
|
let last = analyzerBottom;
|
|
1942
2069
|
for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
|
|
1943
|
-
|
|
2070
|
+
_ctx.strokeStyle = colorStops[ i ].color;
|
|
1944
2071
|
let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
|
|
1945
2072
|
strokeBar( barCenter, last, y );
|
|
1946
2073
|
last = y - ledSpaceV;
|
|
@@ -1950,53 +2077,53 @@ export default class AudioMotionAnalyzer {
|
|
|
1950
2077
|
strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
|
|
1951
2078
|
}
|
|
1952
2079
|
else if ( posX >= initialX ) {
|
|
1953
|
-
if (
|
|
2080
|
+
if ( _radial )
|
|
1954
2081
|
radialPoly( posX, 0, width, barHeight, isOutline );
|
|
1955
2082
|
else if ( isRound ) {
|
|
1956
2083
|
const halfWidth = width / 2,
|
|
1957
2084
|
y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
|
|
1958
2085
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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 );
|
|
1964
2091
|
strokeIf( isOutline );
|
|
1965
|
-
|
|
2092
|
+
_ctx.fill();
|
|
1966
2093
|
}
|
|
1967
2094
|
else {
|
|
1968
|
-
const offset = isOutline ?
|
|
1969
|
-
|
|
1970
|
-
|
|
2095
|
+
const offset = isOutline ? _ctx.lineWidth : 0;
|
|
2096
|
+
_ctx.beginPath();
|
|
2097
|
+
_ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
|
|
1971
2098
|
strokeIf( isOutline );
|
|
1972
|
-
|
|
2099
|
+
_ctx.fill();
|
|
1973
2100
|
}
|
|
1974
2101
|
}
|
|
1975
2102
|
}
|
|
1976
2103
|
|
|
1977
2104
|
// Draw peak
|
|
1978
2105
|
const peak = bar.peak[ channel ];
|
|
1979
|
-
if ( peak > 0 &&
|
|
2106
|
+
if ( peak > 0 && showPeaks && ! showPeakLine && ! isLumi && posX >= initialX && posX < finalX ) {
|
|
1980
2107
|
// set opacity
|
|
1981
|
-
if ( isOutline &&
|
|
1982
|
-
|
|
2108
|
+
if ( isOutline && _lineWidth > 0 )
|
|
2109
|
+
_ctx.globalAlpha = 1;
|
|
1983
2110
|
else if ( isAlpha )
|
|
1984
|
-
|
|
2111
|
+
_ctx.globalAlpha = peak;
|
|
1985
2112
|
|
|
1986
2113
|
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
1987
|
-
if (
|
|
2114
|
+
if ( _colorMode == COLOR_BAR_LEVEL || isTrueLeds )
|
|
1988
2115
|
setBarColor( peak );
|
|
1989
2116
|
|
|
1990
2117
|
// render peak according to current mode / effect
|
|
1991
2118
|
if ( isLeds ) {
|
|
1992
2119
|
const ledPeak = ledPosY( peak );
|
|
1993
2120
|
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
1994
|
-
|
|
2121
|
+
_ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
1995
2122
|
}
|
|
1996
|
-
else if ( !
|
|
1997
|
-
|
|
1998
|
-
else if (
|
|
1999
|
-
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 );
|
|
2000
2127
|
}
|
|
2001
2128
|
|
|
2002
2129
|
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
@@ -2005,41 +2132,72 @@ export default class AudioMotionAnalyzer {
|
|
|
2005
2132
|
if ( ! useCanvas )
|
|
2006
2133
|
continue;
|
|
2007
2134
|
|
|
2008
|
-
|
|
2135
|
+
_ctx.restore(); // restore clip region
|
|
2009
2136
|
|
|
2010
2137
|
// restore global alpha
|
|
2011
|
-
|
|
2138
|
+
_ctx.globalAlpha = 1;
|
|
2012
2139
|
|
|
2013
2140
|
// Fill/stroke drawing path for mode 10
|
|
2014
|
-
if (
|
|
2141
|
+
if ( _mode == 10 ) {
|
|
2015
2142
|
setBarColor(); // select channel gradient
|
|
2016
2143
|
|
|
2017
|
-
if (
|
|
2018
|
-
if (
|
|
2144
|
+
if ( _radial ) {
|
|
2145
|
+
if ( _mirror ) {
|
|
2019
2146
|
let p;
|
|
2020
2147
|
while ( p = points.pop() )
|
|
2021
|
-
|
|
2148
|
+
_ctx.lineTo( ...radialXY( ...p, -1 ) );
|
|
2022
2149
|
}
|
|
2023
|
-
|
|
2150
|
+
_ctx.closePath();
|
|
2024
2151
|
}
|
|
2025
2152
|
|
|
2026
|
-
if (
|
|
2027
|
-
|
|
2153
|
+
if ( _lineWidth > 0 )
|
|
2154
|
+
_ctx.stroke();
|
|
2028
2155
|
|
|
2029
2156
|
if ( fillAlpha > 0 ) {
|
|
2030
|
-
if (
|
|
2157
|
+
if ( _radial ) {
|
|
2031
2158
|
// exclude the center circle from the fill area
|
|
2032
|
-
|
|
2033
|
-
|
|
2159
|
+
_ctx.moveTo( centerX + radius, centerY );
|
|
2160
|
+
_ctx.arc( centerX, centerY, radius, 0, TAU, true );
|
|
2034
2161
|
}
|
|
2035
2162
|
else { // close the fill area
|
|
2036
|
-
|
|
2037
|
-
|
|
2163
|
+
_ctx.lineTo( finalX, analyzerBottom );
|
|
2164
|
+
_ctx.lineTo( initialX, analyzerBottom );
|
|
2038
2165
|
}
|
|
2039
2166
|
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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
|
+
}
|
|
2043
2201
|
}
|
|
2044
2202
|
}
|
|
2045
2203
|
|
|
@@ -2052,14 +2210,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2052
2210
|
|
|
2053
2211
|
if ( useCanvas ) {
|
|
2054
2212
|
// Mirror effect
|
|
2055
|
-
if (
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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 );
|
|
2059
2217
|
}
|
|
2060
2218
|
|
|
2061
2219
|
// restore solid lines
|
|
2062
|
-
|
|
2220
|
+
_ctx.setLineDash([]);
|
|
2063
2221
|
|
|
2064
2222
|
// draw frequency scale (X-axis)
|
|
2065
2223
|
drawScaleX();
|
|
@@ -2070,14 +2228,11 @@ export default class AudioMotionAnalyzer {
|
|
|
2070
2228
|
|
|
2071
2229
|
// call callback function, if defined
|
|
2072
2230
|
if ( this.onCanvasDraw ) {
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
this.onCanvasDraw( this, { timestamp,
|
|
2076
|
-
|
|
2231
|
+
_ctx.save();
|
|
2232
|
+
_ctx.fillStyle = _ctx.strokeStyle = _canvasGradients[0];
|
|
2233
|
+
this.onCanvasDraw( this, { timestamp, _canvasGradients } );
|
|
2234
|
+
_ctx.restore();
|
|
2077
2235
|
}
|
|
2078
|
-
|
|
2079
|
-
// schedule next canvas update
|
|
2080
|
-
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
2081
2236
|
}
|
|
2082
2237
|
|
|
2083
2238
|
/**
|
|
@@ -2110,24 +2265,19 @@ export default class AudioMotionAnalyzer {
|
|
|
2110
2265
|
* Generate currently selected gradient
|
|
2111
2266
|
*/
|
|
2112
2267
|
_makeGrad() {
|
|
2113
|
-
|
|
2114
2268
|
if ( ! this._ready )
|
|
2115
2269
|
return;
|
|
2116
2270
|
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
channelLayout = this._chLayout,
|
|
2271
|
+
const { canvas, _ctx, _radial, _reflexRatio } = this,
|
|
2272
|
+
{ analyzerWidth, initialX, radius } = this._aux,
|
|
2120
2273
|
{ isLumi } = this._flg,
|
|
2121
|
-
|
|
2122
|
-
|
|
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;
|
|
2123
2280
|
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
2124
|
-
analyzerRatio = 1 - this._reflexRatio,
|
|
2125
|
-
{ analyzerWidth, initialX, radius } = this._aux;
|
|
2126
|
-
|
|
2127
|
-
// for radial mode
|
|
2128
|
-
const centerX = canvas.width >> 1,
|
|
2129
|
-
centerY = canvas.height >> 1,
|
|
2130
|
-
maxRadius = Math.min( centerX, centerY );
|
|
2131
2281
|
|
|
2132
2282
|
for ( const channel of [0,1] ) {
|
|
2133
2283
|
const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
@@ -2136,13 +2286,13 @@ export default class AudioMotionAnalyzer {
|
|
|
2136
2286
|
|
|
2137
2287
|
let grad;
|
|
2138
2288
|
|
|
2139
|
-
if (
|
|
2140
|
-
grad =
|
|
2289
|
+
if ( _radial )
|
|
2290
|
+
grad = _ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * isDualVertical );
|
|
2141
2291
|
else
|
|
2142
|
-
grad =
|
|
2292
|
+
grad = _ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
|
|
2143
2293
|
|
|
2144
2294
|
if ( colorStops ) {
|
|
2145
|
-
const dual =
|
|
2295
|
+
const dual = isDualVertical && ! this._splitGradient && ( ! isHorizontal || _radial );
|
|
2146
2296
|
|
|
2147
2297
|
for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
|
|
2148
2298
|
const maxIndex = colorStops.length - 1;
|
|
@@ -2155,17 +2305,17 @@ export default class AudioMotionAnalyzer {
|
|
|
2155
2305
|
offset /= 2;
|
|
2156
2306
|
|
|
2157
2307
|
// constrain the offset within the useful analyzer areas (avoid reflex areas)
|
|
2158
|
-
if (
|
|
2308
|
+
if ( isDualVertical && ! isLumi && ! _radial && ! isHorizontal ) {
|
|
2159
2309
|
offset *= analyzerRatio;
|
|
2160
2310
|
// skip the first reflex area in split mode
|
|
2161
2311
|
if ( ! dual && offset > .5 * analyzerRatio )
|
|
2162
|
-
offset += .5 *
|
|
2312
|
+
offset += .5 * _reflexRatio;
|
|
2163
2313
|
}
|
|
2164
2314
|
|
|
2165
2315
|
// only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
|
|
2166
2316
|
if ( channelArea == 1 ) {
|
|
2167
2317
|
// add colors in reverse order if radial or lumi are active
|
|
2168
|
-
if (
|
|
2318
|
+
if ( _radial || isLumi ) {
|
|
2169
2319
|
const revIndex = maxIndex - index;
|
|
2170
2320
|
colorStop = colorStops[ revIndex ];
|
|
2171
2321
|
offset = 1 - colorStop.pos / 2;
|
|
@@ -2183,7 +2333,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2183
2333
|
grad.addColorStop( offset, colorStop.color );
|
|
2184
2334
|
|
|
2185
2335
|
// create additional color stop at the end of first channel to prevent bleeding
|
|
2186
|
-
if (
|
|
2336
|
+
if ( isDualVertical && index == maxIndex && offset < .5 )
|
|
2187
2337
|
grad.addColorStop( .5, colorStop.color );
|
|
2188
2338
|
});
|
|
2189
2339
|
} // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
|
|
@@ -2218,12 +2368,10 @@ export default class AudioMotionAnalyzer {
|
|
|
2218
2368
|
* Internal function to change canvas dimensions on demand
|
|
2219
2369
|
*/
|
|
2220
2370
|
_setCanvas( reason ) {
|
|
2221
|
-
// if initialization is not finished, quit
|
|
2222
2371
|
if ( ! this._ready )
|
|
2223
2372
|
return;
|
|
2224
2373
|
|
|
2225
|
-
const
|
|
2226
|
-
canvas = ctx.canvas,
|
|
2374
|
+
const { canvas, _ctx } = this,
|
|
2227
2375
|
canvasX = this._scaleX.canvas,
|
|
2228
2376
|
pixelRatio = window.devicePixelRatio / ( this._loRes + 1 );
|
|
2229
2377
|
|
|
@@ -2254,12 +2402,12 @@ export default class AudioMotionAnalyzer {
|
|
|
2254
2402
|
|
|
2255
2403
|
// if not in overlay mode, paint the canvas black
|
|
2256
2404
|
if ( ! this.overlay ) {
|
|
2257
|
-
|
|
2258
|
-
|
|
2405
|
+
_ctx.fillStyle = '#000';
|
|
2406
|
+
_ctx.fillRect( 0, 0, newWidth, newHeight );
|
|
2259
2407
|
}
|
|
2260
2408
|
|
|
2261
2409
|
// set lineJoin property for area fill mode (this is reset whenever the canvas size changes)
|
|
2262
|
-
|
|
2410
|
+
_ctx.lineJoin = 'bevel';
|
|
2263
2411
|
|
|
2264
2412
|
// update dimensions of the scale canvas
|
|
2265
2413
|
canvasX.width = newWidth;
|
|
@@ -2324,6 +2472,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2324
2472
|
loRes : false,
|
|
2325
2473
|
lumiBars : false,
|
|
2326
2474
|
maxDecibels : -25,
|
|
2475
|
+
maxFPS : 0,
|
|
2327
2476
|
maxFreq : 22000,
|
|
2328
2477
|
minDecibels : -85,
|
|
2329
2478
|
minFreq : 20,
|
|
@@ -2332,6 +2481,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2332
2481
|
noteLabels : false,
|
|
2333
2482
|
outlineBars : false,
|
|
2334
2483
|
overlay : false,
|
|
2484
|
+
peakLine : false,
|
|
2335
2485
|
radial : false,
|
|
2336
2486
|
reflexAlpha : 0.15,
|
|
2337
2487
|
reflexBright : 1,
|