audiomotion-analyzer 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -30
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +634 -446
- package/src/index.d.ts +18 -2
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* audioMotion-analyzer
|
|
3
3
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
4
4
|
*
|
|
5
|
-
* @version 4.
|
|
5
|
+
* @version 4.1.1
|
|
6
6
|
* @author Henrique Avila Vianna <hvianna@gmail.com> <https://henriquevianna.com>
|
|
7
7
|
* @license AGPL-3.0-or-later
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const VERSION = '4.
|
|
10
|
+
const VERSION = '4.1.1';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
13
|
const TAU = 2 * Math.PI,
|
|
@@ -19,6 +19,9 @@ const CANVAS_BACKGROUND_COLOR = '#000',
|
|
|
19
19
|
CHANNEL_COMBINED = 'dual-combined',
|
|
20
20
|
CHANNEL_SINGLE = 'single',
|
|
21
21
|
CHANNEL_VERTICAL = 'dual-vertical',
|
|
22
|
+
COLOR_BAR_INDEX = 'bar-index',
|
|
23
|
+
COLOR_BAR_LEVEL = 'bar-level',
|
|
24
|
+
COLOR_GRADIENT = 'gradient',
|
|
22
25
|
GRADIENT_DEFAULT_BGCOLOR = '#111',
|
|
23
26
|
FILTER_NONE = '',
|
|
24
27
|
FILTER_A = 'A',
|
|
@@ -49,9 +52,9 @@ const PRISM = [ '#a35', '#c66', '#e94', '#ed0', '#9d5', '#4d8', '#2cb', '#0bc',
|
|
|
49
52
|
GRADIENTS = [
|
|
50
53
|
[ 'classic', {
|
|
51
54
|
colorStops: [
|
|
52
|
-
'
|
|
53
|
-
{
|
|
54
|
-
'
|
|
55
|
+
'red',
|
|
56
|
+
{ color: 'yellow', level: .85, pos: .6 },
|
|
57
|
+
{ color: 'lime', level: .475 }
|
|
55
58
|
]
|
|
56
59
|
}],
|
|
57
60
|
[ 'prism', {
|
|
@@ -92,9 +95,25 @@ class AudioMotionError extends Error {
|
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
// helper function
|
|
98
|
+
// helper function - output deprecation warning message on console
|
|
96
99
|
const deprecate = ( name, alternative ) => console.warn( `${name} is deprecated. Use ${alternative} instead.` );
|
|
97
100
|
|
|
101
|
+
// helper function - validate a given value with an array of strings (by default, all lowercase)
|
|
102
|
+
// returns the validated value, or the first element of `list` if `value` is not found in the array
|
|
103
|
+
const validateFromList = ( value, list, modifier = 'toLowerCase' ) => list[ Math.max( 0, list.indexOf( ( '' + value )[ modifier ]() ) ) ];
|
|
104
|
+
|
|
105
|
+
// Polyfill for Array.findLastIndex()
|
|
106
|
+
if ( ! Array.prototype.findLastIndex ) {
|
|
107
|
+
Array.prototype.findLastIndex = function( callback ) {
|
|
108
|
+
let index = this.length;
|
|
109
|
+
while ( index-- > 0 ) {
|
|
110
|
+
if ( callback( this[ index ] ) )
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
return -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
98
117
|
// AudioMotionAnalyzer class
|
|
99
118
|
|
|
100
119
|
export default class AudioMotionAnalyzer {
|
|
@@ -110,10 +129,12 @@ export default class AudioMotionAnalyzer {
|
|
|
110
129
|
|
|
111
130
|
this._ready = false;
|
|
112
131
|
|
|
113
|
-
// Initialize internal
|
|
132
|
+
// Initialize internal objects
|
|
133
|
+
this._aux = {};
|
|
134
|
+
this._flg = {};
|
|
114
135
|
this._gradients = {}; // registered gradients
|
|
115
136
|
this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
|
|
116
|
-
this._canvasGradients = []; //
|
|
137
|
+
this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
|
|
117
138
|
|
|
118
139
|
// Register built-in gradients
|
|
119
140
|
for ( const [ name, options ] of GRADIENTS )
|
|
@@ -262,9 +283,6 @@ export default class AudioMotionAnalyzer {
|
|
|
262
283
|
}
|
|
263
284
|
window.addEventListener( 'click', unlockContext );
|
|
264
285
|
|
|
265
|
-
// initialize internal variables
|
|
266
|
-
this._calcAux();
|
|
267
|
-
|
|
268
286
|
// Set configuration options and use defaults for any missing properties
|
|
269
287
|
this._setProps( options, true );
|
|
270
288
|
|
|
@@ -290,7 +308,7 @@ export default class AudioMotionAnalyzer {
|
|
|
290
308
|
}
|
|
291
309
|
set alphaBars( value ) {
|
|
292
310
|
this._alphaBars = !! value;
|
|
293
|
-
this.
|
|
311
|
+
this._calcBars();
|
|
294
312
|
}
|
|
295
313
|
|
|
296
314
|
get ansiBands() {
|
|
@@ -306,15 +324,14 @@ export default class AudioMotionAnalyzer {
|
|
|
306
324
|
}
|
|
307
325
|
set barSpace( value ) {
|
|
308
326
|
this._barSpace = +value || 0;
|
|
309
|
-
this.
|
|
327
|
+
this._calcBars();
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
get channelLayout() {
|
|
313
331
|
return this._chLayout;
|
|
314
332
|
}
|
|
315
333
|
set channelLayout( value ) {
|
|
316
|
-
|
|
317
|
-
this._chLayout = LAYOUTS[ Math.max( 0, LAYOUTS.indexOf( ( '' + value ).toLowerCase() ) ) ];
|
|
334
|
+
this._chLayout = validateFromList( value, [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ] );
|
|
318
335
|
|
|
319
336
|
// update node connections
|
|
320
337
|
this._input.disconnect();
|
|
@@ -323,13 +340,17 @@ export default class AudioMotionAnalyzer {
|
|
|
323
340
|
if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
|
|
324
341
|
this._analyzer[0].connect( this._chLayout != CHANNEL_SINGLE ? this._merger : this._output );
|
|
325
342
|
|
|
326
|
-
|
|
327
|
-
this._calcAux();
|
|
328
|
-
this._createScales();
|
|
329
|
-
this._calcLeds();
|
|
343
|
+
this._calcBars();
|
|
330
344
|
this._makeGrad();
|
|
331
345
|
}
|
|
332
346
|
|
|
347
|
+
get colorMode() {
|
|
348
|
+
return this._colorMode;
|
|
349
|
+
}
|
|
350
|
+
set colorMode( value ) {
|
|
351
|
+
this._colorMode = validateFromList( value, [ COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL ] );
|
|
352
|
+
}
|
|
353
|
+
|
|
333
354
|
get fftSize() {
|
|
334
355
|
return this._analyzer[0].fftSize;
|
|
335
356
|
}
|
|
@@ -345,9 +366,7 @@ export default class AudioMotionAnalyzer {
|
|
|
345
366
|
return this._frequencyScale;
|
|
346
367
|
}
|
|
347
368
|
set frequencyScale( value ) {
|
|
348
|
-
|
|
349
|
-
this._frequencyScale = FREQUENCY_SCALES[ Math.max( 0, FREQUENCY_SCALES.indexOf( ( '' + value ).toLowerCase() ) ) ];
|
|
350
|
-
this._calcAux();
|
|
369
|
+
this._frequencyScale = validateFromList( value, [ SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR ] );
|
|
351
370
|
this._calcBars();
|
|
352
371
|
}
|
|
353
372
|
|
|
@@ -385,7 +404,7 @@ export default class AudioMotionAnalyzer {
|
|
|
385
404
|
}
|
|
386
405
|
set ledBars( value ) {
|
|
387
406
|
this._showLeds = !! value;
|
|
388
|
-
this.
|
|
407
|
+
this._calcBars();
|
|
389
408
|
}
|
|
390
409
|
|
|
391
410
|
get linearAmplitude() {
|
|
@@ -402,6 +421,13 @@ export default class AudioMotionAnalyzer {
|
|
|
402
421
|
this._linearBoost = value >= 1 ? +value : 1;
|
|
403
422
|
}
|
|
404
423
|
|
|
424
|
+
get lineWidth() {
|
|
425
|
+
return this._lineWidth;
|
|
426
|
+
}
|
|
427
|
+
set lineWidth( value ) {
|
|
428
|
+
this._lineWidth = +value || 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
405
431
|
get loRes() {
|
|
406
432
|
return this._loRes;
|
|
407
433
|
}
|
|
@@ -415,8 +441,7 @@ export default class AudioMotionAnalyzer {
|
|
|
415
441
|
}
|
|
416
442
|
set lumiBars( value ) {
|
|
417
443
|
this._lumiBars = !! value;
|
|
418
|
-
this.
|
|
419
|
-
this._calcLeds();
|
|
444
|
+
this._calcBars();
|
|
420
445
|
this._makeGrad();
|
|
421
446
|
}
|
|
422
447
|
|
|
@@ -465,7 +490,6 @@ export default class AudioMotionAnalyzer {
|
|
|
465
490
|
}
|
|
466
491
|
set mirror( value ) {
|
|
467
492
|
this._mirror = Math.sign( value ) | 0; // ensure only -1, 0 or 1
|
|
468
|
-
this._calcAux();
|
|
469
493
|
this._calcBars();
|
|
470
494
|
this._makeGrad();
|
|
471
495
|
}
|
|
@@ -477,7 +501,6 @@ export default class AudioMotionAnalyzer {
|
|
|
477
501
|
const mode = value | 0;
|
|
478
502
|
if ( mode >= 0 && mode <= 10 && mode != 9 ) {
|
|
479
503
|
this._mode = mode;
|
|
480
|
-
this._calcAux();
|
|
481
504
|
this._calcBars();
|
|
482
505
|
this._makeGrad();
|
|
483
506
|
}
|
|
@@ -498,7 +521,7 @@ export default class AudioMotionAnalyzer {
|
|
|
498
521
|
}
|
|
499
522
|
set outlineBars( value ) {
|
|
500
523
|
this._outlineBars = !! value;
|
|
501
|
-
this.
|
|
524
|
+
this._calcBars();
|
|
502
525
|
}
|
|
503
526
|
|
|
504
527
|
get radial() {
|
|
@@ -506,7 +529,6 @@ export default class AudioMotionAnalyzer {
|
|
|
506
529
|
}
|
|
507
530
|
set radial( value ) {
|
|
508
531
|
this._radial = !! value;
|
|
509
|
-
this._calcAux();
|
|
510
532
|
this._calcBars();
|
|
511
533
|
this._makeGrad();
|
|
512
534
|
}
|
|
@@ -520,12 +542,19 @@ export default class AudioMotionAnalyzer {
|
|
|
520
542
|
throw new AudioMotionError( ERR_REFLEX_OUT_OF_RANGE );
|
|
521
543
|
else {
|
|
522
544
|
this._reflexRatio = value;
|
|
523
|
-
this.
|
|
545
|
+
this._calcBars();
|
|
524
546
|
this._makeGrad();
|
|
525
|
-
this._calcLeds();
|
|
526
547
|
}
|
|
527
548
|
}
|
|
528
549
|
|
|
550
|
+
get roundBars() {
|
|
551
|
+
return this._roundBars;
|
|
552
|
+
}
|
|
553
|
+
set roundBars( value ) {
|
|
554
|
+
this._roundBars = !! value;
|
|
555
|
+
this._calcBars();
|
|
556
|
+
}
|
|
557
|
+
|
|
529
558
|
get smoothing() {
|
|
530
559
|
return this._analyzer[0].smoothingTimeConstant;
|
|
531
560
|
}
|
|
@@ -561,6 +590,13 @@ export default class AudioMotionAnalyzer {
|
|
|
561
590
|
this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
|
|
562
591
|
}
|
|
563
592
|
|
|
593
|
+
get trueLeds() {
|
|
594
|
+
return this._trueLeds;
|
|
595
|
+
}
|
|
596
|
+
set trueLeds( value ) {
|
|
597
|
+
this._trueLeds = !! value;
|
|
598
|
+
}
|
|
599
|
+
|
|
564
600
|
get volume() {
|
|
565
601
|
return this._output.gain.value;
|
|
566
602
|
}
|
|
@@ -572,8 +608,7 @@ export default class AudioMotionAnalyzer {
|
|
|
572
608
|
return this._weightingFilter;
|
|
573
609
|
}
|
|
574
610
|
set weightingFilter( value ) {
|
|
575
|
-
|
|
576
|
-
this._weightingFilter = WEIGHTING_FILTERS[ Math.max( 0, WEIGHTING_FILTERS.indexOf( ( '' + value ).toUpperCase() ) ) ];
|
|
611
|
+
this._weightingFilter = validateFromList( value, [ FILTER_NONE, FILTER_A, FILTER_B, FILTER_C, FILTER_D, FILTER_468 ], 'toUpperCase' );
|
|
577
612
|
}
|
|
578
613
|
|
|
579
614
|
get width() {
|
|
@@ -611,32 +646,35 @@ export default class AudioMotionAnalyzer {
|
|
|
611
646
|
return this._fsWidth;
|
|
612
647
|
}
|
|
613
648
|
get isAlphaBars() {
|
|
614
|
-
return this.
|
|
649
|
+
return this._flg.isAlpha;
|
|
615
650
|
}
|
|
616
651
|
get isBandsMode() {
|
|
617
|
-
return this.
|
|
652
|
+
return this._flg.isBands;
|
|
618
653
|
}
|
|
619
654
|
get isFullscreen() {
|
|
620
655
|
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
621
656
|
}
|
|
622
657
|
get isLedBars() {
|
|
623
|
-
return this.
|
|
658
|
+
return this._flg.isLeds;
|
|
624
659
|
}
|
|
625
660
|
get isLumiBars() {
|
|
626
|
-
return this.
|
|
661
|
+
return this._flg.isLumi;
|
|
627
662
|
}
|
|
628
663
|
get isOctaveBands() {
|
|
629
|
-
return this.
|
|
664
|
+
return this._flg.isOctaves;
|
|
630
665
|
}
|
|
631
666
|
get isOn() {
|
|
632
667
|
return this._runId !== undefined;
|
|
633
668
|
}
|
|
634
669
|
get isOutlineBars() {
|
|
635
|
-
return this.
|
|
670
|
+
return this._flg.isOutline;
|
|
636
671
|
}
|
|
637
672
|
get pixelRatio() {
|
|
638
673
|
return this._pixelRatio;
|
|
639
674
|
}
|
|
675
|
+
get isRoundBars() {
|
|
676
|
+
return this._flg.isRound;
|
|
677
|
+
}
|
|
640
678
|
static get version() {
|
|
641
679
|
return VERSION;
|
|
642
680
|
}
|
|
@@ -790,19 +828,41 @@ export default class AudioMotionAnalyzer {
|
|
|
790
828
|
* @param {object} options
|
|
791
829
|
*/
|
|
792
830
|
registerGradient( name, options ) {
|
|
793
|
-
if ( typeof name
|
|
831
|
+
if ( typeof name != 'string' || name.trim().length == 0 )
|
|
794
832
|
throw new AudioMotionError( ERR_GRADIENT_INVALID_NAME );
|
|
795
833
|
|
|
796
|
-
if ( typeof options
|
|
834
|
+
if ( typeof options != 'object' )
|
|
797
835
|
throw new AudioMotionError( ERR_GRADIENT_NOT_AN_OBJECT );
|
|
798
836
|
|
|
799
|
-
|
|
837
|
+
const { colorStops } = options;
|
|
838
|
+
|
|
839
|
+
if ( ! Array.isArray( colorStops ) || ! colorStops.length )
|
|
800
840
|
throw new AudioMotionError( ERR_GRADIENT_MISSING_COLOR );
|
|
801
841
|
|
|
842
|
+
const count = colorStops.length,
|
|
843
|
+
isInvalid = val => +val != val || val < 0 || val > 1;
|
|
844
|
+
|
|
845
|
+
// normalize all colorStops as objects with `pos`, `color` and `level` properties
|
|
846
|
+
colorStops.forEach( ( colorStop, index ) => {
|
|
847
|
+
const pos = index / Math.max( 1, count - 1 );
|
|
848
|
+
if ( typeof colorStop != 'object' ) // only color string was defined
|
|
849
|
+
colorStops[ index ] = { pos, color: colorStop };
|
|
850
|
+
else if ( isInvalid( colorStop.pos ) )
|
|
851
|
+
colorStop.pos = pos;
|
|
852
|
+
|
|
853
|
+
if ( isInvalid( colorStop.level ) )
|
|
854
|
+
colorStops[ index ].level = 1 - index / count;
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// make sure colorStops is in descending `level` order and that the first one has `level == 1`
|
|
858
|
+
// this is crucial for proper operation of 'bar-level' colorMode!
|
|
859
|
+
colorStops.sort( ( a, b ) => a.level < b.level ? 1 : a.level > b.level ? -1 : 0 );
|
|
860
|
+
colorStops[0].level = 1;
|
|
861
|
+
|
|
802
862
|
this._gradients[ name ] = {
|
|
803
863
|
bgColor: options.bgColor || GRADIENT_DEFAULT_BGCOLOR,
|
|
804
864
|
dir: options.dir,
|
|
805
|
-
colorStops:
|
|
865
|
+
colorStops: colorStops
|
|
806
866
|
};
|
|
807
867
|
|
|
808
868
|
// if the registered gradient is one of the currently selected gradients, regenerate them
|
|
@@ -854,7 +914,7 @@ export default class AudioMotionAnalyzer {
|
|
|
854
914
|
}
|
|
855
915
|
|
|
856
916
|
this._ledParams = maxLeds > 0 && spaceV > 0 && spaceH >= 0 ? [ maxLeds, spaceV, spaceH ] : undefined;
|
|
857
|
-
this.
|
|
917
|
+
this._calcBars();
|
|
858
918
|
}
|
|
859
919
|
|
|
860
920
|
/**
|
|
@@ -939,37 +999,7 @@ export default class AudioMotionAnalyzer {
|
|
|
939
999
|
}
|
|
940
1000
|
|
|
941
1001
|
/**
|
|
942
|
-
*
|
|
943
|
-
*/
|
|
944
|
-
_calcAux() {
|
|
945
|
-
const canvas = this.canvas,
|
|
946
|
-
isRadial = this._radial,
|
|
947
|
-
isDual = this._chLayout == CHANNEL_VERTICAL && ! isRadial,
|
|
948
|
-
centerX = canvas.width >> 1;
|
|
949
|
-
|
|
950
|
-
this._radius = Math.min( canvas.width, canvas.height ) * ( this._chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
|
|
951
|
-
this._barSpacePx = Math.min( this._barWidth - 1, ( this._barSpace > 0 && this._barSpace < 1 ) ? this._barWidth * this._barSpace : this._barSpace );
|
|
952
|
-
this._isBandsMode = this._mode % 10 != 0;
|
|
953
|
-
this._isOctaveBands = this._isBandsMode && this._frequencyScale == SCALE_LOG;
|
|
954
|
-
this._isLedDisplay = this._showLeds && this._isBandsMode && ! isRadial;
|
|
955
|
-
this._isLumiBars = this._lumiBars && this._isBandsMode && ! isRadial;
|
|
956
|
-
this._isAlphaBars = this._alphaBars && ! this._isLumiBars && this._mode != 10;
|
|
957
|
-
this._isOutline = this._outlineBars && this._isBandsMode && ! this._isLumiBars && ! this._isLedDisplay;
|
|
958
|
-
this._maximizeLeds = this._chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! this._isLumiBars;
|
|
959
|
-
|
|
960
|
-
this._channelHeight = canvas.height - ( isDual && ! this._isLedDisplay ? .5 : 0 ) >> isDual;
|
|
961
|
-
this._analyzerHeight = this._channelHeight * ( this._isLumiBars || isRadial ? 1 : 1 - this._reflexRatio ) | 0;
|
|
962
|
-
|
|
963
|
-
// channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
|
|
964
|
-
// TODO: improve this, make it configurable?
|
|
965
|
-
this._channelGap = isDual ? canvas.height - this._channelHeight * 2 : 0;
|
|
966
|
-
|
|
967
|
-
this._analyzerWidth = canvas.width - centerX * ( this._mirror != 0 );
|
|
968
|
-
this._initialX = centerX * ( this._mirror == -1 && ! isRadial );
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Calculate the X-coordinate on canvas for each analyzer bar
|
|
1002
|
+
* Compute all internal data required for the analyzer, based on its current settings
|
|
973
1003
|
*/
|
|
974
1004
|
_calcBars() {
|
|
975
1005
|
const bars = this._bars = []; // initialize object property
|
|
@@ -977,9 +1007,63 @@ export default class AudioMotionAnalyzer {
|
|
|
977
1007
|
if ( ! this._ready )
|
|
978
1008
|
return;
|
|
979
1009
|
|
|
1010
|
+
const barSpace = this._barSpace,
|
|
1011
|
+
canvas = this.canvas,
|
|
1012
|
+
centerX = canvas.width >> 1,
|
|
1013
|
+
chLayout = this._chLayout,
|
|
1014
|
+
isAnsiBands = this._ansiBands,
|
|
1015
|
+
isRadial = this._radial,
|
|
1016
|
+
isDual = chLayout == CHANNEL_VERTICAL && ! isRadial,
|
|
1017
|
+
maxFreq = this._maxFreq,
|
|
1018
|
+
minFreq = this._minFreq,
|
|
1019
|
+
mode = this._mode,
|
|
1020
|
+
|
|
1021
|
+
// COMPUTE FLAGS
|
|
1022
|
+
|
|
1023
|
+
isBands = mode % 10 != 0,
|
|
1024
|
+
isOctaves = isBands && this._frequencyScale == SCALE_LOG,
|
|
1025
|
+
isLeds = this._showLeds && isBands && ! isRadial,
|
|
1026
|
+
isLumi = this._lumiBars && isBands && ! isRadial,
|
|
1027
|
+
isAlpha = this._alphaBars && ! isLumi && mode != 10,
|
|
1028
|
+
isOutline = this._outlineBars && isBands && ! isLumi && ! isLeds,
|
|
1029
|
+
isRound = this._roundBars && isBands && ! isLumi && ! isLeds,
|
|
1030
|
+
noLedGap = chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! isLumi,
|
|
1031
|
+
|
|
1032
|
+
// COMPUTE AUXILIARY VALUES
|
|
1033
|
+
|
|
1034
|
+
// channelHeight is the total canvas height dedicated to each channel, including the reflex area, if any)
|
|
1035
|
+
channelHeight = canvas.height - ( isDual && ! isLeds ? .5 : 0 ) >> isDual,
|
|
1036
|
+
// analyzerHeight is the effective height used to render the analyzer, excluding the reflex area
|
|
1037
|
+
analyzerHeight = channelHeight * ( isLumi || isRadial ? 1 : 1 - this._reflexRatio ) | 0,
|
|
1038
|
+
|
|
1039
|
+
analyzerWidth = canvas.width - centerX * ( this._mirror != 0 ),
|
|
1040
|
+
|
|
1041
|
+
// channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
|
|
1042
|
+
// TODO: improve this, make it configurable?
|
|
1043
|
+
channelGap = isDual ? canvas.height - channelHeight * 2 : 0,
|
|
1044
|
+
|
|
1045
|
+
initialX = centerX * ( this._mirror == -1 && ! isRadial ),
|
|
1046
|
+
radius = Math.min( canvas.width, canvas.height ) * ( chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* CREATE ANALYZER BANDS
|
|
1050
|
+
*
|
|
1051
|
+
* USES:
|
|
1052
|
+
* analyzerWidth
|
|
1053
|
+
* initialX
|
|
1054
|
+
* isBands
|
|
1055
|
+
* isOctaves
|
|
1056
|
+
*
|
|
1057
|
+
* GENERATES:
|
|
1058
|
+
* bars (populates this._bars)
|
|
1059
|
+
* bardWidth
|
|
1060
|
+
* scaleMin
|
|
1061
|
+
* unitWidth
|
|
1062
|
+
*/
|
|
1063
|
+
|
|
980
1064
|
// helper function
|
|
981
1065
|
// bar object: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
|
|
982
|
-
const barsPush
|
|
1066
|
+
const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
|
|
983
1067
|
|
|
984
1068
|
/*
|
|
985
1069
|
A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
|
|
@@ -1012,15 +1096,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1012
1096
|
return [ bin, ratio ];
|
|
1013
1097
|
}
|
|
1014
1098
|
|
|
1015
|
-
|
|
1016
|
-
initialX = this._initialX,
|
|
1017
|
-
isAnsiBands = this._ansiBands,
|
|
1018
|
-
maxFreq = this._maxFreq,
|
|
1019
|
-
minFreq = this._minFreq;
|
|
1099
|
+
let barWidth, scaleMin, unitWidth;
|
|
1020
1100
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if ( this._isOctaveBands ) {
|
|
1101
|
+
if ( isOctaves ) {
|
|
1024
1102
|
// helper function to round a value to a given number of significant digits
|
|
1025
1103
|
// `atLeast` set to true prevents reducing the number of integer significant digits
|
|
1026
1104
|
const roundSD = ( value, digits, atLeast ) => +value.toPrecision( atLeast ? Math.max( digits, 1 + Math.log10( value ) | 0 ) : digits );
|
|
@@ -1075,9 +1153,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1075
1153
|
currFreq *= bandWidth;
|
|
1076
1154
|
} while ( currFreq <= maxFreq );
|
|
1077
1155
|
|
|
1078
|
-
|
|
1156
|
+
barWidth = analyzerWidth / bars.length;
|
|
1079
1157
|
|
|
1080
|
-
bars.forEach( ( bar, index ) => bar.posX = initialX + index *
|
|
1158
|
+
bars.forEach( ( bar, index ) => bar.posX = initialX + index * barWidth );
|
|
1081
1159
|
|
|
1082
1160
|
const firstBar = bars[0],
|
|
1083
1161
|
lastBar = bars[ bars.length - 1 ];
|
|
@@ -1097,7 +1175,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1097
1175
|
[ lastBar.binHi, lastBar.ratioHi ] = calcRatio( maxFreq );
|
|
1098
1176
|
}
|
|
1099
1177
|
}
|
|
1100
|
-
else if (
|
|
1178
|
+
else if ( isBands ) { // a bands mode is selected, but frequency scale is not logarithmic
|
|
1101
1179
|
|
|
1102
1180
|
const bands = [0,24,12,8,6,4,3,2,1][ this._mode ] * 10;
|
|
1103
1181
|
|
|
@@ -1112,15 +1190,15 @@ export default class AudioMotionAnalyzer {
|
|
|
1112
1190
|
}
|
|
1113
1191
|
}
|
|
1114
1192
|
|
|
1115
|
-
|
|
1193
|
+
barWidth = analyzerWidth / bands;
|
|
1116
1194
|
|
|
1117
1195
|
scaleMin = this._freqScaling( minFreq );
|
|
1118
1196
|
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
1119
1197
|
|
|
1120
|
-
for ( let i = 0, posX = 0; i < bands; i++, posX +=
|
|
1198
|
+
for ( let i = 0, posX = 0; i < bands; i++, posX += barWidth ) {
|
|
1121
1199
|
const freqLo = invFreqScaling( scaleMin + posX / unitWidth ),
|
|
1122
|
-
freq = invFreqScaling( scaleMin + ( posX +
|
|
1123
|
-
freqHi = invFreqScaling( scaleMin + ( posX +
|
|
1200
|
+
freq = invFreqScaling( scaleMin + ( posX + barWidth / 2 ) / unitWidth ),
|
|
1201
|
+
freqHi = invFreqScaling( scaleMin + ( posX + barWidth ) / unitWidth ),
|
|
1124
1202
|
[ binLo, ratioLo ] = calcRatio( freqLo ),
|
|
1125
1203
|
[ binHi, ratioHi ] = calcRatio( freqHi );
|
|
1126
1204
|
|
|
@@ -1129,7 +1207,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1129
1207
|
|
|
1130
1208
|
}
|
|
1131
1209
|
else { // Discrete frequencies modes
|
|
1132
|
-
|
|
1210
|
+
barWidth = 1;
|
|
1133
1211
|
|
|
1134
1212
|
scaleMin = this._freqScaling( minFreq );
|
|
1135
1213
|
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
@@ -1157,78 +1235,130 @@ export default class AudioMotionAnalyzer {
|
|
|
1157
1235
|
}
|
|
1158
1236
|
}
|
|
1159
1237
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1238
|
+
/**
|
|
1239
|
+
* COMPUTE ATTRIBUTES FOR THE LED BARS
|
|
1240
|
+
*
|
|
1241
|
+
* USES:
|
|
1242
|
+
* analyzerHeight
|
|
1243
|
+
* barWidth
|
|
1244
|
+
* noLedGap
|
|
1245
|
+
*
|
|
1246
|
+
* GENERATES:
|
|
1247
|
+
* spaceH
|
|
1248
|
+
* spaceV
|
|
1249
|
+
* this._leds
|
|
1250
|
+
*/
|
|
1251
|
+
|
|
1252
|
+
let spaceH = 0,
|
|
1253
|
+
spaceV = 0;
|
|
1254
|
+
|
|
1255
|
+
if ( isLeds ) {
|
|
1256
|
+
// adjustment for high pixel-ratio values on low-resolution screens (Android TV)
|
|
1257
|
+
const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
|
|
1258
|
+
|
|
1259
|
+
const params = [ [],
|
|
1260
|
+
[ 128, 3, .45 ], // mode 1
|
|
1261
|
+
[ 128, 4, .225 ], // mode 2
|
|
1262
|
+
[ 96, 6, .225 ], // mode 3
|
|
1263
|
+
[ 80, 6, .225 ], // mode 4
|
|
1264
|
+
[ 80, 6, .125 ], // mode 5
|
|
1265
|
+
[ 64, 6, .125 ], // mode 6
|
|
1266
|
+
[ 48, 8, .125 ], // mode 7
|
|
1267
|
+
[ 24, 16, .125 ], // mode 8
|
|
1268
|
+
];
|
|
1269
|
+
|
|
1270
|
+
// use custom LED parameters if set, or the default parameters for the current mode
|
|
1271
|
+
const customParams = this._ledParams,
|
|
1272
|
+
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ mode ];
|
|
1273
|
+
|
|
1274
|
+
let ledCount, maxHeight = analyzerHeight;
|
|
1275
|
+
|
|
1276
|
+
if ( customParams ) {
|
|
1277
|
+
const minHeight = 2 * dPR;
|
|
1278
|
+
let blockHeight;
|
|
1279
|
+
ledCount = maxLeds + 1;
|
|
1280
|
+
do {
|
|
1281
|
+
ledCount--;
|
|
1282
|
+
blockHeight = maxHeight / ledCount / ( 1 + spaceVRatio );
|
|
1283
|
+
spaceV = blockHeight * spaceVRatio;
|
|
1284
|
+
} while ( ( blockHeight < minHeight || spaceV < minHeight ) && ledCount > 1 );
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
// calculate vertical spacing - aim for the reference ratio, but make sure it's at least 2px
|
|
1288
|
+
const refRatio = 540 / spaceVRatio;
|
|
1289
|
+
spaceV = Math.min( spaceVRatio * dPR, Math.max( 2, maxHeight / refRatio + .1 | 0 ) );
|
|
1290
|
+
}
|
|
1163
1291
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1292
|
+
// remove the extra spacing below the last line of LEDs
|
|
1293
|
+
if ( noLedGap )
|
|
1294
|
+
maxHeight += spaceV;
|
|
1166
1295
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
// update LED properties
|
|
1171
|
-
this._calcLeds();
|
|
1172
|
-
}
|
|
1296
|
+
// recalculate the number of leds, considering the effective spaceV
|
|
1297
|
+
if ( ! customParams )
|
|
1298
|
+
ledCount = Math.min( maxLeds, maxHeight / ( spaceV * 2 ) | 0 );
|
|
1173
1299
|
|
|
1174
|
-
|
|
1175
|
-
* Calculate attributes for the vintage LEDs effect, based on visualization mode and canvas resolution
|
|
1176
|
-
*/
|
|
1177
|
-
_calcLeds() {
|
|
1178
|
-
if ( ! this._isBandsMode || ! this._ready )
|
|
1179
|
-
return;
|
|
1300
|
+
spaceH = spaceHRatio >= 1 ? spaceHRatio : barWidth * spaceHRatio;
|
|
1180
1301
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
[ 96, 6, .225 ], // mode 3
|
|
1188
|
-
[ 80, 6, .225 ], // mode 4
|
|
1189
|
-
[ 80, 6, .125 ], // mode 5
|
|
1190
|
-
[ 64, 6, .125 ], // mode 6
|
|
1191
|
-
[ 48, 8, .125 ], // mode 7
|
|
1192
|
-
[ 24, 16, .125 ], // mode 8
|
|
1193
|
-
];
|
|
1194
|
-
|
|
1195
|
-
// use custom LED parameters if set, or the default parameters for the current mode
|
|
1196
|
-
const customParams = this._ledParams,
|
|
1197
|
-
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ this._mode ];
|
|
1198
|
-
|
|
1199
|
-
let ledCount, spaceV,
|
|
1200
|
-
analyzerHeight = this._analyzerHeight;
|
|
1201
|
-
|
|
1202
|
-
if ( customParams ) {
|
|
1203
|
-
const minHeight = 2 * dPR;
|
|
1204
|
-
let blockHeight;
|
|
1205
|
-
ledCount = maxLeds + 1;
|
|
1206
|
-
do {
|
|
1207
|
-
ledCount--;
|
|
1208
|
-
blockHeight = analyzerHeight / ledCount / ( 1 + spaceVRatio );
|
|
1209
|
-
spaceV = blockHeight * spaceVRatio;
|
|
1210
|
-
} while ( ( blockHeight < minHeight || spaceV < minHeight ) && ledCount > 1 );
|
|
1302
|
+
this._leds = [
|
|
1303
|
+
ledCount,
|
|
1304
|
+
spaceH,
|
|
1305
|
+
spaceV,
|
|
1306
|
+
maxHeight / ledCount - spaceV // ledHeight
|
|
1307
|
+
];
|
|
1211
1308
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1309
|
+
|
|
1310
|
+
// COMPUTE ADDITIONAL BAR POSITIONING, ACCORDING TO THE CURRENT SETTINGS
|
|
1311
|
+
// uses: barSpace, barWidth, spaceH
|
|
1312
|
+
|
|
1313
|
+
const barSpacePx = Math.min( barWidth - 1, barSpace * ( barSpace > 0 && barSpace < 1 ? barWidth : 1 ) );
|
|
1314
|
+
|
|
1315
|
+
if ( isBands )
|
|
1316
|
+
barWidth -= Math.max( isLeds ? spaceH : 0, barSpacePx );
|
|
1317
|
+
|
|
1318
|
+
bars.forEach( ( bar, index ) => {
|
|
1319
|
+
let posX = bar.posX,
|
|
1320
|
+
width = barWidth;
|
|
1321
|
+
|
|
1322
|
+
// in bands modes we need to update bar.posX to account for bar/led spacing
|
|
1323
|
+
|
|
1324
|
+
if ( isBands ) {
|
|
1325
|
+
if ( barSpace == 0 && ! isLeds ) {
|
|
1326
|
+
// when barSpace == 0 use integer values for perfect gapless positioning
|
|
1327
|
+
posX |= 0;
|
|
1328
|
+
width |= 0;
|
|
1329
|
+
if ( index > 0 && posX > bars[ index - 1 ].posX + bars[ index - 1 ].width ) {
|
|
1330
|
+
posX--;
|
|
1331
|
+
width++;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
else
|
|
1335
|
+
posX += Math.max( ( isLeds ? spaceH : 0 ), barSpacePx ) / 2;
|
|
1336
|
+
|
|
1337
|
+
bar.posX = posX; // update
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
bar.barCenter = posX + ( barWidth == 1 ? 0 : width / 2 );
|
|
1341
|
+
bar.width = width;
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
// COMPUTE CHANNEL COORDINATES (uses spaceV)
|
|
1345
|
+
|
|
1346
|
+
const channelCoords = [];
|
|
1347
|
+
for ( const channel of [0,1] ) {
|
|
1348
|
+
const channelTop = chLayout == CHANNEL_VERTICAL ? ( channelHeight + channelGap ) * channel : 0,
|
|
1349
|
+
channelBottom = channelTop + channelHeight,
|
|
1350
|
+
analyzerBottom = channelTop + analyzerHeight - ( ! isLeds || noLedGap ? 0 : spaceV );
|
|
1351
|
+
|
|
1352
|
+
channelCoords.push( { channelTop, channelBottom, analyzerBottom } );
|
|
1216
1353
|
}
|
|
1217
1354
|
|
|
1218
|
-
//
|
|
1219
|
-
if ( this._maximizeLeds )
|
|
1220
|
-
analyzerHeight += spaceV;
|
|
1355
|
+
// SAVE INTERNAL PROPERTIES
|
|
1221
1356
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
ledCount = Math.min( maxLeds, analyzerHeight / ( spaceV * 2 ) | 0 );
|
|
1357
|
+
this._aux = { analyzerHeight, analyzerWidth, channelCoords, channelHeight, channelGap, initialX, radius, scaleMin, unitWidth };
|
|
1358
|
+
this._flg = { isAlpha, isBands, isLeds, isLumi, isOctaves, isOutline, isRound, noLedGap };
|
|
1225
1359
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
spaceHRatio >= 1 ? spaceHRatio : this._barWidth * spaceHRatio, // spaceH
|
|
1229
|
-
spaceV,
|
|
1230
|
-
analyzerHeight / ledCount - spaceV // ledHeight
|
|
1231
|
-
];
|
|
1360
|
+
// generate the X-axis and radial scales
|
|
1361
|
+
this._createScales();
|
|
1232
1362
|
}
|
|
1233
1363
|
|
|
1234
1364
|
/**
|
|
@@ -1238,7 +1368,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1238
1368
|
if ( ! this._ready )
|
|
1239
1369
|
return;
|
|
1240
1370
|
|
|
1241
|
-
const analyzerWidth = this.
|
|
1371
|
+
const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
|
|
1242
1372
|
canvas = this._canvasCtx.canvas,
|
|
1243
1373
|
scaleX = this._scaleX,
|
|
1244
1374
|
scaleR = this._scaleR,
|
|
@@ -1246,20 +1376,22 @@ export default class AudioMotionAnalyzer {
|
|
|
1246
1376
|
canvasR = scaleR.canvas,
|
|
1247
1377
|
freqLabels = [],
|
|
1248
1378
|
frequencyScale= this._frequencyScale,
|
|
1249
|
-
initialX = this._initialX,
|
|
1250
|
-
isDual = this._chLayout == CHANNEL_VERTICAL,
|
|
1251
|
-
isMirror = this._mirror,
|
|
1252
1379
|
isNoteLabels = this._noteLabels,
|
|
1380
|
+
isRadial = this._radial,
|
|
1381
|
+
isVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
1382
|
+
mirror = this._mirror,
|
|
1253
1383
|
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1254
|
-
scaleHeight = Math.min( canvas.width, canvas.height )
|
|
1384
|
+
scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
|
|
1255
1385
|
fontSizeX = canvasX.height >> 1,
|
|
1256
1386
|
fontSizeR = scaleHeight >> 1,
|
|
1387
|
+
labelWidthX = fontSizeX * ( isNoteLabels ? .7 : 1.5 ),
|
|
1388
|
+
labelWidthR = fontSizeR * ( isNoteLabels ? 1 : 2 ),
|
|
1257
1389
|
root12 = 2 ** ( 1 / 12 );
|
|
1258
1390
|
|
|
1259
1391
|
if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
|
|
1260
1392
|
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
1261
1393
|
if ( frequencyScale == SCALE_LINEAR )
|
|
1262
|
-
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3 );
|
|
1394
|
+
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
|
|
1263
1395
|
else
|
|
1264
1396
|
freqLabels.push( 8e3, 16e3 );
|
|
1265
1397
|
}
|
|
@@ -1270,7 +1402,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1270
1402
|
if ( freq >= this._minFreq && freq <= this._maxFreq ) {
|
|
1271
1403
|
const pitch = scale[ note ],
|
|
1272
1404
|
isC = pitch == 'C';
|
|
1273
|
-
if ( ( pitch && isNoteLabels && !
|
|
1405
|
+
if ( ( pitch && isNoteLabels && ! mirror ) || isC )
|
|
1274
1406
|
freqLabels.push( isNoteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
|
|
1275
1407
|
}
|
|
1276
1408
|
freq *= root12;
|
|
@@ -1278,24 +1410,21 @@ export default class AudioMotionAnalyzer {
|
|
|
1278
1410
|
}
|
|
1279
1411
|
}
|
|
1280
1412
|
|
|
1281
|
-
// in radial
|
|
1282
|
-
canvasR.width = canvasR.height = (
|
|
1413
|
+
// in radial dual-vertical layout, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
|
|
1414
|
+
canvasR.width = canvasR.height = ( radius << 1 ) + ( isVertical * scaleHeight );
|
|
1283
1415
|
|
|
1284
|
-
const
|
|
1285
|
-
radialY =
|
|
1416
|
+
const centerR = canvasR.width >> 1,
|
|
1417
|
+
radialY = centerR - scaleHeight * .7; // vertical position of text labels in the circular scale
|
|
1286
1418
|
|
|
1287
1419
|
// helper function
|
|
1288
1420
|
const radialLabel = ( x, label ) => {
|
|
1289
|
-
if ( isNoteLabels && ! isDual && ! ['C','E','G'].includes( label[0] ) )
|
|
1290
|
-
return;
|
|
1291
|
-
|
|
1292
1421
|
const angle = TAU * ( x / canvas.width ),
|
|
1293
1422
|
adjAng = angle - HALF_PI, // rotate angles so 0 is at the top
|
|
1294
1423
|
posX = radialY * Math.cos( adjAng ),
|
|
1295
1424
|
posY = radialY * Math.sin( adjAng );
|
|
1296
1425
|
|
|
1297
1426
|
scaleR.save();
|
|
1298
|
-
scaleR.translate(
|
|
1427
|
+
scaleR.translate( centerR + posX, centerR + posY );
|
|
1299
1428
|
scaleR.rotate( angle );
|
|
1300
1429
|
scaleR.fillText( label, 0, 0 );
|
|
1301
1430
|
scaleR.restore();
|
|
@@ -1307,7 +1436,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1307
1436
|
scaleX.fillStyle = scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
|
|
1308
1437
|
scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1309
1438
|
|
|
1310
|
-
scaleR.arc(
|
|
1439
|
+
scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
|
|
1311
1440
|
scaleR.lineWidth = scaleHeight;
|
|
1312
1441
|
scaleR.stroke();
|
|
1313
1442
|
|
|
@@ -1316,32 +1445,49 @@ export default class AudioMotionAnalyzer {
|
|
|
1316
1445
|
scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
|
|
1317
1446
|
scaleX.textAlign = scaleR.textAlign = 'center';
|
|
1318
1447
|
|
|
1319
|
-
let prevX =
|
|
1448
|
+
let prevX = -labelWidthX / 4,
|
|
1449
|
+
prevR = -labelWidthR;
|
|
1320
1450
|
|
|
1321
1451
|
for ( const item of freqLabels ) {
|
|
1322
1452
|
const [ freq, label ] = Array.isArray( item ) ? item : [ item, item < 1e3 ? item | 0 : `${ ( item / 100 | 0 ) / 10 }k` ],
|
|
1323
|
-
x =
|
|
1453
|
+
x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
|
|
1324
1454
|
y = canvasX.height * .75,
|
|
1325
1455
|
isC = label[0] == 'C',
|
|
1326
|
-
maxW = fontSizeX * ( isNoteLabels && !
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1456
|
+
maxW = fontSizeX * ( isNoteLabels && ! mirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1457
|
+
|
|
1458
|
+
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1459
|
+
scaleX.fillStyle = scaleR.fillStyle = isC && ! mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1460
|
+
|
|
1461
|
+
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1462
|
+
if ( isNoteLabels ) {
|
|
1463
|
+
let allowedLabels = ['C'];
|
|
1464
|
+
if ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 250 ) ||
|
|
1465
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 125 || freq > 1e3 ) ) )
|
|
1466
|
+
allowedLabels.push('G');
|
|
1467
|
+
if ( frequencyScale == SCALE_LOG || freq > 4e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ||
|
|
1468
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 250 || freq > 2e3 ) ) )
|
|
1469
|
+
allowedLabels.push('E');
|
|
1470
|
+
if ( frequencyScale == SCALE_LINEAR && freq > 4e3 ||
|
|
1471
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ) ) )
|
|
1472
|
+
allowedLabels.push('D','F','A','B');
|
|
1473
|
+
if ( ! allowedLabels.includes( label[0] ) )
|
|
1474
|
+
continue; // skip this label
|
|
1475
|
+
}
|
|
1331
1476
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1477
|
+
// linear scale
|
|
1478
|
+
if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
|
|
1479
|
+
scaleX.fillText( label, initialX + x, y, maxW );
|
|
1480
|
+
if ( mirror && ( x > labelWidthX || mirror == 1 ) )
|
|
1481
|
+
scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
|
|
1482
|
+
prevX = x + Math.min( maxW, scaleX.measureText( label ).width ) / 2;
|
|
1483
|
+
}
|
|
1338
1484
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1485
|
+
// radial scale
|
|
1486
|
+
if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
|
|
1487
|
+
radialLabel( x, label );
|
|
1488
|
+
if ( mirror && ( x > labelWidthR || mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
|
|
1489
|
+
radialLabel( -x, label );
|
|
1490
|
+
prevR = x;
|
|
1345
1491
|
}
|
|
1346
1492
|
}
|
|
1347
1493
|
}
|
|
@@ -1351,48 +1497,132 @@ export default class AudioMotionAnalyzer {
|
|
|
1351
1497
|
* this is called 60 times per second by requestAnimationFrame()
|
|
1352
1498
|
*/
|
|
1353
1499
|
_draw( timestamp ) {
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1500
|
+
const { isAlpha, isBands, isLeds, isLumi,
|
|
1501
|
+
isOctaves, isOutline, isRound, noLedGap } = this._flg,
|
|
1356
1502
|
ctx = this._canvasCtx,
|
|
1357
1503
|
canvas = ctx.canvas,
|
|
1358
1504
|
canvasX = this._scaleX.canvas,
|
|
1359
1505
|
canvasR = this._scaleR.canvas,
|
|
1360
1506
|
canvasGradients= this._canvasGradients,
|
|
1507
|
+
centerX = canvas.width >> 1,
|
|
1508
|
+
centerY = canvas.height >> 1,
|
|
1509
|
+
colorMode = this._colorMode,
|
|
1361
1510
|
energy = this._energy,
|
|
1362
1511
|
fillAlpha = this.fillAlpha,
|
|
1363
1512
|
mode = this._mode,
|
|
1364
|
-
isAlphaBars = this._isAlphaBars,
|
|
1365
|
-
isLedDisplay = this._isLedDisplay,
|
|
1366
1513
|
isLinear = this._linearAmplitude,
|
|
1367
|
-
isLumiBars = this._isLumiBars,
|
|
1368
|
-
isBandsMode = this._isBandsMode,
|
|
1369
|
-
isOutline = this._isOutline,
|
|
1370
1514
|
isOverlay = this.overlay,
|
|
1371
1515
|
isRadial = this._radial,
|
|
1516
|
+
isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
|
|
1372
1517
|
channelLayout = this._chLayout,
|
|
1373
|
-
lineWidth =
|
|
1518
|
+
lineWidth = this._lineWidth,
|
|
1374
1519
|
mirrorMode = this._mirror,
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
analyzerWidth = isRadial ? canvas.width : this._analyzerWidth,
|
|
1379
|
-
initialX = this._initialX,
|
|
1520
|
+
{ analyzerHeight, channelCoords,
|
|
1521
|
+
channelHeight, channelGap, initialX, radius } = this._aux,
|
|
1522
|
+
analyzerWidth = isRadial ? canvas.width : this._aux.analyzerWidth,
|
|
1380
1523
|
finalX = initialX + analyzerWidth,
|
|
1381
|
-
centerX = canvas.width >> 1,
|
|
1382
|
-
centerY = canvas.height >> 1,
|
|
1383
|
-
radius = this._radius,
|
|
1384
1524
|
showBgColor = this.showBgColor,
|
|
1385
1525
|
maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
|
|
1386
1526
|
maxdB = this.maxDecibels,
|
|
1387
1527
|
mindB = this.minDecibels,
|
|
1388
1528
|
dbRange = maxdB - mindB,
|
|
1389
1529
|
useCanvas = this.useCanvas,
|
|
1390
|
-
weightingFilter= this._weightingFilter
|
|
1530
|
+
weightingFilter= this._weightingFilter,
|
|
1531
|
+
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1391
1532
|
|
|
1392
1533
|
if ( energy.val > 0 )
|
|
1393
1534
|
this._spinAngle += this._spinSpeed * RPM;
|
|
1394
1535
|
|
|
1395
|
-
|
|
1536
|
+
/* HELPER FUNCTIONS */
|
|
1537
|
+
|
|
1538
|
+
// create Reflex effect
|
|
1539
|
+
const doReflex = channel => {
|
|
1540
|
+
if ( this._reflexRatio > 0 && ! isLumi ) {
|
|
1541
|
+
let posY, height;
|
|
1542
|
+
if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
|
|
1543
|
+
posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
|
|
1544
|
+
height = channelHeight - analyzerHeight;
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
posY = canvas.height - analyzerHeight * 2;
|
|
1548
|
+
height = analyzerHeight;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
ctx.save();
|
|
1552
|
+
|
|
1553
|
+
// set alpha and brightness for the reflection
|
|
1554
|
+
ctx.globalAlpha = this.reflexAlpha;
|
|
1555
|
+
if ( this.reflexBright != 1 )
|
|
1556
|
+
ctx.filter = `brightness(${this.reflexBright})`;
|
|
1557
|
+
|
|
1558
|
+
// create the reflection
|
|
1559
|
+
ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
|
|
1560
|
+
ctx.drawImage( canvas, 0, channelCoords[ channel ].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
|
|
1561
|
+
|
|
1562
|
+
ctx.restore();
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// draw scale on X-axis
|
|
1567
|
+
const drawScaleX = () => {
|
|
1568
|
+
if ( this.showScaleX ) {
|
|
1569
|
+
if ( isRadial ) {
|
|
1570
|
+
ctx.save();
|
|
1571
|
+
ctx.translate( centerX, centerY );
|
|
1572
|
+
if ( this._spinSpeed )
|
|
1573
|
+
ctx.rotate( this._spinAngle + HALF_PI );
|
|
1574
|
+
ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
|
|
1575
|
+
ctx.restore();
|
|
1576
|
+
}
|
|
1577
|
+
else
|
|
1578
|
+
ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// draw scale on Y-axis
|
|
1583
|
+
const drawScaleY = channelTop => {
|
|
1584
|
+
const scaleWidth = canvasX.height,
|
|
1585
|
+
fontSize = scaleWidth >> 1,
|
|
1586
|
+
max = isLinear ? 100 : maxdB,
|
|
1587
|
+
min = isLinear ? 0 : mindB,
|
|
1588
|
+
incr = isLinear ? 20 : 5,
|
|
1589
|
+
interval = analyzerHeight / ( max - min );
|
|
1590
|
+
|
|
1591
|
+
ctx.save();
|
|
1592
|
+
ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1593
|
+
ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1594
|
+
ctx.textAlign = 'right';
|
|
1595
|
+
ctx.lineWidth = 1;
|
|
1596
|
+
|
|
1597
|
+
for ( let val = max; val > min; val -= incr ) {
|
|
1598
|
+
const posY = channelTop + ( max - val ) * interval,
|
|
1599
|
+
even = ( val % 2 == 0 ) | 0;
|
|
1600
|
+
|
|
1601
|
+
if ( even ) {
|
|
1602
|
+
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1603
|
+
if ( mirrorMode != -1 )
|
|
1604
|
+
ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1605
|
+
if ( mirrorMode != 1 )
|
|
1606
|
+
ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
|
|
1607
|
+
ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1608
|
+
ctx.setLineDash([2,4]);
|
|
1609
|
+
ctx.lineDashOffset = 0;
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1613
|
+
ctx.setLineDash([2,8]);
|
|
1614
|
+
ctx.lineDashOffset = 1;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
ctx.beginPath();
|
|
1618
|
+
ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1619
|
+
ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
|
|
1620
|
+
ctx.stroke();
|
|
1621
|
+
}
|
|
1622
|
+
ctx.restore();
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
1396
1626
|
const weightingdB = freq => {
|
|
1397
1627
|
const f2 = freq ** 2,
|
|
1398
1628
|
SQ20_6 = 424.36,
|
|
@@ -1430,6 +1660,15 @@ export default class AudioMotionAnalyzer {
|
|
|
1430
1660
|
return 0; // unknown filter
|
|
1431
1661
|
}
|
|
1432
1662
|
|
|
1663
|
+
// draws (stroke) a bar from x,y1 to x,y2
|
|
1664
|
+
const strokeBar = ( x, y1, y2 ) => {
|
|
1665
|
+
ctx.beginPath();
|
|
1666
|
+
ctx.moveTo( x, y1 );
|
|
1667
|
+
ctx.lineTo( x, y2 );
|
|
1668
|
+
ctx.stroke();
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// conditionally strokes current path on canvas
|
|
1433
1672
|
const strokeIf = flag => {
|
|
1434
1673
|
if ( flag && lineWidth ) {
|
|
1435
1674
|
const alpha = ctx.globalAlpha;
|
|
@@ -1439,52 +1678,107 @@ export default class AudioMotionAnalyzer {
|
|
|
1439
1678
|
}
|
|
1440
1679
|
}
|
|
1441
1680
|
|
|
1442
|
-
//
|
|
1681
|
+
// converts a given X-coordinate to its corresponding angle in radial mode
|
|
1682
|
+
const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
|
|
1683
|
+
|
|
1684
|
+
// converts planar X,Y coordinates to radial coordinates
|
|
1443
1685
|
const radialXY = ( x, y, dir ) => {
|
|
1444
1686
|
const height = radius + y,
|
|
1445
|
-
angle =
|
|
1446
|
-
|
|
1687
|
+
angle = getAngle( x, dir );
|
|
1447
1688
|
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
1448
1689
|
}
|
|
1449
1690
|
|
|
1450
|
-
//
|
|
1691
|
+
// draws a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
1451
1692
|
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1452
1693
|
ctx.beginPath();
|
|
1453
1694
|
for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
|
|
1695
|
+
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1454
1696
|
ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1455
1697
|
ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1456
|
-
|
|
1698
|
+
if ( isRound )
|
|
1699
|
+
ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
|
|
1700
|
+
else
|
|
1701
|
+
ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1457
1702
|
ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1703
|
+
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1704
|
+
ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
|
|
1458
1705
|
}
|
|
1459
|
-
|
|
1460
1706
|
strokeIf( stroke );
|
|
1461
1707
|
ctx.fill();
|
|
1462
1708
|
}
|
|
1463
1709
|
|
|
1464
|
-
//
|
|
1465
|
-
const
|
|
1466
|
-
const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1710
|
+
// converts a value in [0;1] range to a height in pixels that fits into the current LED elements
|
|
1711
|
+
const ledPosY = value => Math.max( 0, ( value * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1467
1712
|
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1713
|
+
// update energy information
|
|
1714
|
+
const updateEnergy = newVal => {
|
|
1715
|
+
energy.val = newVal;
|
|
1716
|
+
if ( newVal >= energy.peak ) {
|
|
1717
|
+
energy.peak = newVal;
|
|
1718
|
+
energy.hold = 30;
|
|
1719
|
+
}
|
|
1720
|
+
else {
|
|
1721
|
+
if ( energy.hold > 0 )
|
|
1722
|
+
energy.hold--;
|
|
1723
|
+
else if ( energy.peak > 0 )
|
|
1724
|
+
energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1471
1727
|
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1728
|
+
// calculate and display (if enabled) the current frame rate
|
|
1729
|
+
const updateFPS = () => {
|
|
1730
|
+
this._frame++;
|
|
1731
|
+
const elapsed = timestamp - this._time;
|
|
1732
|
+
|
|
1733
|
+
if ( elapsed >= 1000 ) {
|
|
1734
|
+
this._fps = this._frame / ( elapsed / 1000 );
|
|
1735
|
+
this._frame = 0;
|
|
1736
|
+
this._time = timestamp;
|
|
1737
|
+
}
|
|
1738
|
+
if ( this.showFPS ) {
|
|
1739
|
+
const size = canvasX.height;
|
|
1740
|
+
ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
1741
|
+
ctx.fillStyle = FPS_COLOR;
|
|
1742
|
+
ctx.textAlign = 'right';
|
|
1743
|
+
ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
/* MAIN FUNCTION */
|
|
1475
1748
|
|
|
1476
1749
|
let currentEnergy = 0;
|
|
1477
1750
|
|
|
1478
|
-
const
|
|
1751
|
+
const bars = this._bars,
|
|
1752
|
+
nBars = bars.length,
|
|
1479
1753
|
nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
|
|
1480
1754
|
|
|
1481
1755
|
for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1482
1756
|
|
|
1483
|
-
const channelTop
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1757
|
+
const { channelTop, channelBottom, analyzerBottom } = channelCoords[ channel ],
|
|
1758
|
+
channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
1759
|
+
colorStops = channelGradient.colorStops,
|
|
1760
|
+
colorCount = colorStops.length,
|
|
1761
|
+
bgColor = ( ! showBgColor || isLeds && ! isOverlay ) ? '#000' : channelGradient.bgColor,
|
|
1762
|
+
mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
|
|
1763
|
+
|
|
1764
|
+
// helper function for FFT data interpolation (uses fftData)
|
|
1765
|
+
const interpolate = ( bin, ratio ) => {
|
|
1766
|
+
const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
|
|
1767
|
+
return isNaN( value ) ? -Infinity : value;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// set fillStyle and strokeStyle according to current colorMode (uses: channel, colorStops, colorCount)
|
|
1771
|
+
const setBarColor = ( value = 0, barIndex = 0 ) => {
|
|
1772
|
+
let color;
|
|
1773
|
+
// for mode 10, always use the channel gradient (ignore colorMode)
|
|
1774
|
+
if ( ( colorMode == COLOR_GRADIENT && ! isTrueLeds ) || mode == 10 )
|
|
1775
|
+
color = canvasGradients[ channel ];
|
|
1776
|
+
else {
|
|
1777
|
+
const selectedIndex = colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
|
|
1778
|
+
color = colorStops[ selectedIndex ].color;
|
|
1779
|
+
}
|
|
1780
|
+
ctx.fillStyle = ctx.strokeStyle = color;
|
|
1781
|
+
}
|
|
1488
1782
|
|
|
1489
1783
|
if ( useCanvas ) {
|
|
1490
1784
|
// clear the channel area, if in overlay mode
|
|
@@ -1506,60 +1800,25 @@ export default class AudioMotionAnalyzer {
|
|
|
1506
1800
|
ctx.globalAlpha = 1;
|
|
1507
1801
|
}
|
|
1508
1802
|
|
|
1509
|
-
// draw dB scale (Y-axis)
|
|
1510
|
-
if ( this.showScaleY && !
|
|
1511
|
-
|
|
1512
|
-
fontSize = scaleWidth >> 1,
|
|
1513
|
-
max = isLinear ? 100 : maxdB,
|
|
1514
|
-
min = isLinear ? 0 : mindB,
|
|
1515
|
-
incr = isLinear ? 20 : 5,
|
|
1516
|
-
interval = analyzerHeight / ( max - min );
|
|
1517
|
-
|
|
1518
|
-
ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1519
|
-
ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1520
|
-
ctx.textAlign = 'right';
|
|
1521
|
-
ctx.lineWidth = 1;
|
|
1522
|
-
|
|
1523
|
-
for ( let val = max; val > min; val -= incr ) {
|
|
1524
|
-
const posY = channelTop + ( max - val ) * interval,
|
|
1525
|
-
even = ( val % 2 == 0 ) | 0;
|
|
1526
|
-
|
|
1527
|
-
if ( even ) {
|
|
1528
|
-
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1529
|
-
if ( mirrorMode != -1 )
|
|
1530
|
-
ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1531
|
-
if ( mirrorMode != 1 )
|
|
1532
|
-
ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
|
|
1533
|
-
ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1534
|
-
ctx.setLineDash([2,4]);
|
|
1535
|
-
ctx.lineDashOffset = 0;
|
|
1536
|
-
}
|
|
1537
|
-
else {
|
|
1538
|
-
ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1539
|
-
ctx.setLineDash([2,8]);
|
|
1540
|
-
ctx.lineDashOffset = 1;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
ctx.beginPath();
|
|
1544
|
-
ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1545
|
-
ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
|
|
1546
|
-
ctx.stroke();
|
|
1547
|
-
}
|
|
1548
|
-
// restore line properties
|
|
1549
|
-
ctx.setLineDash([]);
|
|
1550
|
-
ctx.lineDashOffset = 0;
|
|
1551
|
-
}
|
|
1803
|
+
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
1804
|
+
if ( this.showScaleY && ! isLumi && ! isRadial && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) )
|
|
1805
|
+
drawScaleY( channelTop );
|
|
1552
1806
|
|
|
1553
1807
|
// set line width and dash for LEDs effect
|
|
1554
|
-
if (
|
|
1808
|
+
if ( isLeds ) {
|
|
1555
1809
|
ctx.setLineDash( [ ledHeight, ledSpaceV ] );
|
|
1556
|
-
ctx.lineWidth = width;
|
|
1810
|
+
ctx.lineWidth = bars[0].width;
|
|
1557
1811
|
}
|
|
1558
1812
|
else // for outline effect ensure linewidth is not greater than half the bar width
|
|
1559
|
-
ctx.lineWidth = isOutline ? Math.min( lineWidth, width / 2 ) : lineWidth;
|
|
1560
|
-
|
|
1561
|
-
// set
|
|
1562
|
-
ctx.
|
|
1813
|
+
ctx.lineWidth = isOutline ? Math.min( lineWidth, bars[0].width / 2 ) : lineWidth;
|
|
1814
|
+
|
|
1815
|
+
// set clip region
|
|
1816
|
+
ctx.save();
|
|
1817
|
+
if ( ! isRadial ) {
|
|
1818
|
+
const channelRegion = new Path2D();
|
|
1819
|
+
channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
|
|
1820
|
+
ctx.clip( channelRegion );
|
|
1821
|
+
}
|
|
1563
1822
|
} // if ( useCanvas )
|
|
1564
1823
|
|
|
1565
1824
|
// get a new array of data from the FFT
|
|
@@ -1570,12 +1829,6 @@ export default class AudioMotionAnalyzer {
|
|
|
1570
1829
|
if ( weightingFilter )
|
|
1571
1830
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1572
1831
|
|
|
1573
|
-
// helper function for FFT data interpolation
|
|
1574
|
-
const interpolate = ( bin, ratio ) => {
|
|
1575
|
-
const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
|
|
1576
|
-
return isNaN( value ) ? -Infinity : value;
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
1832
|
// start drawing path (for mode 10)
|
|
1580
1833
|
ctx.beginPath();
|
|
1581
1834
|
|
|
@@ -1584,24 +1837,24 @@ export default class AudioMotionAnalyzer {
|
|
|
1584
1837
|
|
|
1585
1838
|
// draw bars / lines
|
|
1586
1839
|
|
|
1587
|
-
for ( let
|
|
1840
|
+
for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
|
|
1588
1841
|
|
|
1589
|
-
const bar =
|
|
1590
|
-
{ freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1842
|
+
const bar = bars[ barIndex ],
|
|
1843
|
+
{ posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1591
1844
|
|
|
1592
|
-
let
|
|
1845
|
+
let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
|
|
1593
1846
|
|
|
1594
1847
|
// check additional bins (if any) for this bar and keep the highest value
|
|
1595
1848
|
for ( let j = binLo + 1; j < binHi; j++ ) {
|
|
1596
|
-
if ( fftData[ j ] >
|
|
1597
|
-
|
|
1849
|
+
if ( fftData[ j ] > barValue )
|
|
1850
|
+
barValue = fftData[ j ];
|
|
1598
1851
|
}
|
|
1599
1852
|
|
|
1600
1853
|
// normalize bar amplitude in [0;1] range
|
|
1601
|
-
|
|
1854
|
+
barValue = this._normalizedB( barValue );
|
|
1602
1855
|
|
|
1603
|
-
bar.value[ channel ] =
|
|
1604
|
-
currentEnergy +=
|
|
1856
|
+
bar.value[ channel ] = barValue;
|
|
1857
|
+
currentEnergy += barValue;
|
|
1605
1858
|
|
|
1606
1859
|
// update bar peak
|
|
1607
1860
|
if ( bar.peak[ channel ] > 0 ) {
|
|
@@ -1612,8 +1865,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1612
1865
|
}
|
|
1613
1866
|
|
|
1614
1867
|
// check if it's a new peak for this bar
|
|
1615
|
-
if (
|
|
1616
|
-
bar.peak[ channel ] =
|
|
1868
|
+
if ( barValue >= bar.peak[ channel ] ) {
|
|
1869
|
+
bar.peak[ channel ] = barValue;
|
|
1617
1870
|
bar.hold[ channel ] = 30; // set peak hold time to 30 frames (0.5s)
|
|
1618
1871
|
}
|
|
1619
1872
|
|
|
@@ -1622,31 +1875,30 @@ export default class AudioMotionAnalyzer {
|
|
|
1622
1875
|
continue;
|
|
1623
1876
|
|
|
1624
1877
|
// set opacity for bar effects
|
|
1625
|
-
if (
|
|
1626
|
-
ctx.globalAlpha =
|
|
1878
|
+
if ( isLumi || isAlpha )
|
|
1879
|
+
ctx.globalAlpha = barValue;
|
|
1627
1880
|
else if ( isOutline )
|
|
1628
1881
|
ctx.globalAlpha = fillAlpha;
|
|
1629
1882
|
|
|
1883
|
+
// set fillStyle and strokeStyle for the current bar
|
|
1884
|
+
setBarColor( barValue, barIndex );
|
|
1885
|
+
|
|
1630
1886
|
// compute actual bar height on screen
|
|
1631
|
-
barHeight =
|
|
1887
|
+
let barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
|
|
1632
1888
|
|
|
1633
1889
|
// invert bar for radial channel 1
|
|
1634
1890
|
if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
|
|
1635
1891
|
barHeight *= -1;
|
|
1636
1892
|
|
|
1637
|
-
// bar width may need small adjustments for some bars, when barSpace == 0
|
|
1638
|
-
let adjWidth = width,
|
|
1639
|
-
posX = bar.posX;
|
|
1640
|
-
|
|
1641
1893
|
// Draw current bar or line segment
|
|
1642
1894
|
|
|
1643
1895
|
if ( mode == 10 ) {
|
|
1644
|
-
// compute the average between the initial bar (
|
|
1896
|
+
// compute the average between the initial bar (barIndex==0) and the next one
|
|
1645
1897
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
1646
|
-
const nextBarAvg =
|
|
1898
|
+
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
|
|
1647
1899
|
|
|
1648
1900
|
if ( isRadial ) {
|
|
1649
|
-
if (
|
|
1901
|
+
if ( barIndex == 0 )
|
|
1650
1902
|
ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ), 1 ) );
|
|
1651
1903
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
1652
1904
|
if ( posX >= 0 ) {
|
|
@@ -1656,7 +1908,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1656
1908
|
}
|
|
1657
1909
|
}
|
|
1658
1910
|
else { // Linear
|
|
1659
|
-
if (
|
|
1911
|
+
if ( barIndex == 0 ) {
|
|
1660
1912
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
1661
1913
|
if ( mirrorMode != -1 ) {
|
|
1662
1914
|
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
@@ -1672,57 +1924,50 @@ export default class AudioMotionAnalyzer {
|
|
|
1672
1924
|
}
|
|
1673
1925
|
}
|
|
1674
1926
|
else {
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
else {
|
|
1679
|
-
if ( barSpace == 0 ) {
|
|
1680
|
-
posX |= 0;
|
|
1681
|
-
if ( i > 0 && posX > this._bars[ i - 1 ].posX + width ) {
|
|
1682
|
-
posX--;
|
|
1683
|
-
adjWidth++;
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
else
|
|
1687
|
-
posX += barSpacePx / 2;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
if ( isLedDisplay ) {
|
|
1692
|
-
const x = posX + width / 2;
|
|
1693
|
-
// draw "unlit" leds
|
|
1694
|
-
if ( showBgColor && ! isOverlay ) {
|
|
1927
|
+
if ( isLeds ) {
|
|
1928
|
+
// draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
|
|
1929
|
+
if ( showBgColor && ! isOverlay && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) ) {
|
|
1695
1930
|
const alpha = ctx.globalAlpha;
|
|
1696
|
-
ctx.beginPath();
|
|
1697
|
-
ctx.moveTo( x, channelTop );
|
|
1698
|
-
ctx.lineTo( x, analyzerBottom );
|
|
1699
1931
|
ctx.strokeStyle = LEDS_UNLIT_COLOR;
|
|
1700
1932
|
ctx.globalAlpha = 1;
|
|
1701
|
-
|
|
1933
|
+
strokeBar( barCenter, channelTop, analyzerBottom );
|
|
1702
1934
|
// restore properties
|
|
1703
1935
|
ctx.strokeStyle = ctx.fillStyle;
|
|
1704
1936
|
ctx.globalAlpha = alpha;
|
|
1705
1937
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1938
|
+
if ( isTrueLeds ) {
|
|
1939
|
+
// ledPosY() is used below to fit one entire led height into the selected range
|
|
1940
|
+
const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
|
|
1941
|
+
let last = analyzerBottom;
|
|
1942
|
+
for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
|
|
1943
|
+
ctx.strokeStyle = colorStops[ i ].color;
|
|
1944
|
+
let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
|
|
1945
|
+
strokeBar( barCenter, last, y );
|
|
1946
|
+
last = y - ledSpaceV;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
else
|
|
1950
|
+
strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
|
|
1710
1951
|
}
|
|
1711
1952
|
else if ( posX >= initialX ) {
|
|
1712
1953
|
if ( isRadial )
|
|
1713
|
-
radialPoly( posX, 0,
|
|
1714
|
-
else {
|
|
1715
|
-
const
|
|
1716
|
-
y =
|
|
1717
|
-
w = adjWidth,
|
|
1718
|
-
h = isLumiBars ? channelBottom : -barHeight;
|
|
1954
|
+
radialPoly( posX, 0, width, barHeight, isOutline );
|
|
1955
|
+
else if ( isRound ) {
|
|
1956
|
+
const halfWidth = width / 2,
|
|
1957
|
+
y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
|
|
1719
1958
|
|
|
1720
1959
|
ctx.beginPath();
|
|
1721
|
-
ctx.moveTo(
|
|
1722
|
-
ctx.lineTo(
|
|
1723
|
-
ctx.
|
|
1724
|
-
ctx.lineTo(
|
|
1725
|
-
|
|
1960
|
+
ctx.moveTo( posX, y );
|
|
1961
|
+
ctx.lineTo( posX, y - barHeight );
|
|
1962
|
+
ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
|
|
1963
|
+
ctx.lineTo( posX + width, y );
|
|
1964
|
+
strokeIf( isOutline );
|
|
1965
|
+
ctx.fill();
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
const offset = isOutline ? ctx.lineWidth : 0;
|
|
1969
|
+
ctx.beginPath();
|
|
1970
|
+
ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
|
|
1726
1971
|
strokeIf( isOutline );
|
|
1727
1972
|
ctx.fill();
|
|
1728
1973
|
}
|
|
@@ -1731,36 +1976,44 @@ export default class AudioMotionAnalyzer {
|
|
|
1731
1976
|
|
|
1732
1977
|
// Draw peak
|
|
1733
1978
|
const peak = bar.peak[ channel ];
|
|
1734
|
-
if ( peak > 0 && this.showPeaks && !
|
|
1735
|
-
//
|
|
1979
|
+
if ( peak > 0 && this.showPeaks && ! isLumi && posX >= initialX && posX < finalX ) {
|
|
1980
|
+
// set opacity
|
|
1736
1981
|
if ( isOutline && lineWidth > 0 )
|
|
1737
1982
|
ctx.globalAlpha = 1;
|
|
1738
|
-
else if (
|
|
1983
|
+
else if ( isAlpha )
|
|
1739
1984
|
ctx.globalAlpha = peak;
|
|
1740
1985
|
|
|
1986
|
+
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
1987
|
+
if ( colorMode == COLOR_BAR_LEVEL || isTrueLeds )
|
|
1988
|
+
setBarColor( peak );
|
|
1989
|
+
|
|
1741
1990
|
// render peak according to current mode / effect
|
|
1742
|
-
if (
|
|
1991
|
+
if ( isLeds ) {
|
|
1743
1992
|
const ledPeak = ledPosY( peak );
|
|
1744
|
-
if ( ledPeak >=
|
|
1993
|
+
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
1745
1994
|
ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
1746
1995
|
}
|
|
1747
1996
|
else if ( ! isRadial )
|
|
1748
|
-
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight,
|
|
1997
|
+
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
|
|
1749
1998
|
else if ( mode != 10 ) // radial - no peaks for mode 10
|
|
1750
|
-
radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ),
|
|
1999
|
+
radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), width, -2 );
|
|
1751
2000
|
}
|
|
1752
2001
|
|
|
1753
|
-
} // for ( let
|
|
2002
|
+
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
1754
2003
|
|
|
1755
2004
|
// if not using the canvas, move earlier to the next channel
|
|
1756
2005
|
if ( ! useCanvas )
|
|
1757
2006
|
continue;
|
|
1758
2007
|
|
|
2008
|
+
ctx.restore(); // restore clip region
|
|
2009
|
+
|
|
1759
2010
|
// restore global alpha
|
|
1760
2011
|
ctx.globalAlpha = 1;
|
|
1761
2012
|
|
|
1762
2013
|
// Fill/stroke drawing path for mode 10
|
|
1763
2014
|
if ( mode == 10 ) {
|
|
2015
|
+
setBarColor(); // select channel gradient
|
|
2016
|
+
|
|
1764
2017
|
if ( isRadial ) {
|
|
1765
2018
|
if ( mirrorMode ) {
|
|
1766
2019
|
let p;
|
|
@@ -1790,47 +2043,12 @@ export default class AudioMotionAnalyzer {
|
|
|
1790
2043
|
}
|
|
1791
2044
|
}
|
|
1792
2045
|
|
|
1793
|
-
// Reflex effect
|
|
1794
|
-
|
|
1795
|
-
let posY, height;
|
|
1796
|
-
if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
|
|
1797
|
-
posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
|
|
1798
|
-
height = channelHeight - analyzerHeight;
|
|
1799
|
-
}
|
|
1800
|
-
else {
|
|
1801
|
-
posY = canvas.height - analyzerHeight * 2;
|
|
1802
|
-
height = analyzerHeight;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
// set alpha and brightness for the reflection
|
|
1806
|
-
ctx.globalAlpha = this.reflexAlpha;
|
|
1807
|
-
if ( this.reflexBright != 1 )
|
|
1808
|
-
ctx.filter = `brightness(${this.reflexBright})`;
|
|
1809
|
-
|
|
1810
|
-
// create the reflection
|
|
1811
|
-
ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
|
|
1812
|
-
ctx.drawImage( canvas, 0, channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
|
|
1813
|
-
|
|
1814
|
-
// reset changed properties
|
|
1815
|
-
ctx.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
1816
|
-
ctx.filter = 'none';
|
|
1817
|
-
ctx.globalAlpha = 1;
|
|
1818
|
-
}
|
|
2046
|
+
// create Reflex effect
|
|
2047
|
+
doReflex( channel );
|
|
1819
2048
|
|
|
1820
2049
|
} // for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1821
2050
|
|
|
1822
|
-
|
|
1823
|
-
energy.val = currentEnergy / ( nBars << ( nChannels - 1 ) );
|
|
1824
|
-
if ( energy.val >= energy.peak ) {
|
|
1825
|
-
energy.peak = energy.val;
|
|
1826
|
-
energy.hold = 30;
|
|
1827
|
-
}
|
|
1828
|
-
else {
|
|
1829
|
-
if ( energy.hold > 0 )
|
|
1830
|
-
energy.hold--;
|
|
1831
|
-
else if ( energy.peak > 0 )
|
|
1832
|
-
energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
|
|
1833
|
-
}
|
|
2051
|
+
updateEnergy( currentEnergy / ( nBars << ( nChannels - 1 ) ) );
|
|
1834
2052
|
|
|
1835
2053
|
if ( useCanvas ) {
|
|
1836
2054
|
// Mirror effect
|
|
@@ -1844,37 +2062,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1844
2062
|
ctx.setLineDash([]);
|
|
1845
2063
|
|
|
1846
2064
|
// draw frequency scale (X-axis)
|
|
1847
|
-
|
|
1848
|
-
if ( isRadial ) {
|
|
1849
|
-
ctx.save();
|
|
1850
|
-
ctx.translate( centerX, centerY );
|
|
1851
|
-
if ( this._spinSpeed )
|
|
1852
|
-
ctx.rotate( this._spinAngle + HALF_PI );
|
|
1853
|
-
ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
|
|
1854
|
-
ctx.restore();
|
|
1855
|
-
}
|
|
1856
|
-
else
|
|
1857
|
-
ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
|
|
1858
|
-
}
|
|
2065
|
+
drawScaleX();
|
|
1859
2066
|
}
|
|
1860
2067
|
|
|
1861
|
-
// calculate and
|
|
1862
|
-
|
|
1863
|
-
this._frame++;
|
|
1864
|
-
const elapsed = timestamp - this._time;
|
|
1865
|
-
|
|
1866
|
-
if ( elapsed >= 1000 ) {
|
|
1867
|
-
this._fps = this._frame / ( elapsed / 1000 );
|
|
1868
|
-
this._frame = 0;
|
|
1869
|
-
this._time = timestamp;
|
|
1870
|
-
}
|
|
1871
|
-
if ( this.showFPS ) {
|
|
1872
|
-
const size = canvasX.height;
|
|
1873
|
-
ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
1874
|
-
ctx.fillStyle = FPS_COLOR;
|
|
1875
|
-
ctx.textAlign = 'right';
|
|
1876
|
-
ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
|
|
1877
|
-
}
|
|
2068
|
+
// calculate and display (if enabled) the current frame rate
|
|
2069
|
+
updateFPS();
|
|
1878
2070
|
|
|
1879
2071
|
// call callback function, if defined
|
|
1880
2072
|
if ( this.onCanvasDraw ) {
|
|
@@ -1925,18 +2117,17 @@ export default class AudioMotionAnalyzer {
|
|
|
1925
2117
|
const ctx = this._canvasCtx,
|
|
1926
2118
|
canvas = ctx.canvas,
|
|
1927
2119
|
channelLayout = this._chLayout,
|
|
1928
|
-
|
|
2120
|
+
{ isLumi } = this._flg,
|
|
1929
2121
|
isRadial = this._radial,
|
|
1930
|
-
gradientHeight =
|
|
2122
|
+
gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
|
|
1931
2123
|
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
1932
2124
|
analyzerRatio = 1 - this._reflexRatio,
|
|
1933
|
-
initialX
|
|
2125
|
+
{ analyzerWidth, initialX, radius } = this._aux;
|
|
1934
2126
|
|
|
1935
2127
|
// for radial mode
|
|
1936
2128
|
const centerX = canvas.width >> 1,
|
|
1937
2129
|
centerY = canvas.height >> 1,
|
|
1938
|
-
maxRadius = Math.min( centerX, centerY )
|
|
1939
|
-
radius = this._radius;
|
|
2130
|
+
maxRadius = Math.min( centerX, centerY );
|
|
1940
2131
|
|
|
1941
2132
|
for ( const channel of [0,1] ) {
|
|
1942
2133
|
const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
@@ -1948,26 +2139,23 @@ export default class AudioMotionAnalyzer {
|
|
|
1948
2139
|
if ( isRadial )
|
|
1949
2140
|
grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( channelLayout == CHANNEL_VERTICAL ) );
|
|
1950
2141
|
else
|
|
1951
|
-
grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX +
|
|
2142
|
+
grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
|
|
1952
2143
|
|
|
1953
2144
|
if ( colorStops ) {
|
|
1954
2145
|
const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
|
|
1955
2146
|
|
|
1956
|
-
// helper function
|
|
1957
|
-
const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
|
|
1958
|
-
|
|
1959
2147
|
for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
|
|
2148
|
+
const maxIndex = colorStops.length - 1;
|
|
1960
2149
|
|
|
1961
|
-
colorStops.forEach( (
|
|
1962
|
-
|
|
1963
|
-
let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / Math.max( 1, maxIndex );
|
|
2150
|
+
colorStops.forEach( ( colorStop, index ) => {
|
|
2151
|
+
let offset = colorStop.pos;
|
|
1964
2152
|
|
|
1965
2153
|
// in dual mode (not split), use half the original offset for each channel
|
|
1966
2154
|
if ( dual )
|
|
1967
2155
|
offset /= 2;
|
|
1968
2156
|
|
|
1969
2157
|
// constrain the offset within the useful analyzer areas (avoid reflex areas)
|
|
1970
|
-
if ( channelLayout == CHANNEL_VERTICAL && !
|
|
2158
|
+
if ( channelLayout == CHANNEL_VERTICAL && ! isLumi && ! isRadial && ! isHorizontal ) {
|
|
1971
2159
|
offset *= analyzerRatio;
|
|
1972
2160
|
// skip the first reflex area in split mode
|
|
1973
2161
|
if ( ! dual && offset > .5 * analyzerRatio )
|
|
@@ -1977,26 +2165,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1977
2165
|
// only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
|
|
1978
2166
|
if ( channelArea == 1 ) {
|
|
1979
2167
|
// add colors in reverse order if radial or lumi are active
|
|
1980
|
-
if ( isRadial ||
|
|
2168
|
+
if ( isRadial || isLumi ) {
|
|
1981
2169
|
const revIndex = maxIndex - index;
|
|
1982
|
-
|
|
1983
|
-
offset = 1 -
|
|
2170
|
+
colorStop = colorStops[ revIndex ];
|
|
2171
|
+
offset = 1 - colorStop.pos / 2;
|
|
1984
2172
|
}
|
|
1985
2173
|
else {
|
|
1986
2174
|
// if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
|
|
1987
2175
|
if ( index == 0 && offset > 0 )
|
|
1988
|
-
addColorStop( .5,
|
|
2176
|
+
grad.addColorStop( .5, colorStop.color );
|
|
1989
2177
|
// bump the offset to the second half of the gradient
|
|
1990
2178
|
offset += .5;
|
|
1991
2179
|
}
|
|
1992
2180
|
}
|
|
1993
2181
|
|
|
1994
2182
|
// add gradient color stop
|
|
1995
|
-
addColorStop( offset,
|
|
2183
|
+
grad.addColorStop( offset, colorStop.color );
|
|
1996
2184
|
|
|
1997
2185
|
// create additional color stop at the end of first channel to prevent bleeding
|
|
1998
2186
|
if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
|
|
1999
|
-
addColorStop( .5,
|
|
2187
|
+
grad.addColorStop( .5, colorStop.color );
|
|
2000
2188
|
});
|
|
2001
2189
|
} // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
|
|
2002
2190
|
}
|
|
@@ -2064,9 +2252,6 @@ export default class AudioMotionAnalyzer {
|
|
|
2064
2252
|
canvas.width = newWidth;
|
|
2065
2253
|
canvas.height = newHeight;
|
|
2066
2254
|
|
|
2067
|
-
// update internal variables
|
|
2068
|
-
this._calcAux();
|
|
2069
|
-
|
|
2070
2255
|
// if not in overlay mode, paint the canvas black
|
|
2071
2256
|
if ( ! this.overlay ) {
|
|
2072
2257
|
ctx.fillStyle = '#000';
|
|
@@ -2078,14 +2263,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2078
2263
|
|
|
2079
2264
|
// update dimensions of the scale canvas
|
|
2080
2265
|
canvasX.width = newWidth;
|
|
2081
|
-
canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) /
|
|
2082
|
-
|
|
2083
|
-
// (re)generate gradient
|
|
2084
|
-
this._makeGrad();
|
|
2266
|
+
canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) / 32 | 0 );
|
|
2085
2267
|
|
|
2086
2268
|
// calculate bar positions and led options
|
|
2087
2269
|
this._calcBars();
|
|
2088
2270
|
|
|
2271
|
+
// (re)generate gradient
|
|
2272
|
+
this._makeGrad();
|
|
2273
|
+
|
|
2089
2274
|
// detect fullscreen changes (for Safari)
|
|
2090
2275
|
if ( this._fsStatus !== undefined && this._fsStatus !== isFullscreen )
|
|
2091
2276
|
reason = REASON_FSCHANGE;
|
|
@@ -2127,6 +2312,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2127
2312
|
barSpace : 0.1,
|
|
2128
2313
|
bgAlpha : 0.7,
|
|
2129
2314
|
channelLayout : CHANNEL_SINGLE,
|
|
2315
|
+
colorMode : COLOR_GRADIENT,
|
|
2130
2316
|
fftSize : 8192,
|
|
2131
2317
|
fillAlpha : 1,
|
|
2132
2318
|
frequencyScale : SCALE_LOG,
|
|
@@ -2151,6 +2337,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2151
2337
|
reflexBright : 1,
|
|
2152
2338
|
reflexFit : true,
|
|
2153
2339
|
reflexRatio : 0,
|
|
2340
|
+
roundBars : false,
|
|
2154
2341
|
showBgColor : true,
|
|
2155
2342
|
showFPS : false,
|
|
2156
2343
|
showPeaks : true,
|
|
@@ -2160,6 +2347,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2160
2347
|
spinSpeed : 0,
|
|
2161
2348
|
splitGradient : false,
|
|
2162
2349
|
start : true,
|
|
2350
|
+
trueLeds : false,
|
|
2163
2351
|
useCanvas : true,
|
|
2164
2352
|
volume : 1,
|
|
2165
2353
|
weightingFilter: FILTER_NONE
|