audiomotion-analyzer 4.2.0 → 4.3.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 +17 -12
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +286 -241
- package/src/index.d.ts +1 -1
package/README.md
CHANGED
|
@@ -333,11 +333,12 @@ Defaults to **0.7**.
|
|
|
333
333
|
|
|
334
334
|
Defines the number and layout of analyzer channels.
|
|
335
335
|
|
|
336
|
-
channelLayout
|
|
337
|
-
|
|
338
|
-
'single'
|
|
339
|
-
'dual-combined'
|
|
340
|
-
'dual-
|
|
336
|
+
channelLayout | Description | Note
|
|
337
|
+
------------------|-------------|------
|
|
338
|
+
'single' | Single channel analyzer, representing the combined output of both left and right channels.
|
|
339
|
+
'dual-combined' | Dual channel analyzer, both channels overlaid. Works best with semi-transparent **Graph** [`mode`](#mode-number) or [`outlineBars`](#outlinebars-boolean).
|
|
340
|
+
'dual-horizontal' | Dual channel, side by side - see [`mirror`](#mirror-number) for additional layout options. | *since v4.3.0*
|
|
341
|
+
'dual-vertical' | Dual channel, left channel at the top half of the canvas and right channel at the bottom.
|
|
341
342
|
|
|
342
343
|
!> When a *dual* layout is selected, any mono (single channel) audio source connected to the analyzer will output sound only from the left speaker,
|
|
343
344
|
unless a stereo source is simultaneously connected to the analyzer, which will force the mono input to be upmixed to stereo.
|
|
@@ -668,15 +669,19 @@ It is preferable to use the [`setFreqRange()`](#setfreqrange-minfreq-maxfreq-) m
|
|
|
668
669
|
|
|
669
670
|
*Available since v3.3.0*
|
|
670
671
|
|
|
671
|
-
|
|
672
|
+
When [`channelLayout`](#channellayout-string) is **dual-horizontal**, this property controls the orientation of the X-axis (frequencies) on both channels.
|
|
672
673
|
|
|
673
|
-
|
|
674
|
-
:-----:|--------
|
|
675
|
-
-1 | Mirrors the analyzer to the left (low frequencies at the center of the screen)
|
|
676
|
-
0 | Disables mirror effect (default)
|
|
677
|
-
1 | Mirrors the analyzer to the right (high frequencies at the center of the screen)
|
|
674
|
+
For other layouts, it horizontally mirrors the spectrum image to the left or right side.
|
|
678
675
|
|
|
679
|
-
|
|
676
|
+
Valid values are:
|
|
677
|
+
|
|
678
|
+
mirror | Description
|
|
679
|
+
:-----:|-------------
|
|
680
|
+
-1 | Low frequencies meet at the center of the screen (mirror left)
|
|
681
|
+
0 | No mirror effect or change to axis orientation (default)
|
|
682
|
+
1 | High frequencies meet at the center of the screen (mirror right)
|
|
683
|
+
|
|
684
|
+
**Note:** On [`radial`](#radial-boolean) spectrum with channel layouts other than *dual-horizontal*, both `1` and `-1` have the same effect.
|
|
680
685
|
|
|
681
686
|
Defaults to **0**.
|
|
682
687
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audiomotion-analyzer",
|
|
3
3
|
"description": "High-resolution real-time graphic audio spectrum analyzer JavaScript module with no dependencies.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"main": "./src/audioMotion-analyzer.js",
|
|
6
6
|
"module": "./src/audioMotion-analyzer.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
* audioMotion-analyzer
|
|
3
3
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
4
4
|
*
|
|
5
|
-
* @version 4.
|
|
5
|
+
* @version 4.3.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.3.0';
|
|
11
11
|
|
|
12
12
|
// internal constants
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const PI = Math.PI,
|
|
14
|
+
TAU = 2 * PI,
|
|
15
|
+
HALF_PI = PI / 2,
|
|
15
16
|
C_1 = 8.17579892; // frequency for C -1
|
|
16
17
|
|
|
17
18
|
const CANVAS_BACKGROUND_COLOR = '#000',
|
|
18
19
|
CHANNEL_COMBINED = 'dual-combined',
|
|
20
|
+
CHANNEL_HORIZONTAL = 'dual-horizontal',
|
|
19
21
|
CHANNEL_SINGLE = 'single',
|
|
20
22
|
CHANNEL_VERTICAL = 'dual-vertical',
|
|
21
23
|
COLOR_BAR_INDEX = 'bar-index',
|
|
@@ -35,6 +37,7 @@ const CANVAS_BACKGROUND_COLOR = '#000',
|
|
|
35
37
|
FONT_FAMILY = 'sans-serif',
|
|
36
38
|
FPS_COLOR = '#0f0',
|
|
37
39
|
LEDS_UNLIT_COLOR = '#7f7f7f22',
|
|
40
|
+
MODE_GRAPH = 10,
|
|
38
41
|
REASON_CREATE = 'create',
|
|
39
42
|
REASON_FSCHANGE = 'fschange',
|
|
40
43
|
REASON_LORES = 'lores',
|
|
@@ -352,7 +355,7 @@ export default class AudioMotionAnalyzer {
|
|
|
352
355
|
return this._chLayout;
|
|
353
356
|
}
|
|
354
357
|
set channelLayout( value ) {
|
|
355
|
-
this._chLayout = validateFromList( value, [ CHANNEL_SINGLE, CHANNEL_VERTICAL, CHANNEL_COMBINED ] );
|
|
358
|
+
this._chLayout = validateFromList( value, [ CHANNEL_SINGLE, CHANNEL_HORIZONTAL, CHANNEL_VERTICAL, CHANNEL_COMBINED ] );
|
|
356
359
|
|
|
357
360
|
// update node connections
|
|
358
361
|
this._input.disconnect();
|
|
@@ -1112,43 +1115,39 @@ export default class AudioMotionAnalyzer {
|
|
|
1112
1115
|
return;
|
|
1113
1116
|
}
|
|
1114
1117
|
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
isRadial = this._radial,
|
|
1121
|
-
isDual = chLayout == CHANNEL_VERTICAL && ! isRadial,
|
|
1122
|
-
maxFreq = this._maxFreq,
|
|
1123
|
-
minFreq = this._minFreq,
|
|
1124
|
-
mode = this._mode,
|
|
1118
|
+
const { _ansiBands, _barSpace, canvas, _chLayout, _maxFreq, _minFreq, _mirror, _mode, _radial, _reflexRatio } = this,
|
|
1119
|
+
centerX = canvas.width >> 1,
|
|
1120
|
+
centerY = canvas.height >> 1,
|
|
1121
|
+
isDualVertical = _chLayout == CHANNEL_VERTICAL && ! _radial,
|
|
1122
|
+
isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
|
|
1125
1123
|
|
|
1126
1124
|
// COMPUTE FLAGS
|
|
1127
1125
|
|
|
1128
|
-
isBands =
|
|
1126
|
+
isBands = _mode % 10 != 0, // true for modes 1 to 9
|
|
1129
1127
|
isOctaves = isBands && this._frequencyScale == SCALE_LOG,
|
|
1130
|
-
isLeds = this._showLeds && isBands && !
|
|
1131
|
-
isLumi = this._lumiBars && isBands && !
|
|
1132
|
-
isAlpha = this._alphaBars && ! isLumi &&
|
|
1128
|
+
isLeds = this._showLeds && isBands && ! _radial,
|
|
1129
|
+
isLumi = this._lumiBars && isBands && ! _radial,
|
|
1130
|
+
isAlpha = this._alphaBars && ! isLumi && _mode != MODE_GRAPH,
|
|
1133
1131
|
isOutline = this._outlineBars && isBands && ! isLumi && ! isLeds,
|
|
1134
1132
|
isRound = this._roundBars && isBands && ! isLumi && ! isLeds,
|
|
1135
|
-
noLedGap =
|
|
1133
|
+
noLedGap = _chLayout != CHANNEL_VERTICAL || _reflexRatio > 0 && ! isLumi,
|
|
1136
1134
|
|
|
1137
1135
|
// COMPUTE AUXILIARY VALUES
|
|
1138
1136
|
|
|
1139
1137
|
// channelHeight is the total canvas height dedicated to each channel, including the reflex area, if any)
|
|
1140
|
-
channelHeight = canvas.height - (
|
|
1138
|
+
channelHeight = canvas.height - ( isDualVertical && ! isLeds ? .5 : 0 ) >> isDualVertical,
|
|
1141
1139
|
// analyzerHeight is the effective height used to render the analyzer, excluding the reflex area
|
|
1142
|
-
analyzerHeight = channelHeight * ( isLumi ||
|
|
1140
|
+
analyzerHeight = channelHeight * ( isLumi || _radial ? 1 : 1 - _reflexRatio ) | 0,
|
|
1143
1141
|
|
|
1144
|
-
analyzerWidth = canvas.width - centerX * (
|
|
1142
|
+
analyzerWidth = canvas.width - centerX * ( isDualHorizontal || _mirror != 0 ),
|
|
1145
1143
|
|
|
1146
1144
|
// channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
|
|
1147
1145
|
// TODO: improve this, make it configurable?
|
|
1148
|
-
channelGap =
|
|
1146
|
+
channelGap = isDualVertical ? canvas.height - channelHeight * 2 : 0,
|
|
1149
1147
|
|
|
1150
|
-
initialX = centerX * (
|
|
1151
|
-
|
|
1148
|
+
initialX = centerX * ( _mirror == -1 && ! isDualHorizontal && ! _radial ),
|
|
1149
|
+
innerRadius = Math.min( canvas.width, canvas.height ) * ( _chLayout == CHANNEL_VERTICAL ? .375 : .125 ) | 0,
|
|
1150
|
+
outerRadius = Math.min( centerX, centerY );
|
|
1152
1151
|
|
|
1153
1152
|
/**
|
|
1154
1153
|
* CREATE ANALYZER BANDS
|
|
@@ -1227,12 +1226,12 @@ export default class AudioMotionAnalyzer {
|
|
|
1227
1226
|
|
|
1228
1227
|
// ANSI standard octave bands use the base-10 frequency ratio, as preferred by [ANSI S1.11-2004, p.2]
|
|
1229
1228
|
// The equal-tempered scale uses the base-2 ratio
|
|
1230
|
-
const bands = [0,24,12,8,6,4,3,2,1][
|
|
1231
|
-
bandWidth =
|
|
1229
|
+
const bands = [0,24,12,8,6,4,3,2,1][ _mode ],
|
|
1230
|
+
bandWidth = _ansiBands ? 10 ** ( 3 / ( bands * 10 ) ) : 2 ** ( 1 / bands ), // 10^(3/10N) or 2^(1/N)
|
|
1232
1231
|
halfBand = bandWidth ** .5;
|
|
1233
1232
|
|
|
1234
1233
|
let analyzerBars = [],
|
|
1235
|
-
currFreq =
|
|
1234
|
+
currFreq = _ansiBands ? 7.94328235 / ( bands % 2 ? 1 : halfBand ) : C_1;
|
|
1236
1235
|
// For ANSI bands with even denominators (all except 1/1 and 1/3), the reference frequency (1 kHz)
|
|
1237
1236
|
// must fall on the edges of a pair of adjacent bands, instead of midband [ANSI S1.11-2004, p.2]
|
|
1238
1237
|
// In the equal-tempered scale, all midband frequencies represent a musical note or quarter-tone.
|
|
@@ -1247,16 +1246,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1247
1246
|
|
|
1248
1247
|
// for 1/1, 1/2 and 1/3 ANSI bands, use the preferred numbers to find the nominal midband frequency
|
|
1249
1248
|
// for 1/4 to 1/24, round to 2 or 3 significant digits, according to the MSD [ANSI S1.11-2004, p.12]
|
|
1250
|
-
if (
|
|
1249
|
+
if ( _ansiBands )
|
|
1251
1250
|
freq = bands < 4 ? nearestPreferred( freq ) : roundSD( freq, freq.toString()[0] < 5 ? 3 : 2 );
|
|
1252
1251
|
else
|
|
1253
1252
|
freq = roundSD( freq, 4, true );
|
|
1254
1253
|
|
|
1255
|
-
if ( freq >=
|
|
1254
|
+
if ( freq >= _minFreq )
|
|
1256
1255
|
barsPush( { posX: 0, freq, freqLo, freqHi, binLo, binHi, ratioLo, ratioHi } );
|
|
1257
1256
|
|
|
1258
1257
|
currFreq *= bandWidth;
|
|
1259
|
-
} while ( currFreq <=
|
|
1258
|
+
} while ( currFreq <= _maxFreq );
|
|
1260
1259
|
|
|
1261
1260
|
barWidth = analyzerWidth / bars.length;
|
|
1262
1261
|
|
|
@@ -1270,19 +1269,19 @@ export default class AudioMotionAnalyzer {
|
|
|
1270
1269
|
|
|
1271
1270
|
// clamp edge frequencies to minFreq / maxFreq, if necessary
|
|
1272
1271
|
// this is done after computing scaleMin and unitWidth, for the proper positioning of labels on the X-axis
|
|
1273
|
-
if ( firstBar.freqLo <
|
|
1274
|
-
firstBar.freqLo =
|
|
1275
|
-
[ firstBar.binLo, firstBar.ratioLo ] = calcRatio(
|
|
1272
|
+
if ( firstBar.freqLo < _minFreq ) {
|
|
1273
|
+
firstBar.freqLo = _minFreq;
|
|
1274
|
+
[ firstBar.binLo, firstBar.ratioLo ] = calcRatio( _minFreq );
|
|
1276
1275
|
}
|
|
1277
1276
|
|
|
1278
|
-
if ( lastBar.freqHi >
|
|
1279
|
-
lastBar.freqHi =
|
|
1280
|
-
[ lastBar.binHi, lastBar.ratioHi ] = calcRatio(
|
|
1277
|
+
if ( lastBar.freqHi > _maxFreq ) {
|
|
1278
|
+
lastBar.freqHi = _maxFreq;
|
|
1279
|
+
[ lastBar.binHi, lastBar.ratioHi ] = calcRatio( _maxFreq );
|
|
1281
1280
|
}
|
|
1282
1281
|
}
|
|
1283
1282
|
else if ( isBands ) { // a bands mode is selected, but frequency scale is not logarithmic
|
|
1284
1283
|
|
|
1285
|
-
const bands = [0,24,12,8,6,4,3,2,1][
|
|
1284
|
+
const bands = [0,24,12,8,6,4,3,2,1][ _mode ] * 10;
|
|
1286
1285
|
|
|
1287
1286
|
const invFreqScaling = x => {
|
|
1288
1287
|
switch ( this._frequencyScale ) {
|
|
@@ -1297,8 +1296,8 @@ export default class AudioMotionAnalyzer {
|
|
|
1297
1296
|
|
|
1298
1297
|
barWidth = analyzerWidth / bands;
|
|
1299
1298
|
|
|
1300
|
-
scaleMin = this._freqScaling(
|
|
1301
|
-
unitWidth = analyzerWidth / ( this._freqScaling(
|
|
1299
|
+
scaleMin = this._freqScaling( _minFreq );
|
|
1300
|
+
unitWidth = analyzerWidth / ( this._freqScaling( _maxFreq ) - scaleMin );
|
|
1302
1301
|
|
|
1303
1302
|
for ( let i = 0, posX = 0; i < bands; i++, posX += barWidth ) {
|
|
1304
1303
|
const freqLo = invFreqScaling( scaleMin + posX / unitWidth ),
|
|
@@ -1314,11 +1313,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1314
1313
|
else { // Discrete frequencies modes
|
|
1315
1314
|
barWidth = 1;
|
|
1316
1315
|
|
|
1317
|
-
scaleMin = this._freqScaling(
|
|
1318
|
-
unitWidth = analyzerWidth / ( this._freqScaling(
|
|
1316
|
+
scaleMin = this._freqScaling( _minFreq );
|
|
1317
|
+
unitWidth = analyzerWidth / ( this._freqScaling( _maxFreq ) - scaleMin );
|
|
1319
1318
|
|
|
1320
|
-
const minIndex = this._freqToBin(
|
|
1321
|
-
maxIndex = this._freqToBin(
|
|
1319
|
+
const minIndex = this._freqToBin( _minFreq, 'floor' ),
|
|
1320
|
+
maxIndex = this._freqToBin( _maxFreq );
|
|
1322
1321
|
|
|
1323
1322
|
let lastPos = -999;
|
|
1324
1323
|
|
|
@@ -1374,7 +1373,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1374
1373
|
|
|
1375
1374
|
// use custom LED parameters if set, or the default parameters for the current mode
|
|
1376
1375
|
const customParams = this._ledParams,
|
|
1377
|
-
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[
|
|
1376
|
+
[ maxLeds, spaceVRatio, spaceHRatio ] = customParams || params[ _mode ];
|
|
1378
1377
|
|
|
1379
1378
|
let ledCount, maxHeight = analyzerHeight;
|
|
1380
1379
|
|
|
@@ -1413,9 +1412,9 @@ export default class AudioMotionAnalyzer {
|
|
|
1413
1412
|
}
|
|
1414
1413
|
|
|
1415
1414
|
// COMPUTE ADDITIONAL BAR POSITIONING, ACCORDING TO THE CURRENT SETTINGS
|
|
1416
|
-
// uses:
|
|
1415
|
+
// uses: _barSpace, barWidth, spaceH
|
|
1417
1416
|
|
|
1418
|
-
const barSpacePx = Math.min( barWidth - 1,
|
|
1417
|
+
const barSpacePx = Math.min( barWidth - 1, _barSpace * ( _barSpace > 0 && _barSpace < 1 ? barWidth : 1 ) );
|
|
1419
1418
|
|
|
1420
1419
|
if ( isBands )
|
|
1421
1420
|
barWidth -= Math.max( isLeds ? spaceH : 0, barSpacePx );
|
|
@@ -1427,7 +1426,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1427
1426
|
// in bands modes we need to update bar.posX to account for bar/led spacing
|
|
1428
1427
|
|
|
1429
1428
|
if ( isBands ) {
|
|
1430
|
-
if (
|
|
1429
|
+
if ( _barSpace == 0 && ! isLeds ) {
|
|
1431
1430
|
// when barSpace == 0 use integer values for perfect gapless positioning
|
|
1432
1431
|
posX |= 0;
|
|
1433
1432
|
width |= 0;
|
|
@@ -1450,7 +1449,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1450
1449
|
|
|
1451
1450
|
const channelCoords = [];
|
|
1452
1451
|
for ( const channel of [0,1] ) {
|
|
1453
|
-
const channelTop =
|
|
1452
|
+
const channelTop = _chLayout == CHANNEL_VERTICAL ? ( channelHeight + channelGap ) * channel : 0,
|
|
1454
1453
|
channelBottom = channelTop + channelHeight,
|
|
1455
1454
|
analyzerBottom = channelTop + analyzerHeight - ( ! isLeds || noLedGap ? 0 : spaceV );
|
|
1456
1455
|
|
|
@@ -1459,7 +1458,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1459
1458
|
|
|
1460
1459
|
// SAVE INTERNAL PROPERTIES
|
|
1461
1460
|
|
|
1462
|
-
this._aux = { analyzerHeight, analyzerWidth, channelCoords, channelHeight, channelGap, initialX,
|
|
1461
|
+
this._aux = { analyzerHeight, analyzerWidth, centerX, centerY, channelCoords, channelHeight, channelGap, initialX, innerRadius, outerRadius, scaleMin, unitWidth };
|
|
1463
1462
|
this._flg = { isAlpha, isBands, isLeds, isLumi, isOctaves, isOutline, isRound, noLedGap };
|
|
1464
1463
|
|
|
1465
1464
|
// generate the X-axis and radial scales
|
|
@@ -1473,19 +1472,20 @@ export default class AudioMotionAnalyzer {
|
|
|
1473
1472
|
if ( ! this._ready )
|
|
1474
1473
|
return;
|
|
1475
1474
|
|
|
1476
|
-
const { analyzerWidth, initialX,
|
|
1475
|
+
const { analyzerWidth, initialX, innerRadius, scaleMin, unitWidth } = this._aux,
|
|
1477
1476
|
{ canvas, _frequencyScale, _mirror, _noteLabels, _radial, _scaleX, _scaleR } = this,
|
|
1478
|
-
canvasX
|
|
1479
|
-
canvasR
|
|
1480
|
-
freqLabels
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1477
|
+
canvasX = _scaleX.canvas,
|
|
1478
|
+
canvasR = _scaleR.canvas,
|
|
1479
|
+
freqLabels = [],
|
|
1480
|
+
isDualHorizontal = this._chLayout == CHANNEL_HORIZONTAL,
|
|
1481
|
+
isDualVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
1482
|
+
scale = [ 'C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B' ], // for note labels (no sharp notes)
|
|
1483
|
+
scaleHeight = Math.min( canvas.width, canvas.height ) / 34 | 0, // circular scale height (radial mode)
|
|
1484
|
+
fontSizeX = canvasX.height >> 1,
|
|
1485
|
+
fontSizeR = scaleHeight >> 1,
|
|
1486
|
+
labelWidthX = fontSizeX * ( _noteLabels ? .7 : 1.5 ),
|
|
1487
|
+
labelWidthR = fontSizeR * ( _noteLabels ? 1 : 2 ),
|
|
1488
|
+
root12 = 2 ** ( 1 / 12 );
|
|
1489
1489
|
|
|
1490
1490
|
if ( ! _noteLabels && ( this._ansiBands || _frequencyScale != SCALE_LOG ) ) {
|
|
1491
1491
|
freqLabels.push( 16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3 );
|
|
@@ -1501,7 +1501,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1501
1501
|
if ( freq >= this._minFreq && freq <= this._maxFreq ) {
|
|
1502
1502
|
const pitch = scale[ note ],
|
|
1503
1503
|
isC = pitch == 'C';
|
|
1504
|
-
if ( ( pitch && _noteLabels && ! _mirror ) || isC )
|
|
1504
|
+
if ( ( pitch && _noteLabels && ! _mirror && ! isDualHorizontal ) || isC )
|
|
1505
1505
|
freqLabels.push( _noteLabels ? [ freq, pitch + ( isC ? octave : '' ) ] : freq );
|
|
1506
1506
|
}
|
|
1507
1507
|
freq *= root12;
|
|
@@ -1510,7 +1510,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1510
1510
|
}
|
|
1511
1511
|
|
|
1512
1512
|
// in radial dual-vertical layout, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
|
|
1513
|
-
canvasR.width = canvasR.height = (
|
|
1513
|
+
canvasR.width = canvasR.height = ( innerRadius << 1 ) + ( isDualVertical * scaleHeight );
|
|
1514
1514
|
|
|
1515
1515
|
const centerR = canvasR.width >> 1,
|
|
1516
1516
|
radialY = centerR - scaleHeight * .7; // vertical position of text labels in the circular scale
|
|
@@ -1552,10 +1552,10 @@ export default class AudioMotionAnalyzer {
|
|
|
1552
1552
|
x = unitWidth * ( this._freqScaling( freq ) - scaleMin ),
|
|
1553
1553
|
y = canvasX.height * .75,
|
|
1554
1554
|
isC = label[0] == 'C',
|
|
1555
|
-
maxW = fontSizeX * ( _noteLabels && ! _mirror ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1555
|
+
maxW = fontSizeX * ( _noteLabels && ! _mirror && ! isDualHorizontal ? ( isC ? 1.2 : .6 ) : 3 );
|
|
1556
1556
|
|
|
1557
1557
|
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1558
|
-
_scaleX.fillStyle = _scaleR.fillStyle = isC && ! _mirror ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1558
|
+
_scaleX.fillStyle = _scaleR.fillStyle = isC && ! _mirror && ! isDualHorizontal ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
|
|
1559
1559
|
|
|
1560
1560
|
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1561
1561
|
if ( _noteLabels ) {
|
|
@@ -1565,13 +1565,13 @@ export default class AudioMotionAnalyzer {
|
|
|
1565
1565
|
let allowedLabels = ['C'];
|
|
1566
1566
|
|
|
1567
1567
|
if ( isLog || freq > 2e3 || ( ! isLinear && freq > 250 ) ||
|
|
1568
|
-
( ( ! _radial ||
|
|
1568
|
+
( ( ! _radial || isDualVertical ) && ( ! isLinear && freq > 125 || freq > 1e3 ) ) )
|
|
1569
1569
|
allowedLabels.push('G');
|
|
1570
1570
|
if ( isLog || freq > 4e3 || ( ! isLinear && freq > 500 ) ||
|
|
1571
|
-
( ( ! _radial ||
|
|
1571
|
+
( ( ! _radial || isDualVertical ) && ( ! isLinear && freq > 250 || freq > 2e3 ) ) )
|
|
1572
1572
|
allowedLabels.push('E');
|
|
1573
1573
|
if ( isLinear && freq > 4e3 ||
|
|
1574
|
-
( ( ! _radial ||
|
|
1574
|
+
( ( ! _radial || isDualVertical ) && ( isLog || freq > 2e3 || ( ! isLinear && freq > 500 ) ) ) )
|
|
1575
1575
|
allowedLabels.push('D','F','A','B');
|
|
1576
1576
|
if ( ! allowedLabels.includes( label[0] ) )
|
|
1577
1577
|
continue; // skip this label
|
|
@@ -1579,17 +1579,17 @@ export default class AudioMotionAnalyzer {
|
|
|
1579
1579
|
|
|
1580
1580
|
// linear scale
|
|
1581
1581
|
if ( x >= prevX + labelWidthX / 2 && x <= analyzerWidth ) {
|
|
1582
|
-
_scaleX.fillText( label, initialX + x, y, maxW );
|
|
1583
|
-
if ( _mirror && ( x > labelWidthX || _mirror == 1 ) )
|
|
1584
|
-
_scaleX.fillText( label, ( initialX || canvas.width ) - x, y, maxW );
|
|
1582
|
+
_scaleX.fillText( label, isDualHorizontal && _mirror == -1 ? analyzerWidth - x : initialX + x, y, maxW );
|
|
1583
|
+
if ( isDualHorizontal || ( _mirror && ( x > labelWidthX || _mirror == 1 ) ) )
|
|
1584
|
+
_scaleX.fillText( label, isDualHorizontal && _mirror != 1 ? analyzerWidth + x : ( initialX || canvas.width ) - x, y, maxW );
|
|
1585
1585
|
prevX = x + Math.min( maxW, _scaleX.measureText( label ).width ) / 2;
|
|
1586
1586
|
}
|
|
1587
1587
|
|
|
1588
1588
|
// radial scale
|
|
1589
1589
|
if ( x >= prevR + labelWidthR && x < analyzerWidth - labelWidthR ) { // avoid overlapping the last label over the first one
|
|
1590
|
-
radialLabel( x, label );
|
|
1591
|
-
if ( _mirror && ( x > labelWidthR || _mirror == 1 ) ) // avoid overlapping of first labels on mirror mode
|
|
1592
|
-
radialLabel( -x, label );
|
|
1590
|
+
radialLabel( isDualHorizontal && _mirror == 1 ? analyzerWidth - x : x, label );
|
|
1591
|
+
if ( isDualHorizontal || ( _mirror && ( x > labelWidthR || _mirror == 1 ) ) ) // avoid overlapping of first labels on mirror mode
|
|
1592
|
+
radialLabel( isDualHorizontal && _mirror != -1 ? analyzerWidth + x : -x, label );
|
|
1593
1593
|
prevR = x;
|
|
1594
1594
|
}
|
|
1595
1595
|
}
|
|
@@ -1603,9 +1603,25 @@ export default class AudioMotionAnalyzer {
|
|
|
1603
1603
|
// schedule next canvas update
|
|
1604
1604
|
this._runId = requestAnimationFrame( timestamp => this._draw( timestamp ) );
|
|
1605
1605
|
|
|
1606
|
-
|
|
1606
|
+
// frame rate control
|
|
1607
|
+
const elapsed = timestamp - this._time, // time since last FPS computation
|
|
1608
|
+
frameTime = timestamp - this._last, // time since last rendered frame
|
|
1609
|
+
targetInterval = this._maxFPS ? 975 / this._maxFPS : 0; // small tolerance for best results
|
|
1610
|
+
|
|
1611
|
+
if ( frameTime < targetInterval )
|
|
1607
1612
|
return;
|
|
1608
1613
|
|
|
1614
|
+
this._last = timestamp - ( targetInterval ? frameTime % targetInterval : 0 ); // thanks https://stackoverflow.com/a/19772220/2370385
|
|
1615
|
+
this._frames++;
|
|
1616
|
+
|
|
1617
|
+
if ( elapsed >= 1000 ) { // update FPS every second
|
|
1618
|
+
this._fps = this._frames / elapsed * 1000;
|
|
1619
|
+
this._frames = 0;
|
|
1620
|
+
this._time = timestamp;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// initialize local constants
|
|
1624
|
+
|
|
1609
1625
|
const { isAlpha,
|
|
1610
1626
|
isBands,
|
|
1611
1627
|
isLeds,
|
|
@@ -1616,11 +1632,14 @@ export default class AudioMotionAnalyzer {
|
|
|
1616
1632
|
noLedGap } = this._flg,
|
|
1617
1633
|
|
|
1618
1634
|
{ analyzerHeight,
|
|
1635
|
+
centerX,
|
|
1636
|
+
centerY,
|
|
1619
1637
|
channelCoords,
|
|
1620
1638
|
channelHeight,
|
|
1621
1639
|
channelGap,
|
|
1622
1640
|
initialX,
|
|
1623
|
-
|
|
1641
|
+
innerRadius,
|
|
1642
|
+
outerRadius } = this._aux,
|
|
1624
1643
|
|
|
1625
1644
|
{ _bars,
|
|
1626
1645
|
canvas,
|
|
@@ -1644,20 +1663,19 @@ export default class AudioMotionAnalyzer {
|
|
|
1644
1663
|
useCanvas,
|
|
1645
1664
|
_weightingFilter } = this,
|
|
1646
1665
|
|
|
1647
|
-
canvasX
|
|
1648
|
-
canvasR
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
isDualVertical
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
dbRange = maxDecibels - minDecibels,
|
|
1666
|
+
canvasX = this._scaleX.canvas,
|
|
1667
|
+
canvasR = this._scaleR.canvas,
|
|
1668
|
+
holdFrames = _fps >> 1, // number of frames in half a second
|
|
1669
|
+
isDualCombined = _chLayout == CHANNEL_COMBINED,
|
|
1670
|
+
isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
|
|
1671
|
+
isDualVertical = _chLayout == CHANNEL_VERTICAL,
|
|
1672
|
+
isSingle = _chLayout == CHANNEL_SINGLE,
|
|
1673
|
+
isTrueLeds = isLeds && this._trueLeds && _colorMode == COLOR_GRADIENT,
|
|
1674
|
+
analyzerWidth = _radial ? canvas.width : this._aux.analyzerWidth,
|
|
1675
|
+
finalX = initialX + analyzerWidth,
|
|
1676
|
+
showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
|
|
1677
|
+
maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
|
|
1678
|
+
dbRange = maxDecibels - minDecibels,
|
|
1661
1679
|
[ ledCount, ledSpaceH, ledSpaceV, ledHeight ] = this._leds || [];
|
|
1662
1680
|
|
|
1663
1681
|
if ( _energy.val > 0 )
|
|
@@ -1667,7 +1685,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1667
1685
|
|
|
1668
1686
|
// create Reflex effect
|
|
1669
1687
|
const doReflex = channel => {
|
|
1670
|
-
if ( this._reflexRatio > 0 && ! isLumi ) {
|
|
1688
|
+
if ( this._reflexRatio > 0 && ! isLumi && ! _radial ) {
|
|
1671
1689
|
let posY, height;
|
|
1672
1690
|
if ( this.reflexFit || isDualVertical ) { // always fit reflex in vertical stereo mode
|
|
1673
1691
|
posY = isDualVertical && channel == 0 ? channelHeight + channelGap : 0;
|
|
@@ -1709,49 +1727,6 @@ export default class AudioMotionAnalyzer {
|
|
|
1709
1727
|
}
|
|
1710
1728
|
}
|
|
1711
1729
|
|
|
1712
|
-
// draw scale on Y-axis
|
|
1713
|
-
const drawScaleY = channelTop => {
|
|
1714
|
-
const scaleWidth = canvasX.height,
|
|
1715
|
-
fontSize = scaleWidth >> 1,
|
|
1716
|
-
max = _linearAmplitude ? 100 : maxDecibels,
|
|
1717
|
-
min = _linearAmplitude ? 0 : minDecibels,
|
|
1718
|
-
incr = _linearAmplitude ? 20 : 5,
|
|
1719
|
-
interval = analyzerHeight / ( max - min );
|
|
1720
|
-
|
|
1721
|
-
_ctx.save();
|
|
1722
|
-
_ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1723
|
-
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1724
|
-
_ctx.textAlign = 'right';
|
|
1725
|
-
_ctx.lineWidth = 1;
|
|
1726
|
-
|
|
1727
|
-
for ( let val = max; val > min; val -= incr ) {
|
|
1728
|
-
const posY = channelTop + ( max - val ) * interval,
|
|
1729
|
-
even = ( val % 2 == 0 ) | 0;
|
|
1730
|
-
|
|
1731
|
-
if ( even ) {
|
|
1732
|
-
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1733
|
-
if ( _mirror != -1 )
|
|
1734
|
-
_ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1735
|
-
if ( _mirror != 1 )
|
|
1736
|
-
_ctx.fillText( val, canvas.width - scaleWidth * .1, labelY );
|
|
1737
|
-
_ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1738
|
-
_ctx.setLineDash([2,4]);
|
|
1739
|
-
_ctx.lineDashOffset = 0;
|
|
1740
|
-
}
|
|
1741
|
-
else {
|
|
1742
|
-
_ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1743
|
-
_ctx.setLineDash([2,8]);
|
|
1744
|
-
_ctx.lineDashOffset = 1;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
_ctx.beginPath();
|
|
1748
|
-
_ctx.moveTo( initialX + scaleWidth * even * ( _mirror != -1 ), ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1749
|
-
_ctx.lineTo( finalX - scaleWidth * even * ( _mirror != 1 ), ~~posY + .5 );
|
|
1750
|
-
_ctx.stroke();
|
|
1751
|
-
}
|
|
1752
|
-
_ctx.restore();
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
1730
|
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
1756
1731
|
const weightingdB = freq => {
|
|
1757
1732
|
const f2 = freq ** 2,
|
|
@@ -1808,35 +1783,6 @@ export default class AudioMotionAnalyzer {
|
|
|
1808
1783
|
}
|
|
1809
1784
|
}
|
|
1810
1785
|
|
|
1811
|
-
// converts a given X-coordinate to its corresponding angle in radial mode
|
|
1812
|
-
const getAngle = ( x, dir ) => dir * TAU * ( x / canvas.width ) + this._spinAngle;
|
|
1813
|
-
|
|
1814
|
-
// converts planar X,Y coordinates to radial coordinates
|
|
1815
|
-
const radialXY = ( x, y, dir = 1 ) => {
|
|
1816
|
-
const height = radius + y,
|
|
1817
|
-
angle = getAngle( x, dir );
|
|
1818
|
-
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// draws a polygon of width `w` and height `h` at (x,y) in radial mode
|
|
1822
|
-
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1823
|
-
_ctx.beginPath();
|
|
1824
|
-
for ( const dir of ( _mirror ? [1,-1] : [1] ) ) {
|
|
1825
|
-
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1826
|
-
_ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1827
|
-
_ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1828
|
-
if ( isRound )
|
|
1829
|
-
_ctx.arc( centerX, centerY, radius + y + h, startAngle, endAngle, dir != 1 );
|
|
1830
|
-
else
|
|
1831
|
-
_ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1832
|
-
_ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1833
|
-
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1834
|
-
_ctx.arc( centerX, centerY, radius + y, endAngle, startAngle, dir == 1 );
|
|
1835
|
-
}
|
|
1836
|
-
strokeIf( stroke );
|
|
1837
|
-
_ctx.fill();
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
1786
|
// converts a value in [0;1] range to a height in pixels that fits into the current LED elements
|
|
1841
1787
|
const ledPosY = value => Math.max( 0, ( value * ledCount | 0 ) * ( ledHeight + ledSpaceV ) - ledSpaceV );
|
|
1842
1788
|
|
|
@@ -1854,29 +1800,11 @@ export default class AudioMotionAnalyzer {
|
|
|
1854
1800
|
}
|
|
1855
1801
|
}
|
|
1856
1802
|
|
|
1857
|
-
// calculate and display (if enabled) the current frame rate
|
|
1858
|
-
const updateFPS = () => {
|
|
1859
|
-
const elapsed = timestamp - this._time; // elapsed time since the last FPS computation
|
|
1860
|
-
|
|
1861
|
-
this._last = timestamp - ( this._maxFPS ? elapsed % ( 1000 / this._maxFPS ) : 0 ); // thanks https://stackoverflow.com/a/19772220/2370385
|
|
1862
|
-
this._frames++;
|
|
1863
|
-
|
|
1864
|
-
if ( elapsed >= 1000 ) {
|
|
1865
|
-
this._fps = this._frames / ( elapsed / 1000 );
|
|
1866
|
-
this._frames = 0;
|
|
1867
|
-
this._time = timestamp;
|
|
1868
|
-
}
|
|
1869
|
-
if ( this.showFPS ) {
|
|
1870
|
-
const size = canvasX.height;
|
|
1871
|
-
_ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
1872
|
-
_ctx.fillStyle = FPS_COLOR;
|
|
1873
|
-
_ctx.textAlign = 'right';
|
|
1874
|
-
_ctx.fillText( Math.round( this._fps ), canvas.width - size, size * 2 );
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
1803
|
/* MAIN FUNCTION */
|
|
1879
1804
|
|
|
1805
|
+
if ( overlay )
|
|
1806
|
+
_ctx.clearRect( 0, 0, canvas.width, canvas.height );
|
|
1807
|
+
|
|
1880
1808
|
let currentEnergy = 0;
|
|
1881
1809
|
|
|
1882
1810
|
const nBars = _bars.length,
|
|
@@ -1885,24 +1813,122 @@ export default class AudioMotionAnalyzer {
|
|
|
1885
1813
|
for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
1886
1814
|
|
|
1887
1815
|
const { channelTop, channelBottom, analyzerBottom } = channelCoords[ channel ],
|
|
1888
|
-
channelGradient
|
|
1889
|
-
colorStops
|
|
1890
|
-
colorCount
|
|
1891
|
-
bgColor
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1816
|
+
channelGradient = this._gradients[ this._selectedGrads[ channel ] ],
|
|
1817
|
+
colorStops = channelGradient.colorStops,
|
|
1818
|
+
colorCount = colorStops.length,
|
|
1819
|
+
bgColor = ( ! showBgColor || isLeds && ! overlay ) ? '#000' : channelGradient.bgColor,
|
|
1820
|
+
radialDirection = isDualVertical && _radial && channel ? -1 : 1, // 1 = outwards, -1 = inwards
|
|
1821
|
+
invertedChannel = ( ! channel && _mirror == -1 ) || ( channel && _mirror == 1 ),
|
|
1822
|
+
radialOffsetX = ! isDualHorizontal || ( channel && _mirror != 1 ) ? 0 : analyzerWidth >> ( channel || ! invertedChannel ),
|
|
1823
|
+
angularDirection = isDualHorizontal && invertedChannel ? -1 : 1; // 1 = clockwise, -1 = counterclockwise
|
|
1824
|
+
/*
|
|
1825
|
+
Expanded logic for radialOffsetX and angularDirection:
|
|
1826
|
+
|
|
1827
|
+
let radialOffsetX = 0,
|
|
1828
|
+
angularDirection = 1;
|
|
1829
|
+
|
|
1830
|
+
if ( isDualHorizontal ) {
|
|
1831
|
+
if ( channel == 0 ) { // LEFT channel
|
|
1832
|
+
if ( _mirror == -1 ) {
|
|
1833
|
+
radialOffsetX = analyzerWidth;
|
|
1834
|
+
angularDirection = -1;
|
|
1835
|
+
}
|
|
1836
|
+
else
|
|
1837
|
+
radialOffsetX = analyzerWidth >> 1;
|
|
1838
|
+
}
|
|
1839
|
+
else { // RIGHT channel
|
|
1840
|
+
if ( _mirror == 1 ) {
|
|
1841
|
+
radialOffsetX = analyzerWidth >> 1;
|
|
1842
|
+
angularDirection = -1;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
*/
|
|
1847
|
+
// draw scale on Y-axis (uses: channel, channelTop)
|
|
1848
|
+
const drawScaleY = () => {
|
|
1849
|
+
const scaleWidth = canvasX.height,
|
|
1850
|
+
fontSize = scaleWidth >> 1,
|
|
1851
|
+
max = _linearAmplitude ? 100 : maxDecibels,
|
|
1852
|
+
min = _linearAmplitude ? 0 : minDecibels,
|
|
1853
|
+
incr = _linearAmplitude ? 20 : 5,
|
|
1854
|
+
interval = analyzerHeight / ( max - min ),
|
|
1855
|
+
atStart = _mirror != -1 && ( ! isDualHorizontal || channel == 0 || _mirror == 1 ),
|
|
1856
|
+
atEnd = _mirror != 1 && ( ! isDualHorizontal || channel != _mirror );
|
|
1857
|
+
|
|
1858
|
+
_ctx.save();
|
|
1859
|
+
_ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1860
|
+
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1861
|
+
_ctx.textAlign = 'right';
|
|
1862
|
+
_ctx.lineWidth = 1;
|
|
1863
|
+
|
|
1864
|
+
for ( let val = max; val > min; val -= incr ) {
|
|
1865
|
+
const posY = channelTop + ( max - val ) * interval,
|
|
1866
|
+
even = ( val % 2 == 0 ) | 0;
|
|
1867
|
+
|
|
1868
|
+
if ( even ) {
|
|
1869
|
+
const labelY = posY + fontSize * ( posY == channelTop ? .8 : .35 );
|
|
1870
|
+
if ( atStart )
|
|
1871
|
+
_ctx.fillText( val, scaleWidth * .85, labelY );
|
|
1872
|
+
if ( atEnd )
|
|
1873
|
+
_ctx.fillText( val, ( isDualHorizontal ? analyzerWidth : canvas.width ) - scaleWidth * .1, labelY );
|
|
1874
|
+
_ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1875
|
+
_ctx.setLineDash([2,4]);
|
|
1876
|
+
_ctx.lineDashOffset = 0;
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
_ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1880
|
+
_ctx.setLineDash([2,8]);
|
|
1881
|
+
_ctx.lineDashOffset = 1;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
_ctx.beginPath();
|
|
1885
|
+
_ctx.moveTo( initialX + scaleWidth * even * atStart, ~~posY + .5 ); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1886
|
+
_ctx.lineTo( finalX - scaleWidth * even * atEnd, ~~posY + .5 );
|
|
1887
|
+
_ctx.stroke();
|
|
1888
|
+
}
|
|
1889
|
+
_ctx.restore();
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// FFT bin data interpolation (uses fftData)
|
|
1896
1893
|
const interpolate = ( bin, ratio ) => {
|
|
1897
1894
|
const value = fftData[ bin ] + ( bin < fftData.length - 1 ? ( fftData[ bin + 1 ] - fftData[ bin ] ) * ratio : 0 );
|
|
1898
1895
|
return isNaN( value ) ? -Infinity : value;
|
|
1899
1896
|
}
|
|
1900
1897
|
|
|
1898
|
+
// converts a given X-coordinate to its corresponding angle in radial mode (uses angularDirection)
|
|
1899
|
+
const getAngle = ( x, dir = angularDirection ) => dir * TAU * ( ( x + radialOffsetX ) / canvas.width ) + this._spinAngle;
|
|
1900
|
+
|
|
1901
|
+
// converts planar X,Y coordinates to radial coordinates (uses: getAngle(), radialDirection)
|
|
1902
|
+
const radialXY = ( x, y, dir ) => {
|
|
1903
|
+
const height = innerRadius + y * radialDirection,
|
|
1904
|
+
angle = getAngle( x, dir );
|
|
1905
|
+
return [ centerX + height * Math.cos( angle ), centerY + height * Math.sin( angle ) ];
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
// draws a polygon of width `w` and height `h` at (x,y) in radial mode (uses: angularDirection, radialDirection)
|
|
1909
|
+
const radialPoly = ( x, y, w, h, stroke ) => {
|
|
1910
|
+
_ctx.beginPath();
|
|
1911
|
+
for ( const dir of ( _mirror && ! isDualHorizontal ? [1,-1] : [ angularDirection ] ) ) {
|
|
1912
|
+
const [ startAngle, endAngle ] = isRound ? [ getAngle( x, dir ), getAngle( x + w, dir ) ] : [];
|
|
1913
|
+
_ctx.moveTo( ...radialXY( x, y, dir ) );
|
|
1914
|
+
_ctx.lineTo( ...radialXY( x, y + h, dir ) );
|
|
1915
|
+
if ( isRound )
|
|
1916
|
+
_ctx.arc( centerX, centerY, innerRadius + ( y + h ) * radialDirection, startAngle, endAngle, dir != 1 );
|
|
1917
|
+
else
|
|
1918
|
+
_ctx.lineTo( ...radialXY( x + w, y + h, dir ) );
|
|
1919
|
+
_ctx.lineTo( ...radialXY( x + w, y, dir ) );
|
|
1920
|
+
if ( isRound && ! stroke ) // close the bottom line only when not in outline mode
|
|
1921
|
+
_ctx.arc( centerX, centerY, innerRadius + y * radialDirection, endAngle, startAngle, dir == 1 );
|
|
1922
|
+
}
|
|
1923
|
+
strokeIf( stroke );
|
|
1924
|
+
_ctx.fill();
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1901
1927
|
// set fillStyle and strokeStyle according to current colorMode (uses: channel, colorStops, colorCount)
|
|
1902
1928
|
const setBarColor = ( value = 0, barIndex = 0 ) => {
|
|
1903
1929
|
let color;
|
|
1904
|
-
// for mode
|
|
1905
|
-
if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode ==
|
|
1930
|
+
// for graph mode, always use the channel gradient (ignore colorMode)
|
|
1931
|
+
if ( ( _colorMode == COLOR_GRADIENT && ! isTrueLeds ) || _mode == MODE_GRAPH )
|
|
1906
1932
|
color = _canvasGradients[ channel ];
|
|
1907
1933
|
else {
|
|
1908
1934
|
const selectedIndex = _colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex( item => isLeds ? ledPosY( value ) <= ledPosY( item.level ) : value <= item.level );
|
|
@@ -1911,11 +1937,16 @@ export default class AudioMotionAnalyzer {
|
|
|
1911
1937
|
_ctx.fillStyle = _ctx.strokeStyle = color;
|
|
1912
1938
|
}
|
|
1913
1939
|
|
|
1940
|
+
// CHANNEL START
|
|
1941
|
+
|
|
1914
1942
|
if ( useCanvas ) {
|
|
1915
|
-
//
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1943
|
+
// set transform (horizontal flip and translation) for dual-horizontal layout
|
|
1944
|
+
if ( isDualHorizontal && ! _radial ) {
|
|
1945
|
+
const translateX = analyzerWidth * ( channel + invertedChannel ),
|
|
1946
|
+
flipX = invertedChannel ? -1 : 1;
|
|
1947
|
+
|
|
1948
|
+
_ctx.setTransform( flipX, 0, 0, 1, translateX, 0 );
|
|
1949
|
+
}
|
|
1919
1950
|
|
|
1920
1951
|
// fill the analyzer background if needed (not overlay or overlay + showBgColor)
|
|
1921
1952
|
if ( ! overlay || showBgColor ) {
|
|
@@ -1925,7 +1956,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1925
1956
|
_ctx.fillStyle = bgColor;
|
|
1926
1957
|
|
|
1927
1958
|
// exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
|
|
1928
|
-
if (
|
|
1959
|
+
if ( channel == 0 || ( ! _radial && ! isDualCombined ) )
|
|
1929
1960
|
_ctx.fillRect( initialX, channelTop - channelGap, analyzerWidth, ( overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight ) + channelGap );
|
|
1930
1961
|
|
|
1931
1962
|
_ctx.globalAlpha = 1;
|
|
@@ -1933,7 +1964,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1933
1964
|
|
|
1934
1965
|
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
1935
1966
|
if ( this.showScaleY && ! isLumi && ! _radial && ( channel == 0 || ! isDualCombined ) )
|
|
1936
|
-
drawScaleY(
|
|
1967
|
+
drawScaleY();
|
|
1937
1968
|
|
|
1938
1969
|
// set line width and dash for LEDs effect
|
|
1939
1970
|
if ( isLeds ) {
|
|
@@ -1943,13 +1974,14 @@ export default class AudioMotionAnalyzer {
|
|
|
1943
1974
|
else // for outline effect ensure linewidth is not greater than half the bar width
|
|
1944
1975
|
_ctx.lineWidth = isOutline ? Math.min( _lineWidth, _bars[0].width / 2 ) : _lineWidth;
|
|
1945
1976
|
|
|
1946
|
-
// set
|
|
1977
|
+
// set clipping region
|
|
1947
1978
|
_ctx.save();
|
|
1948
1979
|
if ( ! _radial ) {
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
_ctx.clip(
|
|
1980
|
+
const region = new Path2D();
|
|
1981
|
+
region.rect( 0, channelTop, canvas.width, analyzerHeight );
|
|
1982
|
+
_ctx.clip( region );
|
|
1952
1983
|
}
|
|
1984
|
+
|
|
1953
1985
|
} // if ( useCanvas )
|
|
1954
1986
|
|
|
1955
1987
|
// get a new array of data from the FFT
|
|
@@ -1960,7 +1992,7 @@ export default class AudioMotionAnalyzer {
|
|
|
1960
1992
|
if ( _weightingFilter )
|
|
1961
1993
|
fftData = fftData.map( ( val, idx ) => val + weightingdB( this._binToFreq( idx ) ) );
|
|
1962
1994
|
|
|
1963
|
-
// start drawing path (for mode
|
|
1995
|
+
// start drawing path (for graph mode)
|
|
1964
1996
|
_ctx.beginPath();
|
|
1965
1997
|
|
|
1966
1998
|
// store line graph points to create mirror effect in radial mode
|
|
@@ -2015,18 +2047,21 @@ export default class AudioMotionAnalyzer {
|
|
|
2015
2047
|
setBarColor( barValue, barIndex );
|
|
2016
2048
|
|
|
2017
2049
|
// compute actual bar height on screen
|
|
2018
|
-
const barHeight =
|
|
2050
|
+
const barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY( barValue ) : barValue * maxBarHeight | 0;
|
|
2019
2051
|
|
|
2020
2052
|
// Draw current bar or line segment
|
|
2021
2053
|
|
|
2022
|
-
if ( _mode ==
|
|
2054
|
+
if ( _mode == MODE_GRAPH ) {
|
|
2023
2055
|
// compute the average between the initial bar (barIndex==0) and the next one
|
|
2024
2056
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
2025
|
-
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ _bars[1].binLo ] ) * maxBarHeight
|
|
2057
|
+
const nextBarAvg = barIndex ? 0 : ( this._normalizedB( fftData[ _bars[1].binLo ] ) * maxBarHeight + barHeight ) / 2;
|
|
2026
2058
|
|
|
2027
2059
|
if ( _radial ) {
|
|
2028
|
-
if ( barIndex == 0 )
|
|
2060
|
+
if ( barIndex == 0 ) {
|
|
2061
|
+
if ( isDualHorizontal )
|
|
2062
|
+
_ctx.moveTo( ...radialXY( 0, 0 ) );
|
|
2029
2063
|
_ctx.lineTo( ...radialXY( 0, ( posX < 0 ? nextBarAvg : barHeight ) ) );
|
|
2064
|
+
}
|
|
2030
2065
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
2031
2066
|
if ( posX >= 0 ) {
|
|
2032
2067
|
const point = [ posX, barHeight ];
|
|
@@ -2037,16 +2072,16 @@ export default class AudioMotionAnalyzer {
|
|
|
2037
2072
|
else { // Linear
|
|
2038
2073
|
if ( barIndex == 0 ) {
|
|
2039
2074
|
// start the line off-screen using the previous FFT bin value as the initial amplitude
|
|
2040
|
-
if ( _mirror
|
|
2075
|
+
if ( _mirror == -1 && ! isDualHorizontal )
|
|
2076
|
+
_ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
|
|
2077
|
+
else {
|
|
2041
2078
|
const prevFFTData = binLo ? this._normalizedB( fftData[ binLo - 1 ] ) * maxBarHeight : barHeight; // use previous FFT bin value, when available
|
|
2042
2079
|
_ctx.moveTo( initialX - _lineWidth, analyzerBottom - prevFFTData );
|
|
2043
2080
|
}
|
|
2044
|
-
else
|
|
2045
|
-
_ctx.moveTo( initialX, analyzerBottom - ( posX < initialX ? nextBarAvg : barHeight ) );
|
|
2046
2081
|
}
|
|
2047
2082
|
// draw line to the current point
|
|
2048
2083
|
// avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
|
|
2049
|
-
if ( _mirror != -1 || posX >= initialX )
|
|
2084
|
+
if ( isDualHorizontal || _mirror != -1 || posX >= initialX )
|
|
2050
2085
|
_ctx.lineTo( posX, analyzerBottom - barHeight );
|
|
2051
2086
|
}
|
|
2052
2087
|
}
|
|
@@ -2086,7 +2121,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2086
2121
|
_ctx.beginPath();
|
|
2087
2122
|
_ctx.moveTo( posX, y );
|
|
2088
2123
|
_ctx.lineTo( posX, y - barHeight );
|
|
2089
|
-
_ctx.arc( barCenter, y - barHeight, halfWidth,
|
|
2124
|
+
_ctx.arc( barCenter, y - barHeight, halfWidth, PI, TAU );
|
|
2090
2125
|
_ctx.lineTo( posX + width, y );
|
|
2091
2126
|
strokeIf( isOutline );
|
|
2092
2127
|
_ctx.fill();
|
|
@@ -2118,12 +2153,12 @@ export default class AudioMotionAnalyzer {
|
|
|
2118
2153
|
if ( isLeds ) {
|
|
2119
2154
|
const ledPeak = ledPosY( peak );
|
|
2120
2155
|
if ( ledPeak >= ledSpaceV ) // avoid peak below first led
|
|
2121
|
-
_ctx.fillRect( posX,
|
|
2156
|
+
_ctx.fillRect( posX, analyzerBottom - ledPeak, width, ledHeight );
|
|
2122
2157
|
}
|
|
2123
2158
|
else if ( ! _radial )
|
|
2124
2159
|
_ctx.fillRect( posX, analyzerBottom - peak * maxBarHeight, width, 2 );
|
|
2125
|
-
else if ( _mode !=
|
|
2126
|
-
radialPoly( posX, peak * maxBarHeight
|
|
2160
|
+
else if ( _mode != MODE_GRAPH ) // radial - peaks for graph mode are done by the peak line code
|
|
2161
|
+
radialPoly( posX, peak * maxBarHeight, width, -2 );
|
|
2127
2162
|
}
|
|
2128
2163
|
|
|
2129
2164
|
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
@@ -2132,16 +2167,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2132
2167
|
if ( ! useCanvas )
|
|
2133
2168
|
continue;
|
|
2134
2169
|
|
|
2135
|
-
_ctx.restore(); // restore clip region
|
|
2136
|
-
|
|
2137
2170
|
// restore global alpha
|
|
2138
2171
|
_ctx.globalAlpha = 1;
|
|
2139
2172
|
|
|
2140
|
-
// Fill/stroke drawing path for mode
|
|
2141
|
-
if ( _mode ==
|
|
2173
|
+
// Fill/stroke drawing path for graph mode
|
|
2174
|
+
if ( _mode == MODE_GRAPH ) {
|
|
2142
2175
|
setBarColor(); // select channel gradient
|
|
2143
2176
|
|
|
2144
|
-
if ( _radial ) {
|
|
2177
|
+
if ( _radial && ! isDualHorizontal ) {
|
|
2145
2178
|
if ( _mirror ) {
|
|
2146
2179
|
let p;
|
|
2147
2180
|
while ( p = points.pop() )
|
|
@@ -2156,10 +2189,13 @@ export default class AudioMotionAnalyzer {
|
|
|
2156
2189
|
if ( fillAlpha > 0 ) {
|
|
2157
2190
|
if ( _radial ) {
|
|
2158
2191
|
// exclude the center circle from the fill area
|
|
2159
|
-
|
|
2160
|
-
|
|
2192
|
+
const start = isDualHorizontal ? getAngle( analyzerWidth >> 1 ) : 0,
|
|
2193
|
+
end = isDualHorizontal ? getAngle( analyzerWidth ) : TAU;
|
|
2194
|
+
_ctx.moveTo( ...radialXY( isDualHorizontal ? analyzerWidth >> 1 : 0, 0 ) );
|
|
2195
|
+
_ctx.arc( centerX, centerY, innerRadius, start, end, isDualHorizontal ? ! invertedChannel : true );
|
|
2161
2196
|
}
|
|
2162
|
-
else {
|
|
2197
|
+
else {
|
|
2198
|
+
// close the fill area
|
|
2163
2199
|
_ctx.lineTo( finalX, analyzerBottom );
|
|
2164
2200
|
_ctx.lineTo( initialX, analyzerBottom );
|
|
2165
2201
|
}
|
|
@@ -2182,14 +2218,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2182
2218
|
h = findY( x, h, nextBar.posX, nextBar.peak[ channel ], 0 );
|
|
2183
2219
|
x = 0;
|
|
2184
2220
|
}
|
|
2185
|
-
h *= maxBarHeight
|
|
2221
|
+
h *= maxBarHeight;
|
|
2186
2222
|
if ( showPeakLine ) {
|
|
2187
2223
|
_ctx[ m ]( ...( _radial ? radialXY( x, h ) : [ x, analyzerBottom - h ] ) );
|
|
2188
|
-
if ( _radial && _mirror )
|
|
2224
|
+
if ( _radial && _mirror && ! isDualHorizontal )
|
|
2189
2225
|
points.push( [ x, h ] );
|
|
2190
2226
|
}
|
|
2191
|
-
else if ( h )
|
|
2192
|
-
radialPoly( x, h, 1, -2
|
|
2227
|
+
else if ( h > 0 )
|
|
2228
|
+
radialPoly( x, h, 1, -2 ); // standard peaks (also does mirror)
|
|
2193
2229
|
});
|
|
2194
2230
|
if ( showPeakLine ) {
|
|
2195
2231
|
let p;
|
|
@@ -2201,8 +2237,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2201
2237
|
}
|
|
2202
2238
|
}
|
|
2203
2239
|
|
|
2204
|
-
//
|
|
2205
|
-
|
|
2240
|
+
_ctx.restore(); // restore clip region
|
|
2241
|
+
|
|
2242
|
+
if ( isDualHorizontal && ! _radial )
|
|
2243
|
+
_ctx.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
2244
|
+
|
|
2245
|
+
// create Reflex effect - for dual-combined and dual-horizontal do it only once, after channel 1
|
|
2246
|
+
if ( ( ! isDualHorizontal && ! isDualCombined ) || channel )
|
|
2247
|
+
doReflex( channel );
|
|
2206
2248
|
|
|
2207
2249
|
} // for ( let channel = 0; channel < nChannels; channel++ ) {
|
|
2208
2250
|
|
|
@@ -2210,7 +2252,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2210
2252
|
|
|
2211
2253
|
if ( useCanvas ) {
|
|
2212
2254
|
// Mirror effect
|
|
2213
|
-
if ( _mirror && ! _radial ) {
|
|
2255
|
+
if ( _mirror && ! _radial && ! isDualHorizontal ) {
|
|
2214
2256
|
_ctx.setTransform( -1, 0, 0, 1, canvas.width - initialX, 0 );
|
|
2215
2257
|
_ctx.drawImage( canvas, initialX, 0, centerX, canvas.height, 0, 0, centerX, canvas.height );
|
|
2216
2258
|
_ctx.setTransform( 1, 0, 0, 1, 0, 0 );
|
|
@@ -2223,8 +2265,14 @@ export default class AudioMotionAnalyzer {
|
|
|
2223
2265
|
drawScaleX();
|
|
2224
2266
|
}
|
|
2225
2267
|
|
|
2226
|
-
//
|
|
2227
|
-
|
|
2268
|
+
// display current frame rate
|
|
2269
|
+
if ( this.showFPS ) {
|
|
2270
|
+
const size = canvasX.height;
|
|
2271
|
+
_ctx.font = `bold ${size}px ${FONT_FAMILY}`;
|
|
2272
|
+
_ctx.fillStyle = FPS_COLOR;
|
|
2273
|
+
_ctx.textAlign = 'right';
|
|
2274
|
+
_ctx.fillText( Math.round( _fps ), canvas.width - size, size * 2 );
|
|
2275
|
+
}
|
|
2228
2276
|
|
|
2229
2277
|
// call callback function, if defined
|
|
2230
2278
|
if ( this.onCanvasDraw ) {
|
|
@@ -2269,13 +2317,10 @@ export default class AudioMotionAnalyzer {
|
|
|
2269
2317
|
return;
|
|
2270
2318
|
|
|
2271
2319
|
const { canvas, _ctx, _radial, _reflexRatio } = this,
|
|
2272
|
-
{ analyzerWidth, initialX,
|
|
2320
|
+
{ analyzerWidth, centerX, centerY, initialX, innerRadius, outerRadius } = this._aux,
|
|
2273
2321
|
{ isLumi } = this._flg,
|
|
2274
2322
|
isDualVertical = this._chLayout == CHANNEL_VERTICAL,
|
|
2275
2323
|
analyzerRatio = 1 - _reflexRatio,
|
|
2276
|
-
centerX = canvas.width >> 1,
|
|
2277
|
-
centerY = canvas.height >> 1,
|
|
2278
|
-
maxRadius = Math.min( centerX, centerY ),
|
|
2279
2324
|
gradientHeight = isLumi ? canvas.height : canvas.height * ( 1 - _reflexRatio * ( ! isDualVertical ) ) | 0;
|
|
2280
2325
|
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
2281
2326
|
|
|
@@ -2287,7 +2332,7 @@ export default class AudioMotionAnalyzer {
|
|
|
2287
2332
|
let grad;
|
|
2288
2333
|
|
|
2289
2334
|
if ( _radial )
|
|
2290
|
-
grad = _ctx.createRadialGradient( centerX, centerY,
|
|
2335
|
+
grad = _ctx.createRadialGradient( centerX, centerY, outerRadius, centerX, centerY, innerRadius - ( outerRadius - innerRadius ) * isDualVertical );
|
|
2291
2336
|
else
|
|
2292
2337
|
grad = _ctx.createLinearGradient( ...( isHorizontal ? [ initialX, 0, initialX + analyzerWidth, 0 ] : [ 0, 0, 0, gradientHeight ] ) );
|
|
2293
2338
|
|
package/src/index.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export interface ConstructorOptions extends Options {
|
|
|
88
88
|
source?: HTMLMediaElement | AudioNode;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export type ChannelLayout = "single" | "dual-vertical" | "dual-combined";
|
|
91
|
+
export type ChannelLayout = "single" | "dual-horizontal" | "dual-vertical" | "dual-combined";
|
|
92
92
|
|
|
93
93
|
export type ColorMode = "gradient" | "bar-index" | "bar-level";
|
|
94
94
|
|