audiomotion-analyzer 4.0.0 → 4.1.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 +92 -28
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +622 -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.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.1.0';
|
|
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,13 @@ 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
|
+
|
|
98
105
|
// AudioMotionAnalyzer class
|
|
99
106
|
|
|
100
107
|
export default class AudioMotionAnalyzer {
|
|
@@ -110,10 +117,12 @@ export default class AudioMotionAnalyzer {
|
|
|
110
117
|
|
|
111
118
|
this._ready = false;
|
|
112
119
|
|
|
113
|
-
// Initialize internal
|
|
120
|
+
// Initialize internal objects
|
|
121
|
+
this._aux = {};
|
|
122
|
+
this._flg = {};
|
|
114
123
|
this._gradients = {}; // registered gradients
|
|
115
124
|
this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
|
|
116
|
-
this._canvasGradients = []; //
|
|
125
|
+
this._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
|
|
117
126
|
|
|
118
127
|
// Register built-in gradients
|
|
119
128
|
for ( const [ name, options ] of GRADIENTS )
|
|
@@ -262,9 +271,6 @@ export default class AudioMotionAnalyzer {
|
|
|
262
271
|
}
|
|
263
272
|
window.addEventListener( 'click', unlockContext );
|
|
264
273
|
|
|
265
|
-
// initialize internal variables
|
|
266
|
-
this._calcAux();
|
|
267
|
-
|
|
268
274
|
// Set configuration options and use defaults for any missing properties
|
|
269
275
|
this._setProps( options, true );
|
|
270
276
|
|
|
@@ -290,7 +296,7 @@ export default class AudioMotionAnalyzer {
|
|
|
290
296
|
}
|
|
291
297
|
set alphaBars( value ) {
|
|
292
298
|
this._alphaBars = !! value;
|
|
293
|
-
this.
|
|
299
|
+
this._calcBars();
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
get ansiBands() {
|
|
@@ -306,15 +312,14 @@ export default class AudioMotionAnalyzer {
|
|
|
306
312
|
}
|
|
307
313
|
set barSpace( value ) {
|
|
308
314
|
this._barSpace = +value || 0;
|
|
309
|
-
this.
|
|
315
|
+
this._calcBars();
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
get channelLayout() {
|
|
313
319
|
return this._chLayout;
|
|
314
320
|
}
|
|
315
321
|
set channelLayout( value ) {
|
|
316
|
-
|
|
317
|
-
this._chLayout = LAYOUTS[ Math.max( 0, LAYOUTS.indexOf( ( '' + value ).toLowerCase() ) ) ];
|
|
322
|
+
this._chLayout = validateFromList( value, [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ] );
|
|
318
323
|
|
|
319
324
|
// update node connections
|
|
320
325
|
this._input.disconnect();
|
|
@@ -323,13 +328,17 @@ export default class AudioMotionAnalyzer {
|
|
|
323
328
|
if ( this._outNodes.length ) // connect analyzer only if the output is connected to other nodes
|
|
324
329
|
this._analyzer[0].connect( this._chLayout != CHANNEL_SINGLE ? this._merger : this._output );
|
|
325
330
|
|
|
326
|
-
|
|
327
|
-
this._calcAux();
|
|
328
|
-
this._createScales();
|
|
329
|
-
this._calcLeds();
|
|
331
|
+
this._calcBars();
|
|
330
332
|
this._makeGrad();
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
get colorMode() {
|
|
336
|
+
return this._colorMode;
|
|
337
|
+
}
|
|
338
|
+
set colorMode( value ) {
|
|
339
|
+
this._colorMode = validateFromList( value, [ COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL ] );
|
|
340
|
+
}
|
|
341
|
+
|
|
333
342
|
get fftSize() {
|
|
334
343
|
return this._analyzer[0].fftSize;
|
|
335
344
|
}
|
|
@@ -345,9 +354,7 @@ export default class AudioMotionAnalyzer {
|
|
|
345
354
|
return this._frequencyScale;
|
|
346
355
|
}
|
|
347
356
|
set frequencyScale( value ) {
|
|
348
|
-
|
|
349
|
-
this._frequencyScale = FREQUENCY_SCALES[ Math.max( 0, FREQUENCY_SCALES.indexOf( ( '' + value ).toLowerCase() ) ) ];
|
|
350
|
-
this._calcAux();
|
|
357
|
+
this._frequencyScale = validateFromList( value, [ SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR ] );
|
|
351
358
|
this._calcBars();
|
|
352
359
|
}
|
|
353
360
|
|
|
@@ -385,7 +392,7 @@ export default class AudioMotionAnalyzer {
|
|
|
385
392
|
}
|
|
386
393
|
set ledBars( value ) {
|
|
387
394
|
this._showLeds = !! value;
|
|
388
|
-
this.
|
|
395
|
+
this._calcBars();
|
|
389
396
|
}
|
|
390
397
|
|
|
391
398
|
get linearAmplitude() {
|
|
@@ -402,6 +409,13 @@ export default class AudioMotionAnalyzer {
|
|
|
402
409
|
this._linearBoost = value >= 1 ? +value : 1;
|
|
403
410
|
}
|
|
404
411
|
|
|
412
|
+
get lineWidth() {
|
|
413
|
+
return this._lineWidth;
|
|
414
|
+
}
|
|
415
|
+
set lineWidth( value ) {
|
|
416
|
+
this._lineWidth = +value || 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
405
419
|
get loRes() {
|
|
406
420
|
return this._loRes;
|
|
407
421
|
}
|
|
@@ -415,8 +429,7 @@ export default class AudioMotionAnalyzer {
|
|
|
415
429
|
}
|
|
416
430
|
set lumiBars( value ) {
|
|
417
431
|
this._lumiBars = !! value;
|
|
418
|
-
this.
|
|
419
|
-
this._calcLeds();
|
|
432
|
+
this._calcBars();
|
|
420
433
|
this._makeGrad();
|
|
421
434
|
}
|
|
422
435
|
|
|
@@ -465,7 +478,6 @@ export default class AudioMotionAnalyzer {
|
|
|
465
478
|
}
|
|
466
479
|
set mirror( value ) {
|
|
467
480
|
this._mirror = Math.sign( value ) | 0; // ensure only -1, 0 or 1
|
|
468
|
-
this._calcAux();
|
|
469
481
|
this._calcBars();
|
|
470
482
|
this._makeGrad();
|
|
471
483
|
}
|
|
@@ -477,7 +489,6 @@ export default class AudioMotionAnalyzer {
|
|
|
477
489
|
const mode = value | 0;
|
|
478
490
|
if ( mode >= 0 && mode <= 10 && mode != 9 ) {
|
|
479
491
|
this._mode = mode;
|
|
480
|
-
this._calcAux();
|
|
481
492
|
this._calcBars();
|
|
482
493
|
this._makeGrad();
|
|
483
494
|
}
|
|
@@ -498,7 +509,7 @@ export default class AudioMotionAnalyzer {
|
|
|
498
509
|
}
|
|
499
510
|
set outlineBars( value ) {
|
|
500
511
|
this._outlineBars = !! value;
|
|
501
|
-
this.
|
|
512
|
+
this._calcBars();
|
|
502
513
|
}
|
|
503
514
|
|
|
504
515
|
get radial() {
|
|
@@ -506,7 +517,6 @@ export default class AudioMotionAnalyzer {
|
|
|
506
517
|
}
|
|
507
518
|
set radial( value ) {
|
|
508
519
|
this._radial = !! value;
|
|
509
|
-
this._calcAux();
|
|
510
520
|
this._calcBars();
|
|
511
521
|
this._makeGrad();
|
|
512
522
|
}
|
|
@@ -520,12 +530,19 @@ export default class AudioMotionAnalyzer {
|
|
|
520
530
|
throw new AudioMotionError( ERR_REFLEX_OUT_OF_RANGE );
|
|
521
531
|
else {
|
|
522
532
|
this._reflexRatio = value;
|
|
523
|
-
this.
|
|
533
|
+
this._calcBars();
|
|
524
534
|
this._makeGrad();
|
|
525
|
-
this._calcLeds();
|
|
526
535
|
}
|
|
527
536
|
}
|
|
528
537
|
|
|
538
|
+
get roundBars() {
|
|
539
|
+
return this._roundBars;
|
|
540
|
+
}
|
|
541
|
+
set roundBars( value ) {
|
|
542
|
+
this._roundBars = !! value;
|
|
543
|
+
this._calcBars();
|
|
544
|
+
}
|
|
545
|
+
|
|
529
546
|
get smoothing() {
|
|
530
547
|
return this._analyzer[0].smoothingTimeConstant;
|
|
531
548
|
}
|
|
@@ -561,6 +578,13 @@ export default class AudioMotionAnalyzer {
|
|
|
561
578
|
this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
|
|
562
579
|
}
|
|
563
580
|
|
|
581
|
+
get trueLeds() {
|
|
582
|
+
return this._trueLeds;
|
|
583
|
+
}
|
|
584
|
+
set trueLeds( value ) {
|
|
585
|
+
this._trueLeds = !! value;
|
|
586
|
+
}
|
|
587
|
+
|
|
564
588
|
get volume() {
|
|
565
589
|
return this._output.gain.value;
|
|
566
590
|
}
|
|
@@ -572,8 +596,7 @@ export default class AudioMotionAnalyzer {
|
|
|
572
596
|
return this._weightingFilter;
|
|
573
597
|
}
|
|
574
598
|
set weightingFilter( value ) {
|
|
575
|
-
|
|
576
|
-
this._weightingFilter = WEIGHTING_FILTERS[ Math.max( 0, WEIGHTING_FILTERS.indexOf( ( '' + value ).toUpperCase() ) ) ];
|
|
599
|
+
this._weightingFilter = validateFromList( value, [ FILTER_NONE, FILTER_A, FILTER_B, FILTER_C, FILTER_D, FILTER_468 ], 'toUpperCase' );
|
|
577
600
|
}
|
|
578
601
|
|
|
579
602
|
get width() {
|
|
@@ -611,32 +634,35 @@ export default class AudioMotionAnalyzer {
|
|
|
611
634
|
return this._fsWidth;
|
|
612
635
|
}
|
|
613
636
|
get isAlphaBars() {
|
|
614
|
-
return this.
|
|
637
|
+
return this._flg.isAlpha;
|
|
615
638
|
}
|
|
616
639
|
get isBandsMode() {
|
|
617
|
-
return this.
|
|
640
|
+
return this._flg.isBands;
|
|
618
641
|
}
|
|
619
642
|
get isFullscreen() {
|
|
620
643
|
return ( document.fullscreenElement || document.webkitFullscreenElement ) === this._fsEl;
|
|
621
644
|
}
|
|
622
645
|
get isLedBars() {
|
|
623
|
-
return this.
|
|
646
|
+
return this._flg.isLeds;
|
|
624
647
|
}
|
|
625
648
|
get isLumiBars() {
|
|
626
|
-
return this.
|
|
649
|
+
return this._flg.isLumi;
|
|
627
650
|
}
|
|
628
651
|
get isOctaveBands() {
|
|
629
|
-
return this.
|
|
652
|
+
return this._flg.isOctaves;
|
|
630
653
|
}
|
|
631
654
|
get isOn() {
|
|
632
655
|
return this._runId !== undefined;
|
|
633
656
|
}
|
|
634
657
|
get isOutlineBars() {
|
|
635
|
-
return this.
|
|
658
|
+
return this._flg.isOutline;
|
|
636
659
|
}
|
|
637
660
|
get pixelRatio() {
|
|
638
661
|
return this._pixelRatio;
|
|
639
662
|
}
|
|
663
|
+
get isRoundBars() {
|
|
664
|
+
return this._flg.isRound;
|
|
665
|
+
}
|
|
640
666
|
static get version() {
|
|
641
667
|
return VERSION;
|
|
642
668
|
}
|
|
@@ -790,19 +816,41 @@ export default class AudioMotionAnalyzer {
|
|
|
790
816
|
* @param {object} options
|
|
791
817
|
*/
|
|
792
818
|
registerGradient( name, options ) {
|
|
793
|
-
if ( typeof name
|
|
819
|
+
if ( typeof name != 'string' || name.trim().length == 0 )
|
|
794
820
|
throw new AudioMotionError( ERR_GRADIENT_INVALID_NAME );
|
|
795
821
|
|
|
796
|
-
if ( typeof options
|
|
822
|
+
if ( typeof options != 'object' )
|
|
797
823
|
throw new AudioMotionError( ERR_GRADIENT_NOT_AN_OBJECT );
|
|
798
824
|
|
|
799
|
-
|
|
825
|
+
const { colorStops } = options;
|
|
826
|
+
|
|
827
|
+
if ( ! Array.isArray( colorStops ) || ! colorStops.length )
|
|
800
828
|
throw new AudioMotionError( ERR_GRADIENT_MISSING_COLOR );
|
|
801
829
|
|
|
830
|
+
const count = colorStops.length,
|
|
831
|
+
isInvalid = val => +val != val || val < 0 || val > 1;
|
|
832
|
+
|
|
833
|
+
// normalize all colorStops as objects with `pos`, `color` and `level` properties
|
|
834
|
+
colorStops.forEach( ( colorStop, index ) => {
|
|
835
|
+
const pos = index / Math.max( 1, count - 1 );
|
|
836
|
+
if ( typeof colorStop != 'object' ) // only color string was defined
|
|
837
|
+
colorStops[ index ] = { pos, color: colorStop };
|
|
838
|
+
else if ( isInvalid( colorStop.pos ) )
|
|
839
|
+
colorStop.pos = pos;
|
|
840
|
+
|
|
841
|
+
if ( isInvalid( colorStop.level ) )
|
|
842
|
+
colorStops[ index ].level = 1 - index / count;
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// make sure colorStops is in descending `level` order and that the first one has `level == 1`
|
|
846
|
+
// this is crucial for proper operation of 'bar-level' colorMode!
|
|
847
|
+
colorStops.sort( ( a, b ) => a.level < b.level ? 1 : a.level > b.level ? -1 : 0 );
|
|
848
|
+
colorStops[0].level = 1;
|
|
849
|
+
|
|
802
850
|
this._gradients[ name ] = {
|
|
803
851
|
bgColor: options.bgColor || GRADIENT_DEFAULT_BGCOLOR,
|
|
804
852
|
dir: options.dir,
|
|
805
|
-
colorStops:
|
|
853
|
+
colorStops: colorStops
|
|
806
854
|
};
|
|
807
855
|
|
|
808
856
|
// if the registered gradient is one of the currently selected gradients, regenerate them
|
|
@@ -854,7 +902,7 @@ export default class AudioMotionAnalyzer {
|
|
|
854
902
|
}
|
|
855
903
|
|
|
856
904
|
this._ledParams = maxLeds > 0 && spaceV > 0 && spaceH >= 0 ? [ maxLeds, spaceV, spaceH ] : undefined;
|
|
857
|
-
this.
|
|
905
|
+
this._calcBars();
|
|
858
906
|
}
|
|
859
907
|
|
|
860
908
|
/**
|
|
@@ -939,37 +987,7 @@ export default class AudioMotionAnalyzer {
|
|
|
939
987
|
}
|
|
940
988
|
|
|
941
989
|
/**
|
|
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
|
|
990
|
+
* Compute all internal data required for the analyzer, based on its current settings
|
|
973
991
|
*/
|
|
974
992
|
_calcBars() {
|
|
975
993
|
const bars = this._bars = []; // initialize object property
|
|
@@ -977,9 +995,63 @@ export default class AudioMotionAnalyzer {
|
|
|
977
995
|
if ( ! this._ready )
|
|
978
996
|
return;
|
|
979
997
|
|
|
998
|
+
const barSpace = this._barSpace,
|
|
999
|
+
canvas = this.canvas,
|
|
1000
|
+
centerX = canvas.width >> 1,
|
|
1001
|
+
chLayout = this._chLayout,
|
|
1002
|
+
isAnsiBands = this._ansiBands,
|
|
1003
|
+
isRadial = this._radial,
|
|
1004
|
+
isDual = chLayout == CHANNEL_VERTICAL && ! isRadial,
|
|
1005
|
+
maxFreq = this._maxFreq,
|
|
1006
|
+
minFreq = this._minFreq,
|
|
1007
|
+
mode = this._mode,
|
|
1008
|
+
|
|
1009
|
+
// COMPUTE FLAGS
|
|
1010
|
+
|
|
1011
|
+
isBands = mode % 10 != 0,
|
|
1012
|
+
isOctaves = isBands && this._frequencyScale == SCALE_LOG,
|
|
1013
|
+
isLeds = this._showLeds && isBands && ! isRadial,
|
|
1014
|
+
isLumi = this._lumiBars && isBands && ! isRadial,
|
|
1015
|
+
isAlpha = this._alphaBars && ! isLumi && mode != 10,
|
|
1016
|
+
isOutline = this._outlineBars && isBands && ! isLumi && ! isLeds,
|
|
1017
|
+
isRound = this._roundBars && isBands && ! isLumi && ! isLeds,
|
|
1018
|
+
noLedGap = chLayout != CHANNEL_VERTICAL || this._reflexRatio > 0 && ! isLumi,
|
|
1019
|
+
|
|
1020
|
+
// COMPUTE AUXILIARY VALUES
|
|
1021
|
+
|
|
1022
|
+
// channelHeight is the total canvas height dedicated to each channel, including the reflex area, if any)
|
|
1023
|
+
channelHeight = canvas.height - ( isDual && ! isLeds ? .5 : 0 ) >> isDual,
|
|
1024
|
+
// analyzerHeight is the effective height used to render the analyzer, excluding the reflex area
|
|
1025
|
+
analyzerHeight = channelHeight * ( isLumi || isRadial ? 1 : 1 - this._reflexRatio ) | 0,
|
|
1026
|
+
|
|
1027
|
+
analyzerWidth = canvas.width - centerX * ( this._mirror != 0 ),
|
|
1028
|
+
|
|
1029
|
+
// channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
|
|
1030
|
+
// TODO: improve this, make it configurable?
|
|
1031
|
+
channelGap = isDual ? canvas.height - channelHeight * 2 : 0,
|
|
1032
|
+
|
|
1033
|
+
initialX = centerX * ( this._mirror == -1 && ! isRadial ),
|
|
1034
|
+
radius = Math.min( canvas.width, canvas.height ) * ( chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0;
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* CREATE ANALYZER BANDS
|
|
1038
|
+
*
|
|
1039
|
+
* USES:
|
|
1040
|
+
* analyzerWidth
|
|
1041
|
+
* initialX
|
|
1042
|
+
* isBands
|
|
1043
|
+
* isOctaves
|
|
1044
|
+
*
|
|
1045
|
+
* GENERATES:
|
|
1046
|
+
* bars (populates this._bars)
|
|
1047
|
+
* bardWidth
|
|
1048
|
+
* scaleMin
|
|
1049
|
+
* unitWidth
|
|
1050
|
+
*/
|
|
1051
|
+
|
|
980
1052
|
// helper function
|
|
981
1053
|
// bar object: { posX, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi, peak, hold, value }
|
|
982
|
-
const barsPush
|
|
1054
|
+
const barsPush = args => bars.push( { ...args, peak: [0,0], hold: [0], value: [0] } );
|
|
983
1055
|
|
|
984
1056
|
/*
|
|
985
1057
|
A simple interpolation is used to obtain an approximate amplitude value for any given frequency,
|
|
@@ -1012,15 +1084,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1012
1084
|
return [ bin, ratio ];
|
|
1013
1085
|
}
|
|
1014
1086
|
|
|
1015
|
-
|
|
1016
|
-
initialX = this._initialX,
|
|
1017
|
-
isAnsiBands = this._ansiBands,
|
|
1018
|
-
maxFreq = this._maxFreq,
|
|
1019
|
-
minFreq = this._minFreq;
|
|
1020
|
-
|
|
1021
|
-
let scaleMin, unitWidth;
|
|
1087
|
+
let barWidth, scaleMin, unitWidth;
|
|
1022
1088
|
|
|
1023
|
-
if (
|
|
1089
|
+
if ( isOctaves ) {
|
|
1024
1090
|
// helper function to round a value to a given number of significant digits
|
|
1025
1091
|
// `atLeast` set to true prevents reducing the number of integer significant digits
|
|
1026
1092
|
const roundSD = ( value, digits, atLeast ) => +value.toPrecision( atLeast ? Math.max( digits, 1 + Math.log10( value ) | 0 ) : digits );
|
|
@@ -1075,9 +1141,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1075
1141
|
currFreq *= bandWidth;
|
|
1076
1142
|
} while ( currFreq <= maxFreq );
|
|
1077
1143
|
|
|
1078
|
-
|
|
1144
|
+
barWidth = analyzerWidth / bars.length;
|
|
1079
1145
|
|
|
1080
|
-
bars.forEach( ( bar, index ) => bar.posX = initialX + index *
|
|
1146
|
+
bars.forEach( ( bar, index ) => bar.posX = initialX + index * barWidth );
|
|
1081
1147
|
|
|
1082
1148
|
const firstBar = bars[0],
|
|
1083
1149
|
lastBar = bars[ bars.length - 1 ];
|
|
@@ -1097,7 +1163,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1097
1163
|
[ lastBar.binHi, lastBar.ratioHi ] = calcRatio( maxFreq );
|
|
1098
1164
|
}
|
|
1099
1165
|
}
|
|
1100
|
-
else if (
|
|
1166
|
+
else if ( isBands ) { // a bands mode is selected, but frequency scale is not logarithmic
|
|
1101
1167
|
|
|
1102
1168
|
const bands = [0,24,12,8,6,4,3,2,1][ this._mode ] * 10;
|
|
1103
1169
|
|
|
@@ -1112,15 +1178,15 @@ export default class AudioMotionAnalyzer {
|
|
|
1112
1178
|
}
|
|
1113
1179
|
}
|
|
1114
1180
|
|
|
1115
|
-
|
|
1181
|
+
barWidth = analyzerWidth / bands;
|
|
1116
1182
|
|
|
1117
1183
|
scaleMin = this._freqScaling( minFreq );
|
|
1118
1184
|
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
1119
1185
|
|
|
1120
|
-
for ( let i = 0, posX = 0; i < bands; i++, posX +=
|
|
1186
|
+
for ( let i = 0, posX = 0; i < bands; i++, posX += barWidth ) {
|
|
1121
1187
|
const freqLo = invFreqScaling( scaleMin + posX / unitWidth ),
|
|
1122
|
-
freq = invFreqScaling( scaleMin + ( posX +
|
|
1123
|
-
freqHi = invFreqScaling( scaleMin + ( posX +
|
|
1188
|
+
freq = invFreqScaling( scaleMin + ( posX + barWidth / 2 ) / unitWidth ),
|
|
1189
|
+
freqHi = invFreqScaling( scaleMin + ( posX + barWidth ) / unitWidth ),
|
|
1124
1190
|
[ binLo, ratioLo ] = calcRatio( freqLo ),
|
|
1125
1191
|
[ binHi, ratioHi ] = calcRatio( freqHi );
|
|
1126
1192
|
|
|
@@ -1129,7 +1195,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1129
1195
|
|
|
1130
1196
|
}
|
|
1131
1197
|
else { // Discrete frequencies modes
|
|
1132
|
-
|
|
1198
|
+
barWidth = 1;
|
|
1133
1199
|
|
|
1134
1200
|
scaleMin = this._freqScaling( minFreq );
|
|
1135
1201
|
unitWidth = analyzerWidth / ( this._freqScaling( maxFreq ) - scaleMin );
|
|
@@ -1157,78 +1223,130 @@ export default class AudioMotionAnalyzer {
|
|
|
1157
1223
|
}
|
|
1158
1224
|
}
|
|
1159
1225
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1226
|
+
/**
|
|
1227
|
+
* COMPUTE ATTRIBUTES FOR THE LED BARS
|
|
1228
|
+
*
|
|
1229
|
+
* USES:
|
|
1230
|
+
* analyzerHeight
|
|
1231
|
+
* barWidth
|
|
1232
|
+
* noLedGap
|
|
1233
|
+
*
|
|
1234
|
+
* GENERATES:
|
|
1235
|
+
* spaceH
|
|
1236
|
+
* spaceV
|
|
1237
|
+
* this._leds
|
|
1238
|
+
*/
|
|
1239
|
+
|
|
1240
|
+
let spaceH = 0,
|
|
1241
|
+
spaceV = 0;
|
|
1242
|
+
|
|
1243
|
+
if ( isLeds ) {
|
|
1244
|
+
// adjustment for high pixel-ratio values on low-resolution screens (Android TV)
|
|
1245
|
+
const dPR = this._pixelRatio / ( window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1 );
|
|
1246
|
+
|
|
1247
|
+
const params = [ [],
|
|
1248
|
+
[ 128, 3, .45 ], // mode 1
|
|
1249
|
+
[ 128, 4, .225 ], // mode 2
|
|
1250
|
+
[ 96, 6, .225 ], // mode 3
|
|
1251
|
+
[ 80, 6, .225 ], // mode 4
|
|
1252
|
+
[ 80, 6, .125 ], // mode 5
|
|
1253
|
+
[ 64, 6, .125 ], // mode 6
|
|
1254
|
+
[ 48, 8, .125 ], // mode 7
|
|
1255
|
+
[ 24, 16, .125 ], // mode 8
|
|
1256
|
+
];
|
|
1257
|
+
|
|
1258
|
+
// use custom LED parameters if set, or the default parameters for the current mode
|
|
1259
|
+
const customParams = this._ledParams,
|
|
1260
|
+
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ mode ];
|
|
1261
|
+
|
|
1262
|
+
let ledCount, maxHeight = analyzerHeight;
|
|
1263
|
+
|
|
1264
|
+
if ( customParams ) {
|
|
1265
|
+
const minHeight = 2 * dPR;
|
|
1266
|
+
let blockHeight;
|
|
1267
|
+
ledCount = maxLeds + 1;
|
|
1268
|
+
do {
|
|
1269
|
+
ledCount--;
|
|
1270
|
+
blockHeight = maxHeight / ledCount / ( 1 + spaceVRatio );
|
|
1271
|
+
spaceV = blockHeight * spaceVRatio;
|
|
1272
|
+
} while ( ( blockHeight < minHeight || spaceV < minHeight ) && ledCount > 1 );
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
// calculate vertical spacing - aim for the reference ratio, but make sure it's at least 2px
|
|
1276
|
+
const refRatio = 540 / spaceVRatio;
|
|
1277
|
+
spaceV = Math.min( spaceVRatio * dPR, Math.max( 2, maxHeight / refRatio + .1 | 0 ) );
|
|
1278
|
+
}
|
|
1166
1279
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1280
|
+
// remove the extra spacing below the last line of LEDs
|
|
1281
|
+
if ( noLedGap )
|
|
1282
|
+
maxHeight += spaceV;
|
|
1169
1283
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1284
|
+
// recalculate the number of leds, considering the effective spaceV
|
|
1285
|
+
if ( ! customParams )
|
|
1286
|
+
ledCount = Math.min( maxLeds, maxHeight / ( spaceV * 2 ) | 0 );
|
|
1173
1287
|
|
|
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;
|
|
1288
|
+
spaceH = spaceHRatio >= 1 ? spaceHRatio : barWidth * spaceHRatio;
|
|
1180
1289
|
|
|
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 );
|
|
1290
|
+
this._leds = [
|
|
1291
|
+
ledCount,
|
|
1292
|
+
spaceH,
|
|
1293
|
+
spaceV,
|
|
1294
|
+
maxHeight / ledCount - spaceV // ledHeight
|
|
1295
|
+
];
|
|
1211
1296
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1297
|
+
|
|
1298
|
+
// COMPUTE ADDITIONAL BAR POSITIONING, ACCORDING TO THE CURRENT SETTINGS
|
|
1299
|
+
// uses: barSpace, barWidth, spaceH
|
|
1300
|
+
|
|
1301
|
+
const barSpacePx = Math.min( barWidth - 1, barSpace * ( barSpace > 0 && barSpace < 1 ? barWidth : 1 ) );
|
|
1302
|
+
|
|
1303
|
+
if ( isBands )
|
|
1304
|
+
barWidth -= Math.max( isLeds ? spaceH : 0, barSpacePx );
|
|
1305
|
+
|
|
1306
|
+
bars.forEach( ( bar, index ) => {
|
|
1307
|
+
let posX = bar.posX,
|
|
1308
|
+
width = barWidth;
|
|
1309
|
+
|
|
1310
|
+
// in bands modes we need to update bar.posX to account for bar/led spacing
|
|
1311
|
+
|
|
1312
|
+
if ( isBands ) {
|
|
1313
|
+
if ( barSpace == 0 && ! isLeds ) {
|
|
1314
|
+
// when barSpace == 0 use integer values for perfect gapless positioning
|
|
1315
|
+
posX |= 0;
|
|
1316
|
+
width |= 0;
|
|
1317
|
+
if ( index > 0 && posX > bars[ index - 1 ].posX + bars[ index - 1 ].width ) {
|
|
1318
|
+
posX--;
|
|
1319
|
+
width++;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
else
|
|
1323
|
+
posX += Math.max( ( isLeds ? spaceH : 0 ), barSpacePx ) / 2;
|
|
1324
|
+
|
|
1325
|
+
bar.posX = posX; // update
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
bar.barCenter = posX + ( barWidth == 1 ? 0 : width / 2 );
|
|
1329
|
+
bar.width = width;
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// COMPUTE CHANNEL COORDINATES (uses spaceV)
|
|
1333
|
+
|
|
1334
|
+
const channelCoords = [];
|
|
1335
|
+
for ( const channel of [0,1] ) {
|
|
1336
|
+
const channelTop = chLayout == CHANNEL_VERTICAL ? ( channelHeight + channelGap ) * channel : 0,
|
|
1337
|
+
channelBottom = channelTop + channelHeight,
|
|
1338
|
+
analyzerBottom = channelTop + analyzerHeight - ( ! isLeds || noLedGap ? 0 : spaceV );
|
|
1339
|
+
|
|
1340
|
+
channelCoords.push( { channelTop, channelBottom, analyzerBottom } );
|
|
1216
1341
|
}
|
|
1217
1342
|
|
|
1218
|
-
//
|
|
1219
|
-
if ( this._maximizeLeds )
|
|
1220
|
-
analyzerHeight += spaceV;
|
|
1343
|
+
// SAVE INTERNAL PROPERTIES
|
|
1221
1344
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
ledCount = Math.min( maxLeds, analyzerHeight / ( spaceV * 2 ) | 0 );
|
|
1345
|
+
this._aux = { analyzerHeight, analyzerWidth, channelCoords, channelHeight, channelGap, initialX, radius, scaleMin, unitWidth };
|
|
1346
|
+
this._flg = { isAlpha, isBands, isLeds, isLumi, isOctaves, isOutline, isRound, noLedGap };
|
|
1225
1347
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
spaceHRatio >= 1 ? spaceHRatio : this._barWidth * spaceHRatio, // spaceH
|
|
1229
|
-
spaceV,
|
|
1230
|
-
analyzerHeight / ledCount - spaceV // ledHeight
|
|
1231
|
-
];
|
|
1348
|
+
// generate the X-axis and radial scales
|
|
1349
|
+
this._createScales();
|
|
1232
1350
|
}
|
|
1233
1351
|
|
|
1234
1352
|
/**
|
|
@@ -1238,7 +1356,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1238
1356
|
if ( ! this._ready )
|
|
1239
1357
|
return;
|
|
1240
1358
|
|
|
1241
|
-
const analyzerWidth = this.
|
|
1359
|
+
const { analyzerWidth, initialX, radius, scaleMin, unitWidth } = this._aux,
|
|
1242
1360
|
canvas = this._canvasCtx.canvas,
|
|
1243
1361
|
scaleX = this._scaleX,
|
|
1244
1362
|
scaleR = this._scaleR,
|
|
@@ -1246,20 +1364,22 @@ export default class AudioMotionAnalyzer {
|
|
|
1246
1364
|
canvasR = scaleR.canvas,
|
|
1247
1365
|
freqLabels = [],
|
|
1248
1366
|
frequencyScale= this._frequencyScale,
|
|
1249
|
-
initialX = this._initialX,
|
|
1250
|
-
isDual = this._chLayout == CHANNEL_VERTICAL,
|
|
1251
|
-
isMirror = this._mirror,
|
|
1252
1367
|
isNoteLabels = this._noteLabels,
|
|
1368
|
+
isRadial = this._radial,
|
|
1369
|
+
isVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
1370
|
+
mirror = this._mirror,
|
|
1253
1371
|
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1254
|
-
scaleHeight = Math.min( canvas.width, canvas.height )
|
|
1372
|
+
scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
|
|
1255
1373
|
fontSizeX = canvasX.height >> 1,
|
|
1256
1374
|
fontSizeR = scaleHeight >> 1,
|
|
1375
|
+
labelWidthX = fontSizeX * ( isNoteLabels ? .7 : 1.5 ),
|
|
1376
|
+
labelWidthR = fontSizeR * ( isNoteLabels ? 1 : 2 ),
|
|
1257
1377
|
root12 = 2 ** ( 1 / 12 );
|
|
1258
1378
|
|
|
1259
1379
|
if ( ! isNoteLabels && ( this._ansiBands || frequencyScale != SCALE_LOG ) ) {
|
|
1260
1380
|
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
1261
1381
|
if ( frequencyScale == SCALE_LINEAR )
|
|
1262
|
-
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3 );
|
|
1382
|
+
freqLabels.push( 6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3 );
|
|
1263
1383
|
else
|
|
1264
1384
|
freqLabels.push( 8e3, 16e3 );
|
|
1265
1385
|
}
|
|
@@ -1270,7 +1390,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1270
1390
|
if ( freq >= this._minFreq && freq <= this._maxFreq ) {
|
|
1271
1391
|
const pitch = scale[ note ],
|
|
1272
1392
|
isC = pitch == 'C';
|
|
1273
|
-
if ( ( pitch && isNoteLabels && !
|
|
1393
|
+
if ( ( pitch && isNoteLabels && ! mirror ) || isC )
|
|
1274
1394
|
freqLabels.push( isNoteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
|
|
1275
1395
|
}
|
|
1276
1396
|
freq *= root12;
|
|
@@ -1278,24 +1398,21 @@ export default class AudioMotionAnalyzer {
|
|
|
1278
1398
|
}
|
|
1279
1399
|
}
|
|
1280
1400
|
|
|
1281
|
-
// in radial
|
|
1282
|
-
canvasR.width = canvasR.height = (
|
|
1401
|
+
// in radial dual-vertical layout, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
|
|
1402
|
+
canvasR.width = canvasR.height = ( radius << 1 ) + ( isVertical * scaleHeight );
|
|
1283
1403
|
|
|
1284
|
-
const
|
|
1285
|
-
radialY =
|
|
1404
|
+
const centerR = canvasR.width >> 1,
|
|
1405
|
+
radialY = centerR - scaleHeight * .7; // vertical position of text labels in the circular scale
|
|
1286
1406
|
|
|
1287
1407
|
// helper function
|
|
1288
1408
|
const radialLabel = ( x, label ) => {
|
|
1289
|
-
if ( isNoteLabels && ! isDual && ! ['C','E','G'].includes( label[0] ) )
|
|
1290
|
-
return;
|
|
1291
|
-
|
|
1292
1409
|
const angle = TAU * ( x / canvas.width ),
|
|
1293
1410
|
adjAng = angle - HALF_PI, // rotate angles so 0 is at the top
|
|
1294
1411
|
posX = radialY * Math.cos( adjAng ),
|
|
1295
1412
|
posY = radialY * Math.sin( adjAng );
|
|
1296
1413
|
|
|
1297
1414
|
scaleR.save();
|
|
1298
|
-
scaleR.translate(
|
|
1415
|
+
scaleR.translate( centerR + posX, centerR + posY );
|
|
1299
1416
|
scaleR.rotate( angle );
|
|
1300
1417
|
scaleR.fillText( label, 0, 0 );
|
|
1301
1418
|
scaleR.restore();
|
|
@@ -1307,7 +1424,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1307
1424
|
scaleX.fillStyle = scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
|
|
1308
1425
|
scaleX.fillRect( 0, 0, canvasX.width, canvasX.height );
|
|
1309
1426
|
|
|
1310
|
-
scaleR.arc(
|
|
1427
|
+
scaleR.arc( centerR, centerR, centerR - scaleHeight / 2, 0, TAU );
|
|
1311
1428
|
scaleR.lineWidth = scaleHeight;
|
|
1312
1429
|
scaleR.stroke();
|
|
1313
1430
|
|
|
@@ -1316,32 +1433,49 @@ export default class AudioMotionAnalyzer {
|
|
|
1316
1433
|
scaleR.font = `${ fontSizeR }px ${FONT_FAMILY}`;
|
|
1317
1434
|
scaleX.textAlign = scaleR.textAlign = 'center';
|
|
1318
1435
|
|
|
1319
|
-
let prevX =
|
|
1436
|
+
let prevX = -labelWidthX / 4,
|
|
1437
|
+
prevR = -labelWidthR;
|
|
1320
1438
|
|
|
1321
1439
|
for ( const item of freqLabels ) {
|
|
1322
1440
|
const [ freq, label ] = Array.isArray( item ) ? item : [ item, item < 1e3 ? item | 0 : `${ ( item / 100 | 0 ) / 10 }k` ],
|
|
1323
|
-
x =
|
|
1441
|
+
x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
|
|
1324
1442
|
y = canvasX.height * .75,
|
|
1325
1443
|
isC = label[0] == 'C',
|
|
1326
|
-
maxW = fontSizeX * ( isNoteLabels && !
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1444
|
+
maxW = fontSizeX * ( isNoteLabels && ! mirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1445
|
+
|
|
1446
|
+
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1447
|
+
scaleX.fillStyle = scaleR.fillStyle = isC && ! mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1448
|
+
|
|
1449
|
+
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1450
|
+
if ( isNoteLabels ) {
|
|
1451
|
+
let allowedLabels = ['C'];
|
|
1452
|
+
if ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 250 ) ||
|
|
1453
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 125 || freq > 1e3 ) ) )
|
|
1454
|
+
allowedLabels.push('G');
|
|
1455
|
+
if ( frequencyScale == SCALE_LOG || freq > 4e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ||
|
|
1456
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale != SCALE_LINEAR && freq > 250 || freq > 2e3 ) ) )
|
|
1457
|
+
allowedLabels.push('E');
|
|
1458
|
+
if ( frequencyScale == SCALE_LINEAR && freq > 4e3 ||
|
|
1459
|
+
( ( ! isRadial || isVertical ) && ( frequencyScale == SCALE_LOG || freq > 2e3 || ( frequencyScale != SCALE_LINEAR && freq > 500 ) ) ) )
|
|
1460
|
+
allowedLabels.push('D','F','A','B');
|
|
1461
|
+
if ( ! allowedLabels.includes( label[0] ) )
|
|
1462
|
+
continue; // skip this label
|
|
1463
|
+
}
|
|
1331
1464
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1465
|
+
// linear scale
|
|
1466
|
+
if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
|
|
1467
|
+
scaleX.fillText( label, initialX + x, y, maxW );
|
|
1468
|
+
if ( mirror && ( x > labelWidthX || mirror == 1 ) )
|
|
1469
|
+
scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
|
|
1470
|
+
prevX = x + Math.min( maxW, scaleX.measureText( label ).width ) / 2;
|
|
1471
|
+
}
|
|
1338
1472
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1473
|
+
// radial scale
|
|
1474
|
+
if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
|
|
1475
|
+
radialLabel( x, label );
|
|
1476
|
+
if ( mirror && ( x > labelWidthR || mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
|
|
1477
|
+
radialLabel( -x, label );
|
|
1478
|
+
prevR = x;
|
|
1345
1479
|
}
|
|
1346
1480
|
}
|
|
1347
1481
|
}
|
|
@@ -1351,48 +1485,132 @@ export default class AudioMotionAnalyzer {
|
|
|
1351
1485
|
* this is called 60 times per second by requestAnimationFrame()
|
|
1352
1486
|
*/
|
|
1353
1487
|
_draw( timestamp ) {
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1488
|
+
const { isAlpha, isBands, isLeds, isLumi,
|
|
1489
|
+
isOctaves, isOutline, isRound, noLedGap } = this._flg,
|
|
1356
1490
|
ctx = this._canvasCtx,
|
|
1357
1491
|
canvas = ctx.canvas,
|
|
1358
1492
|
canvasX = this._scaleX.canvas,
|
|
1359
1493
|
canvasR = this._scaleR.canvas,
|
|
1360
1494
|
canvasGradients= this._canvasGradients,
|
|
1495
|
+
centerX = canvas.width >> 1,
|
|
1496
|
+
centerY = canvas.height >> 1,
|
|
1497
|
+
colorMode = this._colorMode,
|
|
1361
1498
|
energy = this._energy,
|
|
1362
1499
|
fillAlpha = this.fillAlpha,
|
|
1363
1500
|
mode = this._mode,
|
|
1364
|
-
isAlphaBars = this._isAlphaBars,
|
|
1365
|
-
isLedDisplay = this._isLedDisplay,
|
|
1366
1501
|
isLinear = this._linearAmplitude,
|
|
1367
|
-
isLumiBars = this._isLumiBars,
|
|
1368
|
-
isBandsMode = this._isBandsMode,
|
|
1369
|
-
isOutline = this._isOutline,
|
|
1370
1502
|
isOverlay = this.overlay,
|
|
1371
1503
|
isRadial = this._radial,
|
|
1504
|
+
isTrueLeds = isLeds && this._trueLeds && colorMode == COLOR_GRADIENT,
|
|
1372
1505
|
channelLayout = this._chLayout,
|
|
1373
|
-
lineWidth =
|
|
1506
|
+
lineWidth = this._lineWidth,
|
|
1374
1507
|
mirrorMode = this._mirror,
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
analyzerWidth = isRadial ? canvas.width : this._analyzerWidth,
|
|
1379
|
-
initialX = this._initialX,
|
|
1508
|
+
{ analyzerHeight, channelCoords,
|
|
1509
|
+
channelHeight, channelGap, initialX, radius } = this._aux,
|
|
1510
|
+
analyzerWidth = isRadial ? canvas.width : this._aux.analyzerWidth,
|
|
1380
1511
|
finalX = initialX + analyzerWidth,
|
|
1381
|
-
centerX = canvas.width >> 1,
|
|
1382
|
-
centerY = canvas.height >> 1,
|
|
1383
|
-
radius = this._radius,
|
|
1384
1512
|
showBgColor = this.showBgColor,
|
|
1385
1513
|
maxBarHeight = isRadial ? Math.min( centerX, centerY ) - radius : analyzerHeight,
|
|
1386
1514
|
maxdB = this.maxDecibels,
|
|
1387
1515
|
mindB = this.minDecibels,
|
|
1388
1516
|
dbRange = maxdB - mindB,
|
|
1389
1517
|
useCanvas = this.useCanvas,
|
|
1390
|
-
weightingFilter= this._weightingFilter
|
|
1518
|
+
weightingFilter= this._weightingFilter,
|
|
1519
|
+
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1391
1520
|
|
|
1392
1521
|
if ( energy.val > 0 )
|
|
1393
1522
|
this._spinAngle += this._spinSpeed * RPM;
|
|
1394
1523
|
|
|
1395
|
-
|
|
1524
|
+
/* HELPER FUNCTIONS */
|
|
1525
|
+
|
|
1526
|
+
// create Reflex effect
|
|
1527
|
+
const doReflex = channel => {
|
|
1528
|
+
if ( this._reflexRatio > 0 && ! isLumi ) {
|
|
1529
|
+
let posY, height;
|
|
1530
|
+
if ( this.reflexFit || channelLayout == CHANNEL_VERTICAL ) { // always fit reflex in vertical stereo mode
|
|
1531
|
+
posY = channelLayout == CHANNEL_VERTICAL && channel == 0 ? channelHeight + channelGap : 0;
|
|
1532
|
+
height = channelHeight - analyzerHeight;
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
posY = canvas.height - analyzerHeight * 2;
|
|
1536
|
+
height = analyzerHeight;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
ctx.save();
|
|
1540
|
+
|
|
1541
|
+
// set alpha and brightness for the reflection
|
|
1542
|
+
ctx.globalAlpha = this.reflexAlpha;
|
|
1543
|
+
if ( this.reflexBright != 1 )
|
|
1544
|
+
ctx.filter = `brightness(${this.reflexBright})`;
|
|
1545
|
+
|
|
1546
|
+
// create the reflection
|
|
1547
|
+
ctx.setTransform( 1, 0, 0, -1, 0, canvas.height );
|
|
1548
|
+
ctx.drawImage( canvas, 0, channelCoords[ channel ].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height );
|
|
1549
|
+
|
|
1550
|
+
ctx.restore();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// draw scale on X-axis
|
|
1555
|
+
const drawScaleX = () => {
|
|
1556
|
+
if ( this.showScaleX ) {
|
|
1557
|
+
if ( isRadial ) {
|
|
1558
|
+
ctx.save();
|
|
1559
|
+
ctx.translate( centerX, centerY );
|
|
1560
|
+
if ( this._spinSpeed )
|
|
1561
|
+
ctx.rotate( this._spinAngle + HALF_PI );
|
|
1562
|
+
ctx.drawImage( canvasR, -canvasR.width >> 1, -canvasR.width >> 1 );
|
|
1563
|
+
ctx.restore();
|
|
1564
|
+
}
|
|
1565
|
+
else
|
|
1566
|
+
ctx.drawImage( canvasX, 0, canvas.height - canvasX.height );
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// draw scale on Y-axis
|
|
1571
|
+
const drawScaleY = channelTop => {
|
|
1572
|
+
const scaleWidth = canvasX.height,
|
|
1573
|
+
fontSize = scaleWidth >> 1,
|
|
1574
|
+
max = isLinear ? 100 : maxdB,
|
|
1575
|
+
min = isLinear ? 0 : mindB,
|
|
1576
|
+
incr = isLinear ? 20 : 5,
|
|
1577
|
+
interval = analyzerHeight / ( max - min );
|
|
1578
|
+
|
|
1579
|
+
ctx.save();
|
|
1580
|
+
ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1581
|
+
ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1582
|
+
ctx.textAlign = 'right';
|
|
1583
|
+
ctx.lineWidth = 1;
|
|
1584
|
+
|
|
1585
|
+
for ( let val = max; val > min; val -= incr ) {
|
|
1586
|
+
const posY = channelTop + ( max - val ) * interval,
|
|
1587
|
+
even = ( val % 2 == 0 ) | 0;
|
|
1588
|
+
|
|
1589
|
+
if ( even ) {
|
|
1590
|
+
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1591
|
+
if ( mirrorMode != -1 )
|
|
1592
|
+
ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1593
|
+
if ( mirrorMode != 1 )
|
|
1594
|
+
ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
|
|
1595
|
+
ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1596
|
+
ctx.setLineDash([2,4]);
|
|
1597
|
+
ctx.lineDashOffset = 0;
|
|
1598
|
+
}
|
|
1599
|
+
else {
|
|
1600
|
+
ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1601
|
+
ctx.setLineDash([2,8]);
|
|
1602
|
+
ctx.lineDashOffset = 1;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
ctx.beginPath();
|
|
1606
|
+
ctx.moveTo( initialX + scaleWidth * even * ( mirrorMode != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1607
|
+
ctx.lineTo( finalX - scaleWidth * even * ( mirrorMode != 1 ), ~~posY + .5 );
|
|
1608
|
+
ctx.stroke();
|
|
1609
|
+
}
|
|
1610
|
+
ctx.restore();
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
1396
1614
|
const weightingdB = freq => {
|
|
1397
1615
|
const f2 = freq ** 2,
|
|
1398
1616
|
SQ20_6 = 424.36,
|
|
@@ -1430,6 +1648,15 @@ export default class AudioMotionAnalyzer {
|
|
|
1430
1648
|
return 0; // unknown filter
|
|
1431
1649
|
}
|
|
1432
1650
|
|
|
1651
|
+
// draws (stroke) a bar from x,y1 to x,y2
|
|
1652
|
+
const strokeBar = ( x, y1, y2 ) => {
|
|
1653
|
+
ctx.beginPath();
|
|
1654
|
+
ctx.moveTo( x, y1 );
|
|
1655
|
+
ctx.lineTo( x, y2 );
|
|
1656
|
+
ctx.stroke();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// conditionally strokes current path on canvas
|
|
1433
1660
|
const strokeIf = flag => {
|
|
1434
1661
|
if ( flag && lineWidth ) {
|
|
1435
1662
|
const alpha = ctx.globalAlpha;
|
|
@@ -1439,52 +1666,107 @@ export default class AudioMotionAnalyzer {
|
|
|
1439
1666
|
}
|
|
1440
1667
|
}
|
|
1441
1668
|
|
|
1442
|
-
//
|
|
1669
|
+
// converts a given X-coordinate to its corresponding angle in radial mode
|
|
1670
|
+
const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
|
|
1671
|
+
|
|
1672
|
+
// converts planar X,Y coordinates to radial coordinates
|
|
1443
1673
|
const radialXY = ( x, y, dir ) => {
|
|
1444
1674
|
const height = radius + y,
|
|
1445
|
-
angle =
|
|
1446
|
-
|
|
1675
|
+
angle = getAngle( x, dir );
|
|
1447
1676
|
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
1448
1677
|
}
|
|
1449
1678
|
|
|
1450
|
-
//
|
|
1679
|
+
// draws a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
1451
1680
|
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1452
1681
|
ctx.beginPath();
|
|
1453
1682
|
for ( const dir of ( mirrorMode ? [1,-1] : [1] ) ) {
|
|
1683
|
+
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1454
1684
|
ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1455
1685
|
ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1456
|
-
|
|
1686
|
+
if ( isRound )
|
|
1687
|
+
ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle );
|
|
1688
|
+
else
|
|
1689
|
+
ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1457
1690
|
ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1691
|
+
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1692
|
+
ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, true );
|
|
1458
1693
|
}
|
|
1459
|
-
|
|
1460
1694
|
strokeIf( stroke );
|
|
1461
1695
|
ctx.fill();
|
|
1462
1696
|
}
|
|
1463
1697
|
|
|
1464
|
-
//
|
|
1465
|
-
const
|
|
1466
|
-
const ledPosY = height => Math.max( 0, ( height * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1698
|
+
// converts a value in [0;1] range to a height in pixels that fits into the current LED elements
|
|
1699
|
+
const ledPosY = value => Math.max( 0, ( value * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1467
1700
|
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1701
|
+
// update energy information
|
|
1702
|
+
const updateEnergy = newVal => {
|
|
1703
|
+
energy.val = newVal;
|
|
1704
|
+
if ( newVal >= energy.peak ) {
|
|
1705
|
+
energy.peak = newVal;
|
|
1706
|
+
energy.hold = 30;
|
|
1707
|
+
}
|
|
1708
|
+
else {
|
|
1709
|
+
if ( energy.hold > 0 )
|
|
1710
|
+
energy.hold--;
|
|
1711
|
+
else if ( energy.peak > 0 )
|
|
1712
|
+
energy.peak *= ( 30 + energy.hold-- ) / 30; // decay (drops to zero in 30 frames)
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1471
1715
|
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
|
|
1716
|
+
// calculate and display (if enabled) the current frame rate
|
|
1717
|
+
const updateFPS = () => {
|
|
1718
|
+
this._frame++;
|
|
1719
|
+
const elapsed = timestamp - this._time;
|
|
1720
|
+
|
|
1721
|
+
if ( elapsed >= 1000 ) {
|
|
1722
|
+
this._fps = this._frame / ( elapsed / 1000 );
|
|
1723
|
+
this._frame = 0;
|
|
1724
|
+
this._time = timestamp;
|
|
1725
|
+
}
|
|
1726
|
+
if ( this.showFPS ) {
|
|
1727
|
+
const size = canvasX.height;
|
|
1728
|
+
ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
1729
|
+
ctx.fillStyle = FPS_COLOR;
|
|
1730
|
+
ctx.textAlign = 'right';
|
|
1731
|
+
ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/* MAIN FUNCTION */
|
|
1475
1736
|
|
|
1476
1737
|
let currentEnergy = 0;
|
|
1477
1738
|
|
|
1478
|
-
const
|
|
1739
|
+
const bars = this._bars,
|
|
1740
|
+
nBars = bars.length,
|
|
1479
1741
|
nChannels = channelLayout == CHANNEL_SINGLE ? 1 : 2;
|
|
1480
1742
|
|
|
1481
1743
|
for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1482
1744
|
|
|
1483
|
-
const channelTop
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1745
|
+
const { channelTop, channelBottom, analyzerBottom } = channelCoords[ channel ],
|
|
1746
|
+
channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
1747
|
+
colorStops = channelGradient.colorStops,
|
|
1748
|
+
colorCount = colorStops.length,
|
|
1749
|
+
bgColor = ( ! showBgColor || isLeds && ! isOverlay ) ? '#000' : channelGradient.bgColor,
|
|
1750
|
+
mustClear = channel == 0 || ! isRadial && channelLayout != CHANNEL_COMBINED;
|
|
1751
|
+
|
|
1752
|
+
// helper function for FFT data interpolation (uses fftData)
|
|
1753
|
+
const interpolate = ( bin, ratio ) => {
|
|
1754
|
+
const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
|
|
1755
|
+
return isNaN( value ) ? -Infinity : value;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// set fillStyle and strokeStyle according to current colorMode (uses: channel, colorStops, colorCount)
|
|
1759
|
+
const setBarColor = ( value = 0, barIndex = 0 ) => {
|
|
1760
|
+
let color;
|
|
1761
|
+
// for mode 10, always use the channel gradient (ignore colorMode)
|
|
1762
|
+
if ( ( colorMode == COLOR_GRADIENT && ! isTrueLeds ) || mode == 10 )
|
|
1763
|
+
color = canvasGradients[ channel ];
|
|
1764
|
+
else {
|
|
1765
|
+
const selectedIndex = colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
|
|
1766
|
+
color = colorStops[ selectedIndex ].color;
|
|
1767
|
+
}
|
|
1768
|
+
ctx.fillStyle = ctx.strokeStyle = color;
|
|
1769
|
+
}
|
|
1488
1770
|
|
|
1489
1771
|
if ( useCanvas ) {
|
|
1490
1772
|
// clear the channel area, if in overlay mode
|
|
@@ -1506,60 +1788,25 @@ export default class AudioMotionAnalyzer {
|
|
|
1506
1788
|
ctx.globalAlpha = 1;
|
|
1507
1789
|
}
|
|
1508
1790
|
|
|
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
|
-
}
|
|
1791
|
+
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
1792
|
+
if ( this.showScaleY && ! isLumi && ! isRadial && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) )
|
|
1793
|
+
drawScaleY( channelTop );
|
|
1552
1794
|
|
|
1553
1795
|
// set line width and dash for LEDs effect
|
|
1554
|
-
if (
|
|
1796
|
+
if ( isLeds ) {
|
|
1555
1797
|
ctx.setLineDash( [ ledHeight, ledSpaceV ] );
|
|
1556
|
-
ctx.lineWidth = width;
|
|
1798
|
+
ctx.lineWidth = bars[0].width;
|
|
1557
1799
|
}
|
|
1558
1800
|
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.
|
|
1801
|
+
ctx.lineWidth = isOutline ? Math.min( lineWidth, bars[0].width / 2 ) : lineWidth;
|
|
1802
|
+
|
|
1803
|
+
// set clip region
|
|
1804
|
+
ctx.save();
|
|
1805
|
+
if ( ! isRadial ) {
|
|
1806
|
+
const channelRegion = new Path2D();
|
|
1807
|
+
channelRegion.rect( 0, channelTop, canvas.width, analyzerHeight );
|
|
1808
|
+
ctx.clip( channelRegion );
|
|
1809
|
+
}
|
|
1563
1810
|
} // if ( useCanvas )
|
|
1564
1811
|
|
|
1565
1812
|
// get a new array of data from the FFT
|
|
@@ -1570,12 +1817,6 @@ export default class AudioMotionAnalyzer {
|
|
|
1570
1817
|
if ( weightingFilter )
|
|
1571
1818
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1572
1819
|
|
|
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
1820
|
// start drawing path (for mode 10)
|
|
1580
1821
|
ctx.beginPath();
|
|
1581
1822
|
|
|
@@ -1584,24 +1825,24 @@ export default class AudioMotionAnalyzer {
|
|
|
1584
1825
|
|
|
1585
1826
|
// draw bars / lines
|
|
1586
1827
|
|
|
1587
|
-
for ( let
|
|
1828
|
+
for ( let barIndex = 0; barIndex < nBars; barIndex++ ) {
|
|
1588
1829
|
|
|
1589
|
-
const bar =
|
|
1590
|
-
{ freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1830
|
+
const bar = bars[ barIndex ],
|
|
1831
|
+
{ posX, barCenter, width, freq, binLo, binHi, ratioLo, ratioHi } = bar;
|
|
1591
1832
|
|
|
1592
|
-
let
|
|
1833
|
+
let barValue = Math.max( interpolate( binLo, ratioLo ), interpolate( binHi, ratioHi ) );
|
|
1593
1834
|
|
|
1594
1835
|
// check additional bins (if any) for this bar and keep the highest value
|
|
1595
1836
|
for ( let j = binLo + 1; j < binHi; j++ ) {
|
|
1596
|
-
if ( fftData[ j ] >
|
|
1597
|
-
|
|
1837
|
+
if ( fftData[ j ] > barValue )
|
|
1838
|
+
barValue = fftData[ j ];
|
|
1598
1839
|
}
|
|
1599
1840
|
|
|
1600
1841
|
// normalize bar amplitude in [0;1] range
|
|
1601
|
-
|
|
1842
|
+
barValue = this._normalizedB( barValue );
|
|
1602
1843
|
|
|
1603
|
-
bar.value[ channel ] =
|
|
1604
|
-
currentEnergy +=
|
|
1844
|
+
bar.value[ channel ] = barValue;
|
|
1845
|
+
currentEnergy += barValue;
|
|
1605
1846
|
|
|
1606
1847
|
// update bar peak
|
|
1607
1848
|
if ( bar.peak[ channel ] > 0 ) {
|
|
@@ -1612,8 +1853,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1612
1853
|
}
|
|
1613
1854
|
|
|
1614
1855
|
// check if it's a new peak for this bar
|
|
1615
|
-
if (
|
|
1616
|
-
bar.peak[ channel ] =
|
|
1856
|
+
if ( barValue >= bar.peak[ channel ] ) {
|
|
1857
|
+
bar.peak[ channel ] = barValue;
|
|
1617
1858
|
bar.hold[ channel ] = 30; // set peak hold time to 30 frames (0.5s)
|
|
1618
1859
|
}
|
|
1619
1860
|
|
|
@@ -1622,31 +1863,30 @@ export default class AudioMotionAnalyzer {
|
|
|
1622
1863
|
continue;
|
|
1623
1864
|
|
|
1624
1865
|
// set opacity for bar effects
|
|
1625
|
-
if (
|
|
1626
|
-
ctx.globalAlpha =
|
|
1866
|
+
if ( isLumi || isAlpha )
|
|
1867
|
+
ctx.globalAlpha = barValue;
|
|
1627
1868
|
else if ( isOutline )
|
|
1628
1869
|
ctx.globalAlpha = fillAlpha;
|
|
1629
1870
|
|
|
1871
|
+
// set fillStyle and strokeStyle for the current bar
|
|
1872
|
+
setBarColor( barValue, barIndex );
|
|
1873
|
+
|
|
1630
1874
|
// compute actual bar height on screen
|
|
1631
|
-
barHeight =
|
|
1875
|
+
let barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
|
|
1632
1876
|
|
|
1633
1877
|
// invert bar for radial channel 1
|
|
1634
1878
|
if ( isRadial && channel == 1 && channelLayout == CHANNEL_VERTICAL )
|
|
1635
1879
|
barHeight *= -1;
|
|
1636
1880
|
|
|
1637
|
-
// bar width may need small adjustments for some bars, when barSpace == 0
|
|
1638
|
-
let adjWidth = width,
|
|
1639
|
-
posX = bar.posX;
|
|
1640
|
-
|
|
1641
1881
|
// Draw current bar or line segment
|
|
1642
1882
|
|
|
1643
1883
|
if ( mode == 10 ) {
|
|
1644
|
-
// compute the average between the initial bar (
|
|
1884
|
+
// compute the average between the initial bar (barIndex==0) and the next one
|
|
1645
1885
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
1646
|
-
const nextBarAvg =
|
|
1886
|
+
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ bars[1].binLo ] ) * maxBarHeight * ( channel && isRadial && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ) + barHeight ) / 2;
|
|
1647
1887
|
|
|
1648
1888
|
if ( isRadial ) {
|
|
1649
|
-
if (
|
|
1889
|
+
if ( barIndex == 0 )
|
|
1650
1890
|
ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ), 1 ) );
|
|
1651
1891
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
1652
1892
|
if ( posX >= 0 ) {
|
|
@@ -1656,7 +1896,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1656
1896
|
}
|
|
1657
1897
|
}
|
|
1658
1898
|
else { // Linear
|
|
1659
|
-
if (
|
|
1899
|
+
if ( barIndex == 0 ) {
|
|
1660
1900
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
1661
1901
|
if ( mirrorMode != -1 ) {
|
|
1662
1902
|
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
@@ -1672,57 +1912,50 @@ export default class AudioMotionAnalyzer {
|
|
|
1672
1912
|
}
|
|
1673
1913
|
}
|
|
1674
1914
|
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 ) {
|
|
1915
|
+
if ( isLeds ) {
|
|
1916
|
+
// draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
|
|
1917
|
+
if ( showBgColor && ! isOverlay && ( channel == 0 || channelLayout != CHANNEL_COMBINED ) ) {
|
|
1695
1918
|
const alpha = ctx.globalAlpha;
|
|
1696
|
-
ctx.beginPath();
|
|
1697
|
-
ctx.moveTo( x, channelTop );
|
|
1698
|
-
ctx.lineTo( x, analyzerBottom );
|
|
1699
1919
|
ctx.strokeStyle = LEDS_UNLIT_COLOR;
|
|
1700
1920
|
ctx.globalAlpha = 1;
|
|
1701
|
-
|
|
1921
|
+
strokeBar( barCenter, channelTop, analyzerBottom );
|
|
1702
1922
|
// restore properties
|
|
1703
1923
|
ctx.strokeStyle = ctx.fillStyle;
|
|
1704
1924
|
ctx.globalAlpha = alpha;
|
|
1705
1925
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1926
|
+
if ( isTrueLeds ) {
|
|
1927
|
+
// ledPosY() is used below to fit one entire led height into the selected range
|
|
1928
|
+
const colorIndex = isLumi ? 0 : colorStops.findLastIndex( item => ledPosY( barValue ) <= ledPosY( item.level ) );
|
|
1929
|
+
let last = analyzerBottom;
|
|
1930
|
+
for ( let i = colorCount - 1; i >= colorIndex; i-- ) {
|
|
1931
|
+
ctx.strokeStyle = colorStops[ i ].color;
|
|
1932
|
+
let y = analyzerBottom - ( i == colorIndex ? barHeight : ledPosY( colorStops[ i ].level ) );
|
|
1933
|
+
strokeBar( barCenter, last, y );
|
|
1934
|
+
last = y - ledSpaceV;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
else
|
|
1938
|
+
strokeBar( barCenter, analyzerBottom, analyzerBottom - barHeight );
|
|
1710
1939
|
}
|
|
1711
1940
|
else if ( posX >= initialX ) {
|
|
1712
1941
|
if ( isRadial )
|
|
1713
|
-
radialPoly( posX, 0,
|
|
1714
|
-
else {
|
|
1715
|
-
const
|
|
1716
|
-
y =
|
|
1717
|
-
w = adjWidth,
|
|
1718
|
-
h = isLumiBars ? channelBottom : -barHeight;
|
|
1942
|
+
radialPoly( posX, 0, width, barHeight, isOutline );
|
|
1943
|
+
else if ( isRound ) {
|
|
1944
|
+
const halfWidth = width / 2,
|
|
1945
|
+
y = analyzerBottom + halfWidth; // round caps have an additional height of half bar width
|
|
1719
1946
|
|
|
1720
1947
|
ctx.beginPath();
|
|
1721
|
-
ctx.moveTo(
|
|
1722
|
-
ctx.lineTo(
|
|
1723
|
-
ctx.
|
|
1724
|
-
ctx.lineTo(
|
|
1725
|
-
|
|
1948
|
+
ctx.moveTo( posX, y );
|
|
1949
|
+
ctx.lineTo( posX, y - barHeight );
|
|
1950
|
+
ctx.arc( barCenter, y - barHeight, halfWidth, Math.PI, TAU );
|
|
1951
|
+
ctx.lineTo( posX + width, y );
|
|
1952
|
+
strokeIf( isOutline );
|
|
1953
|
+
ctx.fill();
|
|
1954
|
+
}
|
|
1955
|
+
else {
|
|
1956
|
+
const offset = isOutline ? ctx.lineWidth : 0;
|
|
1957
|
+
ctx.beginPath();
|
|
1958
|
+
ctx.rect( posX, analyzerBottom + offset, width, -barHeight - offset );
|
|
1726
1959
|
strokeIf( isOutline );
|
|
1727
1960
|
ctx.fill();
|
|
1728
1961
|
}
|
|
@@ -1731,36 +1964,44 @@ export default class AudioMotionAnalyzer {
|
|
|
1731
1964
|
|
|
1732
1965
|
// Draw peak
|
|
1733
1966
|
const peak = bar.peak[ channel ];
|
|
1734
|
-
if ( peak > 0 && this.showPeaks && !
|
|
1735
|
-
//
|
|
1967
|
+
if ( peak > 0 && this.showPeaks && ! isLumi && posX >= initialX && posX < finalX ) {
|
|
1968
|
+
// set opacity
|
|
1736
1969
|
if ( isOutline && lineWidth > 0 )
|
|
1737
1970
|
ctx.globalAlpha = 1;
|
|
1738
|
-
else if (
|
|
1971
|
+
else if ( isAlpha )
|
|
1739
1972
|
ctx.globalAlpha = peak;
|
|
1740
1973
|
|
|
1974
|
+
// select the peak color for 'bar-level' colorMode or 'trueLeds'
|
|
1975
|
+
if ( colorMode == COLOR_BAR_LEVEL || isTrueLeds )
|
|
1976
|
+
setBarColor( peak );
|
|
1977
|
+
|
|
1741
1978
|
// render peak according to current mode / effect
|
|
1742
|
-
if (
|
|
1979
|
+
if ( isLeds ) {
|
|
1743
1980
|
const ledPeak = ledPosY( peak );
|
|
1744
|
-
if ( ledPeak >=
|
|
1981
|
+
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
1745
1982
|
ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
1746
1983
|
}
|
|
1747
1984
|
else if ( ! isRadial )
|
|
1748
|
-
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight,
|
|
1985
|
+
ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
|
|
1749
1986
|
else if ( mode != 10 ) // radial - no peaks for mode 10
|
|
1750
|
-
radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ),
|
|
1987
|
+
radialPoly( posX, peak * maxBarHeight * ( channel && channelLayout == CHANNEL_VERTICAL ? -1 : 1 ), width, -2 );
|
|
1751
1988
|
}
|
|
1752
1989
|
|
|
1753
|
-
} // for ( let
|
|
1990
|
+
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
1754
1991
|
|
|
1755
1992
|
// if not using the canvas, move earlier to the next channel
|
|
1756
1993
|
if ( ! useCanvas )
|
|
1757
1994
|
continue;
|
|
1758
1995
|
|
|
1996
|
+
ctx.restore(); // restore clip region
|
|
1997
|
+
|
|
1759
1998
|
// restore global alpha
|
|
1760
1999
|
ctx.globalAlpha = 1;
|
|
1761
2000
|
|
|
1762
2001
|
// Fill/stroke drawing path for mode 10
|
|
1763
2002
|
if ( mode == 10 ) {
|
|
2003
|
+
setBarColor(); // select channel gradient
|
|
2004
|
+
|
|
1764
2005
|
if ( isRadial ) {
|
|
1765
2006
|
if ( mirrorMode ) {
|
|
1766
2007
|
let p;
|
|
@@ -1790,47 +2031,12 @@ export default class AudioMotionAnalyzer {
|
|
|
1790
2031
|
}
|
|
1791
2032
|
}
|
|
1792
2033
|
|
|
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
|
-
}
|
|
2034
|
+
// create Reflex effect
|
|
2035
|
+
doReflex( channel );
|
|
1819
2036
|
|
|
1820
2037
|
} // for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1821
2038
|
|
|
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
|
-
}
|
|
2039
|
+
updateEnergy( currentEnergy / ( nBars << ( nChannels - 1 ) ) );
|
|
1834
2040
|
|
|
1835
2041
|
if ( useCanvas ) {
|
|
1836
2042
|
// Mirror effect
|
|
@@ -1844,37 +2050,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1844
2050
|
ctx.setLineDash([]);
|
|
1845
2051
|
|
|
1846
2052
|
// 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
|
-
}
|
|
2053
|
+
drawScaleX();
|
|
1859
2054
|
}
|
|
1860
2055
|
|
|
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
|
-
}
|
|
2056
|
+
// calculate and display (if enabled) the current frame rate
|
|
2057
|
+
updateFPS();
|
|
1878
2058
|
|
|
1879
2059
|
// call callback function, if defined
|
|
1880
2060
|
if ( this.onCanvasDraw ) {
|
|
@@ -1925,18 +2105,17 @@ export default class AudioMotionAnalyzer {
|
|
|
1925
2105
|
const ctx = this._canvasCtx,
|
|
1926
2106
|
canvas = ctx.canvas,
|
|
1927
2107
|
channelLayout = this._chLayout,
|
|
1928
|
-
|
|
2108
|
+
{ isLumi } = this._flg,
|
|
1929
2109
|
isRadial = this._radial,
|
|
1930
|
-
gradientHeight =
|
|
2110
|
+
gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - this._reflexRatio * ( channelLayout != CHANNEL_VERTICAL ) ) | 0,
|
|
1931
2111
|
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
1932
2112
|
analyzerRatio = 1 - this._reflexRatio,
|
|
1933
|
-
initialX
|
|
2113
|
+
{ analyzerWidth, initialX, radius } = this._aux;
|
|
1934
2114
|
|
|
1935
2115
|
// for radial mode
|
|
1936
2116
|
const centerX = canvas.width >> 1,
|
|
1937
2117
|
centerY = canvas.height >> 1,
|
|
1938
|
-
maxRadius = Math.min( centerX, centerY )
|
|
1939
|
-
radius = this._radius;
|
|
2118
|
+
maxRadius = Math.min( centerX, centerY );
|
|
1940
2119
|
|
|
1941
2120
|
for ( const channel of [0,1] ) {
|
|
1942
2121
|
const currGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
@@ -1948,26 +2127,23 @@ export default class AudioMotionAnalyzer {
|
|
|
1948
2127
|
if ( isRadial )
|
|
1949
2128
|
grad = ctx.createRadialGradient( centerX, centerY, maxRadius, centerX, centerY, radius - ( maxRadius - radius ) * ( channelLayout == CHANNEL_VERTICAL ) );
|
|
1950
2129
|
else
|
|
1951
|
-
grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX +
|
|
2130
|
+
grad = ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
|
|
1952
2131
|
|
|
1953
2132
|
if ( colorStops ) {
|
|
1954
2133
|
const dual = channelLayout == CHANNEL_VERTICAL && ! this._splitGradient && ( ! isHorizontal || isRadial );
|
|
1955
2134
|
|
|
1956
|
-
// helper function
|
|
1957
|
-
const addColorStop = ( offset, colorInfo ) => grad.addColorStop( offset, colorInfo.color || colorInfo );
|
|
1958
|
-
|
|
1959
2135
|
for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ ) {
|
|
2136
|
+
const maxIndex = colorStops.length - 1;
|
|
1960
2137
|
|
|
1961
|
-
colorStops.forEach( (
|
|
1962
|
-
|
|
1963
|
-
let offset = colorInfo.pos !== undefined ? colorInfo.pos : index / Math.max( 1, maxIndex );
|
|
2138
|
+
colorStops.forEach( ( colorStop, index ) => {
|
|
2139
|
+
let offset = colorStop.pos;
|
|
1964
2140
|
|
|
1965
2141
|
// in dual mode (not split), use half the original offset for each channel
|
|
1966
2142
|
if ( dual )
|
|
1967
2143
|
offset /= 2;
|
|
1968
2144
|
|
|
1969
2145
|
// constrain the offset within the useful analyzer areas (avoid reflex areas)
|
|
1970
|
-
if ( channelLayout == CHANNEL_VERTICAL && !
|
|
2146
|
+
if ( channelLayout == CHANNEL_VERTICAL && ! isLumi && ! isRadial && ! isHorizontal ) {
|
|
1971
2147
|
offset *= analyzerRatio;
|
|
1972
2148
|
// skip the first reflex area in split mode
|
|
1973
2149
|
if ( ! dual && offset > .5 * analyzerRatio )
|
|
@@ -1977,26 +2153,26 @@ export default class AudioMotionAnalyzer {
|
|
|
1977
2153
|
// only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
|
|
1978
2154
|
if ( channelArea == 1 ) {
|
|
1979
2155
|
// add colors in reverse order if radial or lumi are active
|
|
1980
|
-
if ( isRadial ||
|
|
2156
|
+
if ( isRadial || isLumi ) {
|
|
1981
2157
|
const revIndex = maxIndex - index;
|
|
1982
|
-
|
|
1983
|
-
offset = 1 -
|
|
2158
|
+
colorStop = colorStops[ revIndex ];
|
|
2159
|
+
offset = 1 - colorStop.pos / 2;
|
|
1984
2160
|
}
|
|
1985
2161
|
else {
|
|
1986
2162
|
// if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
|
|
1987
2163
|
if ( index == 0 && offset > 0 )
|
|
1988
|
-
addColorStop( .5,
|
|
2164
|
+
grad.addColorStop( .5, colorStop.color );
|
|
1989
2165
|
// bump the offset to the second half of the gradient
|
|
1990
2166
|
offset += .5;
|
|
1991
2167
|
}
|
|
1992
2168
|
}
|
|
1993
2169
|
|
|
1994
2170
|
// add gradient color stop
|
|
1995
|
-
addColorStop( offset,
|
|
2171
|
+
grad.addColorStop( offset, colorStop.color );
|
|
1996
2172
|
|
|
1997
2173
|
// create additional color stop at the end of first channel to prevent bleeding
|
|
1998
2174
|
if ( channelLayout == CHANNEL_VERTICAL && index == maxIndex && offset < .5 )
|
|
1999
|
-
addColorStop( .5,
|
|
2175
|
+
grad.addColorStop( .5, colorStop.color );
|
|
2000
2176
|
});
|
|
2001
2177
|
} // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
|
|
2002
2178
|
}
|
|
@@ -2064,9 +2240,6 @@ export default class AudioMotionAnalyzer {
|
|
|
2064
2240
|
canvas.width = newWidth;
|
|
2065
2241
|
canvas.height = newHeight;
|
|
2066
2242
|
|
|
2067
|
-
// update internal variables
|
|
2068
|
-
this._calcAux();
|
|
2069
|
-
|
|
2070
2243
|
// if not in overlay mode, paint the canvas black
|
|
2071
2244
|
if ( ! this.overlay ) {
|
|
2072
2245
|
ctx.fillStyle = '#000';
|
|
@@ -2078,14 +2251,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2078
2251
|
|
|
2079
2252
|
// update dimensions of the scale canvas
|
|
2080
2253
|
canvasX.width = newWidth;
|
|
2081
|
-
canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) /
|
|
2082
|
-
|
|
2083
|
-
// (re)generate gradient
|
|
2084
|
-
this._makeGrad();
|
|
2254
|
+
canvasX.height = Math.max( 20 * pixelRatio, Math.min( newWidth, newHeight ) / 32 | 0 );
|
|
2085
2255
|
|
|
2086
2256
|
// calculate bar positions and led options
|
|
2087
2257
|
this._calcBars();
|
|
2088
2258
|
|
|
2259
|
+
// (re)generate gradient
|
|
2260
|
+
this._makeGrad();
|
|
2261
|
+
|
|
2089
2262
|
// detect fullscreen changes (for Safari)
|
|
2090
2263
|
if ( this._fsStatus !== undefined && this._fsStatus !== isFullscreen )
|
|
2091
2264
|
reason = REASON_FSCHANGE;
|
|
@@ -2127,6 +2300,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2127
2300
|
barSpace : 0.1,
|
|
2128
2301
|
bgAlpha : 0.7,
|
|
2129
2302
|
channelLayout : CHANNEL_SINGLE,
|
|
2303
|
+
colorMode : COLOR_GRADIENT,
|
|
2130
2304
|
fftSize : 8192,
|
|
2131
2305
|
fillAlpha : 1,
|
|
2132
2306
|
frequencyScale : SCALE_LOG,
|
|
@@ -2151,6 +2325,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2151
2325
|
reflexBright : 1,
|
|
2152
2326
|
reflexFit : true,
|
|
2153
2327
|
reflexRatio : 0,
|
|
2328
|
+
roundBars : false,
|
|
2154
2329
|
showBgColor : true,
|
|
2155
2330
|
showFPS : false,
|
|
2156
2331
|
showPeaks : true,
|
|
@@ -2160,6 +2335,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2160
2335
|
spinSpeed : 0,
|
|
2161
2336
|
splitGradient : false,
|
|
2162
2337
|
start : true,
|
|
2338
|
+
trueLeds : false,
|
|
2163
2339
|
useCanvas : true,
|
|
2164
2340
|
volume : 1,
|
|
2165
2341
|
weightingFilter: FILTER_NONE
|