audiomotion-analyzer 4.5.2 → 5.0.0-alpha.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 +1078 -738
- package/dist/index.js +952 -603
- package/package.json +1 -1
- package/src/audioMotion-analyzer.js +964 -658
- package/src/index.d.ts +117 -81
package/dist/index.js
CHANGED
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
Object.defineProperty(_exports, "__esModule", {
|
|
17
17
|
value: true
|
|
18
18
|
});
|
|
19
|
-
_exports.default = _exports.AudioMotionAnalyzer = void 0;
|
|
19
|
+
_exports.default = _exports.SCALE_MEL = _exports.SCALE_LOG = _exports.SCALE_LINEAR = _exports.SCALE_BARK = _exports.REASON_USER = _exports.REASON_RESIZE = _exports.REASON_LORES = _exports.REASON_FULLSCREENCHANGE = _exports.REASON_CREATE = _exports.RADIAL_OUTER = _exports.RADIAL_OFF = _exports.RADIAL_INNER = _exports.PEAKS_OFF = _exports.PEAKS_FADE = _exports.PEAKS_DROP = _exports.MODE_GRAPH = _exports.MODE_BARS = _exports.LEDS_VINTAGE = _exports.LEDS_OFF = _exports.LEDS_MODERN = _exports.LAYOUT_VERTICAL = _exports.LAYOUT_SINGLE = _exports.LAYOUT_HORIZONTAL = _exports.LAYOUT_COMBINED = _exports.FILTER_NONE = _exports.FILTER_D = _exports.FILTER_C = _exports.FILTER_B = _exports.FILTER_A = _exports.FILTER_468 = _exports.ERR_INVALID_AUDIO_SOURCE = _exports.ERR_INVALID_AUDIO_CONTEXT = _exports.ERR_AUDIO_CONTEXT_FAIL = _exports.ENERGY_TREBLE = _exports.ENERGY_PEAK = _exports.ENERGY_MIDRANGE = _exports.ENERGY_LOWMID = _exports.ENERGY_HIGHMID = _exports.ENERGY_BASS = _exports.COLORMODE_LEVEL = _exports.COLORMODE_INDEX = _exports.COLORMODE_GRADIENT = _exports.BANDS_OCTAVE_HALF = _exports.BANDS_OCTAVE_FULL = _exports.BANDS_OCTAVE_8TH = _exports.BANDS_OCTAVE_6TH = _exports.BANDS_OCTAVE_4TH = _exports.BANDS_OCTAVE_3RD = _exports.BANDS_OCTAVE_24TH = _exports.BANDS_OCTAVE_12TH = _exports.BANDS_FFT = _exports.AudioMotionAnalyzer = _exports.ALPHABARS_ON = _exports.ALPHABARS_OFF = _exports.ALPHABARS_FULL = void 0;
|
|
20
20
|
/**!
|
|
21
21
|
* audioMotion-analyzer
|
|
22
22
|
* High-resolution real-time graphic audio spectrum analyzer JS module
|
|
23
23
|
*
|
|
24
|
-
* @version
|
|
24
|
+
* @version 5.0.0-alpha.0
|
|
25
25
|
* @author Henrique Avila Vianna <hvianna@gmail.com> <https://henriquevianna.com>
|
|
26
26
|
* @license AGPL-3.0-or-later
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
const VERSION = '
|
|
29
|
+
const VERSION = '5.0.0-alpha.0';
|
|
30
30
|
|
|
31
31
|
// internal constants
|
|
32
32
|
const PI = Math.PI,
|
|
@@ -34,161 +34,242 @@
|
|
|
34
34
|
HALF_PI = PI / 2,
|
|
35
35
|
C_1 = 8.17579892; // frequency for C -1
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
CHANNEL_COMBINED = 'dual-combined',
|
|
39
|
-
CHANNEL_HORIZONTAL = 'dual-horizontal',
|
|
40
|
-
CHANNEL_SINGLE = 'single',
|
|
41
|
-
CHANNEL_VERTICAL = 'dual-vertical',
|
|
42
|
-
COLOR_BAR_INDEX = 'bar-index',
|
|
43
|
-
COLOR_BAR_LEVEL = 'bar-level',
|
|
44
|
-
COLOR_GRADIENT = 'gradient',
|
|
45
|
-
DEBOUNCE_TIMEOUT = 60,
|
|
37
|
+
const DEBOUNCE_TIMEOUT = 60,
|
|
46
38
|
EVENT_CLICK = 'click',
|
|
47
39
|
EVENT_FULLSCREENCHANGE = 'fullscreenchange',
|
|
48
40
|
EVENT_RESIZE = 'resize',
|
|
49
|
-
GRADIENT_DEFAULT_BGCOLOR = '#111',
|
|
50
|
-
FILTER_NONE = '',
|
|
51
|
-
FILTER_A = 'A',
|
|
52
|
-
FILTER_B = 'B',
|
|
53
|
-
FILTER_C = 'C',
|
|
54
|
-
FILTER_D = 'D',
|
|
55
|
-
FILTER_468 = '468',
|
|
56
41
|
FONT_FAMILY = 'sans-serif',
|
|
57
42
|
FPS_COLOR = '#0f0',
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
43
|
+
MIN_AXIS_DIMENSION = 20,
|
|
44
|
+
OPTION_EMPTY = '',
|
|
45
|
+
OPTION_OFF = 'off',
|
|
46
|
+
OPTION_ON = 'on';
|
|
47
|
+
|
|
48
|
+
// exported constants
|
|
49
|
+
const ALPHABARS_FULL = _exports.ALPHABARS_FULL = 'full',
|
|
50
|
+
ALPHABARS_OFF = _exports.ALPHABARS_OFF = OPTION_OFF,
|
|
51
|
+
ALPHABARS_ON = _exports.ALPHABARS_ON = OPTION_ON,
|
|
52
|
+
BANDS_FFT = _exports.BANDS_FFT = 0,
|
|
53
|
+
BANDS_OCTAVE_FULL = _exports.BANDS_OCTAVE_FULL = 1,
|
|
54
|
+
BANDS_OCTAVE_HALF = _exports.BANDS_OCTAVE_HALF = 2,
|
|
55
|
+
BANDS_OCTAVE_3RD = _exports.BANDS_OCTAVE_3RD = 3,
|
|
56
|
+
BANDS_OCTAVE_4TH = _exports.BANDS_OCTAVE_4TH = 4,
|
|
57
|
+
BANDS_OCTAVE_6TH = _exports.BANDS_OCTAVE_6TH = 5,
|
|
58
|
+
BANDS_OCTAVE_8TH = _exports.BANDS_OCTAVE_8TH = 6,
|
|
59
|
+
BANDS_OCTAVE_12TH = _exports.BANDS_OCTAVE_12TH = 7,
|
|
60
|
+
BANDS_OCTAVE_24TH = _exports.BANDS_OCTAVE_24TH = 8,
|
|
61
|
+
COLORMODE_GRADIENT = _exports.COLORMODE_GRADIENT = 'gradient',
|
|
62
|
+
COLORMODE_INDEX = _exports.COLORMODE_INDEX = 'bar-index',
|
|
63
|
+
COLORMODE_LEVEL = _exports.COLORMODE_LEVEL = 'bar-level',
|
|
64
|
+
ENERGY_BASS = _exports.ENERGY_BASS = 'bass',
|
|
65
|
+
ENERGY_HIGHMID = _exports.ENERGY_HIGHMID = 'highMid',
|
|
66
|
+
ENERGY_LOWMID = _exports.ENERGY_LOWMID = 'lowMid',
|
|
67
|
+
ENERGY_MIDRANGE = _exports.ENERGY_MIDRANGE = 'mid',
|
|
68
|
+
ENERGY_PEAK = _exports.ENERGY_PEAK = 'peak',
|
|
69
|
+
ENERGY_TREBLE = _exports.ENERGY_TREBLE = 'treble',
|
|
70
|
+
ERR_AUDIO_CONTEXT_FAIL = _exports.ERR_AUDIO_CONTEXT_FAIL = 1,
|
|
71
|
+
ERR_INVALID_AUDIO_CONTEXT = _exports.ERR_INVALID_AUDIO_CONTEXT = 2,
|
|
72
|
+
ERR_INVALID_AUDIO_SOURCE = _exports.ERR_INVALID_AUDIO_SOURCE = 3,
|
|
73
|
+
FILTER_NONE = _exports.FILTER_NONE = OPTION_EMPTY,
|
|
74
|
+
FILTER_A = _exports.FILTER_A = 'A',
|
|
75
|
+
FILTER_B = _exports.FILTER_B = 'B',
|
|
76
|
+
FILTER_C = _exports.FILTER_C = 'C',
|
|
77
|
+
FILTER_D = _exports.FILTER_D = 'D',
|
|
78
|
+
FILTER_468 = _exports.FILTER_468 = '468',
|
|
79
|
+
LAYOUT_COMBINED = _exports.LAYOUT_COMBINED = 'dual-combined',
|
|
80
|
+
LAYOUT_HORIZONTAL = _exports.LAYOUT_HORIZONTAL = 'dual-horizontal',
|
|
81
|
+
LAYOUT_SINGLE = _exports.LAYOUT_SINGLE = 'single',
|
|
82
|
+
LAYOUT_VERTICAL = _exports.LAYOUT_VERTICAL = 'dual-vertical',
|
|
83
|
+
LEDS_MODERN = _exports.LEDS_MODERN = 'modern',
|
|
84
|
+
LEDS_OFF = _exports.LEDS_OFF = OPTION_OFF,
|
|
85
|
+
LEDS_VINTAGE = _exports.LEDS_VINTAGE = 'vintage',
|
|
86
|
+
MODE_BARS = _exports.MODE_BARS = 'bars',
|
|
87
|
+
MODE_GRAPH = _exports.MODE_GRAPH = 'graph',
|
|
88
|
+
PEAKS_DROP = _exports.PEAKS_DROP = 'drop',
|
|
89
|
+
PEAKS_FADE = _exports.PEAKS_FADE = 'fade',
|
|
90
|
+
PEAKS_OFF = _exports.PEAKS_OFF = OPTION_OFF,
|
|
91
|
+
RADIAL_INNER = _exports.RADIAL_INNER = -1,
|
|
92
|
+
RADIAL_OFF = _exports.RADIAL_OFF = 0,
|
|
93
|
+
RADIAL_OUTER = _exports.RADIAL_OUTER = 1,
|
|
94
|
+
REASON_CREATE = _exports.REASON_CREATE = 'create',
|
|
95
|
+
REASON_FULLSCREENCHANGE = _exports.REASON_FULLSCREENCHANGE = EVENT_FULLSCREENCHANGE,
|
|
96
|
+
REASON_LORES = _exports.REASON_LORES = 'lores',
|
|
97
|
+
REASON_RESIZE = _exports.REASON_RESIZE = EVENT_RESIZE,
|
|
98
|
+
REASON_USER = _exports.REASON_USER = 'user',
|
|
99
|
+
SCALE_BARK = _exports.SCALE_BARK = 'bark',
|
|
100
|
+
SCALE_LINEAR = _exports.SCALE_LINEAR = 'linear',
|
|
101
|
+
SCALE_LOG = _exports.SCALE_LOG = 'log',
|
|
102
|
+
SCALE_MEL = _exports.SCALE_MEL = 'mel';
|
|
103
|
+
|
|
104
|
+
// built-in color themes
|
|
76
105
|
const PRISM = ['#a35', '#c66', '#e94', '#ed0', '#9d5', '#4d8', '#2cb', '#0bc', '#09c', '#36b'],
|
|
77
|
-
|
|
106
|
+
THEMES = [['classic', {
|
|
78
107
|
colorStops: ['red', {
|
|
79
108
|
color: 'yellow',
|
|
80
|
-
level: .
|
|
81
|
-
pos: .6
|
|
109
|
+
level: .9
|
|
82
110
|
}, {
|
|
83
111
|
color: 'lime',
|
|
84
|
-
level: .
|
|
112
|
+
level: .6
|
|
85
113
|
}]
|
|
114
|
+
}], ['mono', {
|
|
115
|
+
colorStops: ['#eee'],
|
|
116
|
+
peakColor: 'red'
|
|
86
117
|
}], ['prism', {
|
|
87
118
|
colorStops: PRISM
|
|
88
119
|
}], ['rainbow', {
|
|
89
|
-
dir: 'h',
|
|
90
120
|
colorStops: ['#817', ...PRISM, '#639']
|
|
91
121
|
}], ['orangered', {
|
|
92
|
-
bgColor: '#3e2f29',
|
|
93
122
|
colorStops: ['OrangeRed']
|
|
94
123
|
}], ['steelblue', {
|
|
95
|
-
bgColor: '#222c35',
|
|
96
124
|
colorStops: ['SteelBlue']
|
|
97
125
|
}]];
|
|
98
126
|
|
|
99
127
|
// settings defaults
|
|
100
128
|
const DEFAULT_SETTINGS = {
|
|
101
|
-
alphaBars:
|
|
129
|
+
alphaBars: ALPHABARS_OFF,
|
|
102
130
|
ansiBands: false,
|
|
131
|
+
bandResolution: BANDS_FFT,
|
|
103
132
|
barSpace: 0.1,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
colorMode: COLOR_GRADIENT,
|
|
107
|
-
fadePeaks: false,
|
|
133
|
+
channelLayout: LAYOUT_SINGLE,
|
|
134
|
+
colorMode: COLORMODE_GRADIENT,
|
|
108
135
|
fftSize: 8192,
|
|
109
|
-
fillAlpha:
|
|
136
|
+
fillAlpha: 0.5,
|
|
110
137
|
frequencyScale: SCALE_LOG,
|
|
111
|
-
gradient: GRADIENTS[0][0],
|
|
112
|
-
gravity: 3.8,
|
|
113
138
|
height: undefined,
|
|
114
|
-
ledBars:
|
|
139
|
+
ledBars: LEDS_OFF,
|
|
115
140
|
linearAmplitude: false,
|
|
116
141
|
linearBoost: 1,
|
|
117
|
-
lineWidth:
|
|
142
|
+
lineWidth: 1,
|
|
118
143
|
loRes: false,
|
|
119
|
-
lumiBars: false,
|
|
120
144
|
maxDecibels: -25,
|
|
121
145
|
maxFPS: 0,
|
|
122
146
|
maxFreq: 22000,
|
|
123
147
|
minDecibels: -85,
|
|
124
148
|
minFreq: 20,
|
|
125
149
|
mirror: 0,
|
|
126
|
-
mode:
|
|
150
|
+
mode: MODE_BARS,
|
|
127
151
|
noteLabels: false,
|
|
128
152
|
outlineBars: false,
|
|
129
|
-
|
|
130
|
-
peakFadeTime: 750,
|
|
153
|
+
peakDecayTime: 750,
|
|
131
154
|
peakHoldTime: 500,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
radius: 0.
|
|
155
|
+
peaks: PEAKS_DROP,
|
|
156
|
+
peakLine: 0,
|
|
157
|
+
radial: RADIAL_OFF,
|
|
158
|
+
radius: 0.5,
|
|
136
159
|
reflexAlpha: 0.15,
|
|
137
160
|
reflexBright: 1,
|
|
138
161
|
reflexFit: true,
|
|
139
162
|
reflexRatio: 0,
|
|
140
163
|
roundBars: false,
|
|
141
|
-
showBgColor: true,
|
|
142
164
|
showFPS: false,
|
|
143
|
-
|
|
165
|
+
showLedMask: true,
|
|
144
166
|
showScaleX: true,
|
|
145
167
|
showScaleY: false,
|
|
146
168
|
smoothing: 0.5,
|
|
147
169
|
spinSpeed: 0,
|
|
148
|
-
|
|
149
|
-
start: true,
|
|
150
|
-
trueLeds: false,
|
|
170
|
+
spreadGradient: false,
|
|
151
171
|
useCanvas: true,
|
|
152
172
|
volume: 1,
|
|
153
173
|
weightingFilter: FILTER_NONE,
|
|
154
174
|
width: undefined
|
|
155
175
|
};
|
|
176
|
+
const DEFAULT_LED_PARAMETERS = [8, 8],
|
|
177
|
+
// ledHeight, gapHeight
|
|
178
|
+
DEFAULT_LEDMASK_PARAMS = [.2, -1, 20]; // alpha, lightness, saturation
|
|
179
|
+
|
|
180
|
+
const DEFAULT_THEME_MODIFIERS = {
|
|
181
|
+
horizontal: false,
|
|
182
|
+
reverse: false
|
|
183
|
+
};
|
|
156
184
|
|
|
157
185
|
// custom error messages
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
ERR_REFLEX_OUT_OF_RANGE = ['ERR_REFLEX_OUT_OF_RANGE', 'Reflex ratio must be >= 0 and < 1'],
|
|
164
|
-
ERR_INVALID_AUDIO_SOURCE = ['ERR_INVALID_AUDIO_SOURCE', 'Audio source must be an instance of HTMLMediaElement or AudioNode'],
|
|
165
|
-
ERR_GRADIENT_INVALID_NAME = ['ERR_GRADIENT_INVALID_NAME', 'Gradient name must be a non-empty string'],
|
|
166
|
-
ERR_GRADIENT_NOT_AN_OBJECT = ['ERR_GRADIENT_NOT_AN_OBJECT', 'Gradient options must be an object'],
|
|
167
|
-
ERR_GRADIENT_MISSING_COLOR = ['ERR_GRADIENT_MISSING_COLOR', 'Gradient colorStops must be a non-empty array'];
|
|
186
|
+
const ERROR_MESSAGE = {
|
|
187
|
+
[ERR_AUDIO_CONTEXT_FAIL]: 'Could not create audio context. Web Audio API not supported?',
|
|
188
|
+
[ERR_INVALID_AUDIO_CONTEXT]: 'Provided audio context is not valid',
|
|
189
|
+
[ERR_INVALID_AUDIO_SOURCE]: 'Audio source must be an instance of HTMLMediaElement or AudioNode'
|
|
190
|
+
};
|
|
168
191
|
class AudioMotionError extends Error {
|
|
169
|
-
constructor(
|
|
170
|
-
|
|
171
|
-
super(message + (value !== undefined ? `: ${value}` : ''));
|
|
192
|
+
constructor(code, value) {
|
|
193
|
+
super(ERROR_MESSAGE[code] + (value !== undefined ? `: ${value}` : ''));
|
|
172
194
|
this.name = 'AudioMotionError';
|
|
173
195
|
this.code = code;
|
|
174
196
|
}
|
|
175
197
|
}
|
|
176
198
|
|
|
177
|
-
|
|
199
|
+
/* helper functions */
|
|
200
|
+
|
|
201
|
+
// clamp a given value between `min` and `max`
|
|
202
|
+
const clamp = (val, min, max) => val <= min ? min : val >= max ? max : val; // TO-DO: handle +val == NaN
|
|
203
|
+
|
|
204
|
+
// convert any CSS color format to HSL format
|
|
205
|
+
const cssColorToHSL = color => {
|
|
206
|
+
const ctx = document.createElement('canvas').getContext('2d'); // use a canvas to convert any CSS color to RGB format
|
|
207
|
+
ctx.fillStyle = color;
|
|
208
|
+
const computedColor = ctx.fillStyle,
|
|
209
|
+
// hex string (#ffffff) - TO-DO: if original color has alpha channel this will be in rgba() format
|
|
210
|
+
[r, g, b] = computedColor.match(/[^#]{2}/g).map(n => parseInt(n, 16) / 255),
|
|
211
|
+
max = Math.max(r, g, b),
|
|
212
|
+
min = Math.min(r, g, b);
|
|
213
|
+
let h,
|
|
214
|
+
s,
|
|
215
|
+
l = (max + min) / 2;
|
|
216
|
+
if (max === min) h = s = 0; // achromatic
|
|
217
|
+
else {
|
|
218
|
+
const d = max - min;
|
|
219
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
220
|
+
switch (max) {
|
|
221
|
+
case r:
|
|
222
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
|
223
|
+
break;
|
|
224
|
+
case g:
|
|
225
|
+
h = (b - r) / d + 2;
|
|
226
|
+
break;
|
|
227
|
+
case b:
|
|
228
|
+
h = (r - g) / d + 4;
|
|
229
|
+
}
|
|
230
|
+
h *= 60;
|
|
231
|
+
}
|
|
232
|
+
return [h, Math.round(s * 100), Math.round(l * 100)];
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// deep clone object
|
|
236
|
+
const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
|
|
237
|
+
|
|
238
|
+
// output deprecation warning message on console
|
|
178
239
|
const deprecate = (name, alternative) => console.warn(`${name} is deprecated. Use ${alternative} instead.`);
|
|
179
240
|
|
|
180
|
-
//
|
|
241
|
+
// find the Y-coordinate of a point located between two other points, given its X-coordinate
|
|
242
|
+
const findY = (x1, y1, x2, y2, x) => y1 + (y2 - y1) * (x - x1) / (x2 - x1);
|
|
243
|
+
|
|
244
|
+
// shorthand for Array.isArray()
|
|
245
|
+
const {
|
|
246
|
+
isArray
|
|
247
|
+
} = Array;
|
|
248
|
+
|
|
249
|
+
// check if a given object is empty (also returns `true` on null, undefined or any non-object value)
|
|
181
250
|
const isEmpty = obj => {
|
|
182
251
|
for (const p in obj) return false;
|
|
183
252
|
return true;
|
|
184
253
|
};
|
|
185
254
|
|
|
186
|
-
//
|
|
255
|
+
// check if given value is numeric
|
|
256
|
+
const isNumeric = val => !isArray(val) && val == +val; // note: +[] == []
|
|
257
|
+
|
|
258
|
+
// check if given value is an object (not null or array, which are also considered objects)
|
|
259
|
+
const isObject = val => typeof val == 'object' && !!val && !isArray(val);
|
|
260
|
+
|
|
261
|
+
// check if given value is a valid channel number
|
|
262
|
+
const isValidChannel = channel => isNumeric(channel) && [0, 1].includes(+channel);
|
|
263
|
+
|
|
264
|
+
// validate a given value with an array of strings (by default, all lowercase)
|
|
187
265
|
// returns the validated value, or the first element of `list` if `value` is not found in the array
|
|
188
266
|
const validateFromList = (value, list, modifier = 'toLowerCase') => list[Math.max(0, list.indexOf(('' + value)[modifier]()))];
|
|
189
267
|
|
|
190
|
-
//
|
|
191
|
-
const
|
|
268
|
+
// returns an array with the given channel number if valid, or [0,1] otherwise
|
|
269
|
+
const validateChannelArray = channel => isValidChannel(channel) ? [+channel] : [0, 1];
|
|
270
|
+
|
|
271
|
+
// output invalid value warning message on console
|
|
272
|
+
const warnInvalid = (name, value) => console.warn(`${name}: ignoring invalid value (${value})`);
|
|
192
273
|
|
|
193
274
|
// Polyfill for Array.findLastIndex()
|
|
194
275
|
if (!Array.prototype.findLastIndex) {
|
|
@@ -201,7 +282,7 @@
|
|
|
201
282
|
};
|
|
202
283
|
}
|
|
203
284
|
|
|
204
|
-
|
|
285
|
+
/* *********************************** class AudioMotionAnalyzer ************************************ */
|
|
205
286
|
|
|
206
287
|
class AudioMotionAnalyzer {
|
|
207
288
|
/**
|
|
@@ -216,7 +297,8 @@
|
|
|
216
297
|
|
|
217
298
|
// Initialize internal objects
|
|
218
299
|
this._aux = {}; // auxiliary variables
|
|
219
|
-
this.
|
|
300
|
+
this._activeThemes = []; // currently active themes for channels 0 and 1 (refer to _makeGrad() for object structure)
|
|
301
|
+
this._bars = [];
|
|
220
302
|
this._destroyed = false;
|
|
221
303
|
this._energy = {
|
|
222
304
|
val: 0,
|
|
@@ -225,12 +307,14 @@
|
|
|
225
307
|
};
|
|
226
308
|
this._flg = {}; // flags
|
|
227
309
|
this._fps = 0;
|
|
228
|
-
this._gradients = {}; // registered gradients
|
|
229
310
|
this._last = 0; // timestamp of last rendered frame
|
|
311
|
+
this._leds = []; // currently effective led attributes (ledCount, ledHeight, ledGap)
|
|
230
312
|
this._outNodes = []; // output nodes
|
|
231
313
|
this._ownContext = false;
|
|
232
|
-
this._selectedGrads = []; // names of the currently selected gradients for channels 0 and 1
|
|
233
314
|
this._sources = []; // input nodes
|
|
315
|
+
this._themes = {}; // registered color themes
|
|
316
|
+
this._xAxis = {}; // X-axis label parameters
|
|
317
|
+
this._yAxis = {}; // Y-axis label parameters
|
|
234
318
|
|
|
235
319
|
// Check if options object passed as first argument
|
|
236
320
|
if (!(container instanceof Element)) {
|
|
@@ -244,8 +328,8 @@
|
|
|
244
328
|
canvas.style = 'max-width: 100%;';
|
|
245
329
|
this._ctx = canvas.getContext('2d');
|
|
246
330
|
|
|
247
|
-
// Register built-in
|
|
248
|
-
for (const [name, options] of
|
|
331
|
+
// Register built-in color themes
|
|
332
|
+
for (const [name, options] of THEMES) this.registerTheme(name, options);
|
|
249
333
|
|
|
250
334
|
// Set container
|
|
251
335
|
this._container = container || !this._ownCanvas && canvas.parentElement || document.body;
|
|
@@ -356,7 +440,7 @@
|
|
|
356
440
|
if (this._fsTimeout) window.clearTimeout(this._fsTimeout);
|
|
357
441
|
|
|
358
442
|
// update the canvas
|
|
359
|
-
this._setCanvas(
|
|
443
|
+
this._setCanvas(REASON_FULLSCREENCHANGE);
|
|
360
444
|
|
|
361
445
|
// delay clearing the flag to prevent any shortly following resize event
|
|
362
446
|
this._fsTimeout = window.setTimeout(() => {
|
|
@@ -383,9 +467,17 @@
|
|
|
383
467
|
signal
|
|
384
468
|
});
|
|
385
469
|
|
|
386
|
-
//
|
|
470
|
+
// Initialize default properties
|
|
471
|
+
this.setTheme();
|
|
472
|
+
this.setXAxis();
|
|
473
|
+
this.setYAxis();
|
|
474
|
+
|
|
475
|
+
// Set configuration options passed to the constructor and use defaults for any missing properties
|
|
387
476
|
this._setProps(options, true);
|
|
388
477
|
|
|
478
|
+
// Start the analyzer, unless `start` is explicitly set to false in the options
|
|
479
|
+
this.toggleAnalyzer(options.start !== false);
|
|
480
|
+
|
|
389
481
|
// Add canvas to the container (only when canvas not provided by user)
|
|
390
482
|
if (this.useCanvas && this._ownCanvas) this._container.appendChild(canvas);
|
|
391
483
|
|
|
@@ -406,8 +498,9 @@
|
|
|
406
498
|
return this._alphaBars;
|
|
407
499
|
}
|
|
408
500
|
set alphaBars(value) {
|
|
409
|
-
this._alphaBars =
|
|
501
|
+
this._alphaBars = validateFromList(value, [ALPHABARS_OFF, ALPHABARS_ON, ALPHABARS_FULL]);
|
|
410
502
|
this._calcBars();
|
|
503
|
+
this._makeGrad();
|
|
411
504
|
}
|
|
412
505
|
get ansiBands() {
|
|
413
506
|
return this._ansiBands;
|
|
@@ -416,6 +509,13 @@
|
|
|
416
509
|
this._ansiBands = !!value;
|
|
417
510
|
this._calcBars();
|
|
418
511
|
}
|
|
512
|
+
get bandResolution() {
|
|
513
|
+
return this._bandRes;
|
|
514
|
+
}
|
|
515
|
+
set bandResolution(value) {
|
|
516
|
+
this._bandRes = clamp(value | 0, 0, 8);
|
|
517
|
+
this._calcBars();
|
|
518
|
+
}
|
|
419
519
|
get barSpace() {
|
|
420
520
|
return this._barSpace;
|
|
421
521
|
}
|
|
@@ -427,15 +527,15 @@
|
|
|
427
527
|
return this._chLayout;
|
|
428
528
|
}
|
|
429
529
|
set channelLayout(value) {
|
|
430
|
-
this._chLayout = validateFromList(value, [
|
|
530
|
+
this._chLayout = validateFromList(value, [LAYOUT_SINGLE, LAYOUT_HORIZONTAL, LAYOUT_VERTICAL, LAYOUT_COMBINED]);
|
|
431
531
|
|
|
432
532
|
// update node connections
|
|
433
533
|
this._input.disconnect();
|
|
434
|
-
this._input.connect(this._chLayout !=
|
|
534
|
+
this._input.connect(this._chLayout != LAYOUT_SINGLE ? this._splitter : this._analyzer[0]);
|
|
435
535
|
this._analyzer[0].disconnect();
|
|
436
536
|
if (this._outNodes.length)
|
|
437
537
|
// connect analyzer only if the output is connected to other nodes
|
|
438
|
-
this._analyzer[0].connect(this._chLayout !=
|
|
538
|
+
this._analyzer[0].connect(this._chLayout != LAYOUT_SINGLE ? this._merger : this._output);
|
|
439
539
|
this._calcBars();
|
|
440
540
|
this._makeGrad();
|
|
441
541
|
}
|
|
@@ -443,13 +543,7 @@
|
|
|
443
543
|
return this._colorMode;
|
|
444
544
|
}
|
|
445
545
|
set colorMode(value) {
|
|
446
|
-
this._colorMode = validateFromList(value, [
|
|
447
|
-
}
|
|
448
|
-
get fadePeaks() {
|
|
449
|
-
return this._fadePeaks;
|
|
450
|
-
}
|
|
451
|
-
set fadePeaks(value) {
|
|
452
|
-
this._fadePeaks = !!value;
|
|
546
|
+
this._colorMode = validateFromList(value, [COLORMODE_GRADIENT, COLORMODE_INDEX, COLORMODE_LEVEL]);
|
|
453
547
|
}
|
|
454
548
|
get fftSize() {
|
|
455
549
|
return this._analyzer[0].fftSize;
|
|
@@ -467,30 +561,6 @@
|
|
|
467
561
|
this._frequencyScale = validateFromList(value, [SCALE_LOG, SCALE_BARK, SCALE_MEL, SCALE_LINEAR]);
|
|
468
562
|
this._calcBars();
|
|
469
563
|
}
|
|
470
|
-
get gradient() {
|
|
471
|
-
return this._selectedGrads[0];
|
|
472
|
-
}
|
|
473
|
-
set gradient(value) {
|
|
474
|
-
this._setGradient(value);
|
|
475
|
-
}
|
|
476
|
-
get gradientLeft() {
|
|
477
|
-
return this._selectedGrads[0];
|
|
478
|
-
}
|
|
479
|
-
set gradientLeft(value) {
|
|
480
|
-
this._setGradient(value, 0);
|
|
481
|
-
}
|
|
482
|
-
get gradientRight() {
|
|
483
|
-
return this._selectedGrads[1];
|
|
484
|
-
}
|
|
485
|
-
set gradientRight(value) {
|
|
486
|
-
this._setGradient(value, 1);
|
|
487
|
-
}
|
|
488
|
-
get gravity() {
|
|
489
|
-
return this._gravity;
|
|
490
|
-
}
|
|
491
|
-
set gravity(value) {
|
|
492
|
-
this._gravity = value > 0 ? +value : this._gravity || DEFAULT_SETTINGS.gravity;
|
|
493
|
-
}
|
|
494
564
|
get height() {
|
|
495
565
|
return this._height;
|
|
496
566
|
}
|
|
@@ -499,10 +569,10 @@
|
|
|
499
569
|
this._setCanvas(REASON_USER);
|
|
500
570
|
}
|
|
501
571
|
get ledBars() {
|
|
502
|
-
return this.
|
|
572
|
+
return this._ledBars;
|
|
503
573
|
}
|
|
504
574
|
set ledBars(value) {
|
|
505
|
-
this.
|
|
575
|
+
this._ledBars = validateFromList(value, [LEDS_OFF, LEDS_MODERN, LEDS_VINTAGE]);
|
|
506
576
|
this._calcBars();
|
|
507
577
|
}
|
|
508
578
|
get linearAmplitude() {
|
|
@@ -530,14 +600,6 @@
|
|
|
530
600
|
this._loRes = !!value;
|
|
531
601
|
this._setCanvas(REASON_LORES);
|
|
532
602
|
}
|
|
533
|
-
get lumiBars() {
|
|
534
|
-
return this._lumiBars;
|
|
535
|
-
}
|
|
536
|
-
set lumiBars(value) {
|
|
537
|
-
this._lumiBars = !!value;
|
|
538
|
-
this._calcBars();
|
|
539
|
-
this._makeGrad();
|
|
540
|
-
}
|
|
541
603
|
get maxDecibels() {
|
|
542
604
|
return this._analyzer[0].maxDecibels;
|
|
543
605
|
}
|
|
@@ -554,10 +616,13 @@
|
|
|
554
616
|
return this._maxFreq;
|
|
555
617
|
}
|
|
556
618
|
set maxFreq(value) {
|
|
557
|
-
if (value
|
|
558
|
-
|
|
559
|
-
|
|
619
|
+
if (!(value > 0)) {
|
|
620
|
+
// should catch all 'falsy' and negative values (`value <= 0` would fail on NaN or undefined)
|
|
621
|
+
warnInvalid('maxFreq', value);
|
|
622
|
+
value = this._maxFreq || DEFAULT_SETTINGS.maxFreq; // keep previous value, if any
|
|
560
623
|
}
|
|
624
|
+
this._maxFreq = Math.min(value, this.audioCtx.sampleRate / 2);
|
|
625
|
+
this._calcBars();
|
|
561
626
|
}
|
|
562
627
|
get minDecibels() {
|
|
563
628
|
return this._analyzer[0].minDecibels;
|
|
@@ -569,10 +634,13 @@
|
|
|
569
634
|
return this._minFreq;
|
|
570
635
|
}
|
|
571
636
|
set minFreq(value) {
|
|
572
|
-
if (value
|
|
573
|
-
|
|
574
|
-
|
|
637
|
+
if (!(value > 0)) {
|
|
638
|
+
// should catch all 'falsy' and negative values (`value <= 0` would fail on NaN or undefined)
|
|
639
|
+
warnInvalid('minFreq', value);
|
|
640
|
+
value = this._minFreq || DEFAULT_SETTINGS.minFreq;
|
|
575
641
|
}
|
|
642
|
+
this._minFreq = +value;
|
|
643
|
+
this._calcBars();
|
|
576
644
|
}
|
|
577
645
|
get mirror() {
|
|
578
646
|
return this._mirror;
|
|
@@ -586,12 +654,9 @@
|
|
|
586
654
|
return this._mode;
|
|
587
655
|
}
|
|
588
656
|
set mode(value) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
this._calcBars();
|
|
593
|
-
this._makeGrad();
|
|
594
|
-
} else throw new AudioMotionError(ERR_INVALID_MODE, value);
|
|
657
|
+
this._mode = validateFromList(value, [MODE_BARS, MODE_GRAPH]);
|
|
658
|
+
this._calcBars();
|
|
659
|
+
this._makeGrad();
|
|
595
660
|
}
|
|
596
661
|
get noteLabels() {
|
|
597
662
|
return this._noteLabels;
|
|
@@ -607,37 +672,37 @@
|
|
|
607
672
|
this._outlineBars = !!value;
|
|
608
673
|
this._calcBars();
|
|
609
674
|
}
|
|
610
|
-
get
|
|
611
|
-
return this.
|
|
675
|
+
get peakDecayTime() {
|
|
676
|
+
return this._peakDecayTime * 1e3;
|
|
612
677
|
}
|
|
613
|
-
set
|
|
614
|
-
|
|
678
|
+
set peakDecayTime(value) {
|
|
679
|
+
// note: time is stored in seconds to reduce the number of operations during rendering
|
|
680
|
+
this._peakDecayTime = (value >= 0 ? +value : this._peakDecayTime ?? DEFAULT_SETTINGS.peakDecayTime) / 1e3;
|
|
615
681
|
}
|
|
616
682
|
get peakHoldTime() {
|
|
617
|
-
return this._peakHoldTime;
|
|
683
|
+
return this._peakHoldTime * 1e3;
|
|
618
684
|
}
|
|
619
685
|
set peakHoldTime(value) {
|
|
620
|
-
|
|
686
|
+
// note: time is stored in seconds to reduce the number of operations during rendering
|
|
687
|
+
this._peakHoldTime = +value / 1e3 || 0;
|
|
621
688
|
}
|
|
622
689
|
get peakLine() {
|
|
623
690
|
return this._peakLine;
|
|
624
691
|
}
|
|
625
692
|
set peakLine(value) {
|
|
626
|
-
this._peakLine =
|
|
693
|
+
this._peakLine = +value || 0;
|
|
694
|
+
}
|
|
695
|
+
get peaks() {
|
|
696
|
+
return this._peaks;
|
|
697
|
+
}
|
|
698
|
+
set peaks(value) {
|
|
699
|
+
this._peaks = validateFromList(value, [PEAKS_OFF, PEAKS_DROP, PEAKS_FADE]);
|
|
627
700
|
}
|
|
628
701
|
get radial() {
|
|
629
702
|
return this._radial;
|
|
630
703
|
}
|
|
631
704
|
set radial(value) {
|
|
632
|
-
this._radial =
|
|
633
|
-
this._calcBars();
|
|
634
|
-
this._makeGrad();
|
|
635
|
-
}
|
|
636
|
-
get radialInvert() {
|
|
637
|
-
return this._radialInvert;
|
|
638
|
-
}
|
|
639
|
-
set radialInvert(value) {
|
|
640
|
-
this._radialInvert = !!value;
|
|
705
|
+
this._radial = [RADIAL_INNER, RADIAL_OFF, RADIAL_OUTER].includes(+value) ? +value : this._radial ?? DEFAULT_SETTINGS.radial;
|
|
641
706
|
this._calcBars();
|
|
642
707
|
this._makeGrad();
|
|
643
708
|
}
|
|
@@ -653,12 +718,14 @@
|
|
|
653
718
|
return this._reflexRatio;
|
|
654
719
|
}
|
|
655
720
|
set reflexRatio(value) {
|
|
656
|
-
value
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
this.
|
|
660
|
-
this._makeGrad();
|
|
721
|
+
if (!(value >= 0 && value < 1)) {
|
|
722
|
+
// also catches undefined and strings that evaluate to NaN
|
|
723
|
+
warnInvalid('reflexRatio', value);
|
|
724
|
+
value = this._reflexRatio ?? DEFAULT_SETTINGS.reflexRatio;
|
|
661
725
|
}
|
|
726
|
+
this._reflexRatio = +value;
|
|
727
|
+
this._calcBars();
|
|
728
|
+
this._makeGrad();
|
|
662
729
|
}
|
|
663
730
|
get roundBars() {
|
|
664
731
|
return this._roundBars;
|
|
@@ -667,6 +734,14 @@
|
|
|
667
734
|
this._roundBars = !!value;
|
|
668
735
|
this._calcBars();
|
|
669
736
|
}
|
|
737
|
+
get showScaleX() {
|
|
738
|
+
return this._sxshow;
|
|
739
|
+
}
|
|
740
|
+
set showScaleX(value) {
|
|
741
|
+
this._sxshow = !!value;
|
|
742
|
+
this._calcBars();
|
|
743
|
+
this._makeGrad();
|
|
744
|
+
}
|
|
670
745
|
get smoothing() {
|
|
671
746
|
return this._analyzer[0].smoothingTimeConstant;
|
|
672
747
|
}
|
|
@@ -681,27 +756,13 @@
|
|
|
681
756
|
if (this._spinSpeed === undefined || value == 0) this._spinAngle = -HALF_PI; // initialize or reset the rotation angle
|
|
682
757
|
this._spinSpeed = value;
|
|
683
758
|
}
|
|
684
|
-
get
|
|
685
|
-
return this.
|
|
759
|
+
get spreadGradient() {
|
|
760
|
+
return this._spread;
|
|
686
761
|
}
|
|
687
|
-
set
|
|
688
|
-
this.
|
|
762
|
+
set spreadGradient(value) {
|
|
763
|
+
this._spread = !!value;
|
|
689
764
|
this._makeGrad();
|
|
690
765
|
}
|
|
691
|
-
get stereo() {
|
|
692
|
-
deprecate('stereo', 'channelLayout');
|
|
693
|
-
return this._chLayout != CHANNEL_SINGLE;
|
|
694
|
-
}
|
|
695
|
-
set stereo(value) {
|
|
696
|
-
deprecate('stereo', 'channelLayout');
|
|
697
|
-
this.channelLayout = value ? CHANNEL_VERTICAL : CHANNEL_SINGLE;
|
|
698
|
-
}
|
|
699
|
-
get trueLeds() {
|
|
700
|
-
return this._trueLeds;
|
|
701
|
-
}
|
|
702
|
-
set trueLeds(value) {
|
|
703
|
-
this._trueLeds = !!value;
|
|
704
|
-
}
|
|
705
766
|
get volume() {
|
|
706
767
|
return this._output.gain.value;
|
|
707
768
|
}
|
|
@@ -749,7 +810,7 @@
|
|
|
749
810
|
return this._fsWidth;
|
|
750
811
|
}
|
|
751
812
|
get isAlphaBars() {
|
|
752
|
-
return this._flg.isAlpha;
|
|
813
|
+
return this._flg.isAlpha || this._flg.isLumi;
|
|
753
814
|
}
|
|
754
815
|
get isBandsMode() {
|
|
755
816
|
return this._flg.isBands;
|
|
@@ -763,9 +824,6 @@
|
|
|
763
824
|
get isLedBars() {
|
|
764
825
|
return this._flg.isLeds;
|
|
765
826
|
}
|
|
766
|
-
get isLumiBars() {
|
|
767
|
-
return this._flg.isLumi;
|
|
768
|
-
}
|
|
769
827
|
get isOctaveBands() {
|
|
770
828
|
return this._flg.isOctaves;
|
|
771
829
|
}
|
|
@@ -824,7 +882,7 @@
|
|
|
824
882
|
|
|
825
883
|
// when connecting the first node, also connect the analyzer nodes to the merger / output nodes
|
|
826
884
|
if (this._outNodes.length == 1) {
|
|
827
|
-
for (const i of [0, 1]) this._analyzer[i].connect(this._chLayout ==
|
|
885
|
+
for (const i of [0, 1]) this._analyzer[i].connect(this._chLayout == LAYOUT_SINGLE && !i ? this._output : this._merger, 0, i);
|
|
828
886
|
}
|
|
829
887
|
}
|
|
830
888
|
|
|
@@ -881,7 +939,7 @@
|
|
|
881
939
|
* @param [{boolean}] if true, stops/releases audio tracks from disconnected media streams (e.g. microphone)
|
|
882
940
|
*/
|
|
883
941
|
disconnectInput(sources, stopTracks) {
|
|
884
|
-
if (!sources) sources = Array.from(this._sources);else if (!
|
|
942
|
+
if (!sources) sources = Array.from(this._sources);else if (!isArray(sources)) sources = [sources];
|
|
885
943
|
for (const node of sources) {
|
|
886
944
|
const idx = this._sources.indexOf(node);
|
|
887
945
|
if (stopTracks && node.mediaStream) {
|
|
@@ -950,20 +1008,20 @@
|
|
|
950
1008
|
|
|
951
1009
|
// if startFreq is a string, check for presets
|
|
952
1010
|
if (startFreq != +startFreq) {
|
|
953
|
-
if (startFreq ==
|
|
1011
|
+
if (startFreq == ENERGY_PEAK) return this._energy.peak;
|
|
954
1012
|
const presets = {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1013
|
+
[ENERGY_BASS]: [20, 250],
|
|
1014
|
+
[ENERGY_LOWMID]: [250, 500],
|
|
1015
|
+
[ENERGY_MIDRANGE]: [500, 2e3],
|
|
1016
|
+
[ENERGY_HIGHMID]: [2e3, 4e3],
|
|
1017
|
+
[ENERGY_TREBLE]: [4e3, 16e3]
|
|
960
1018
|
};
|
|
961
1019
|
if (!presets[startFreq]) return null;
|
|
962
1020
|
[startFreq, endFreq] = presets[startFreq];
|
|
963
1021
|
}
|
|
964
1022
|
const startBin = this._freqToBin(startFreq),
|
|
965
1023
|
endBin = endFreq ? this._freqToBin(endFreq) : startBin,
|
|
966
|
-
chnCount = this._chLayout ==
|
|
1024
|
+
chnCount = this._chLayout == LAYOUT_SINGLE ? 1 : 2;
|
|
967
1025
|
let energy = 0;
|
|
968
1026
|
for (let channel = 0; channel < chnCount; channel++) {
|
|
969
1027
|
for (let i = startBin; i <= endBin; i++) energy += this._normalizedB(this._fftData[channel][i]);
|
|
@@ -978,59 +1036,135 @@
|
|
|
978
1036
|
* @returns {object} Options object
|
|
979
1037
|
*/
|
|
980
1038
|
getOptions(ignore) {
|
|
981
|
-
if (!
|
|
1039
|
+
if (!isArray(ignore)) ignore = [ignore];
|
|
982
1040
|
let options = {};
|
|
983
1041
|
for (const prop of Object.keys(DEFAULT_SETTINGS)) {
|
|
984
|
-
if (!ignore.includes(prop))
|
|
985
|
-
if (prop == 'gradient' && this.gradientLeft != this.gradientRight) {
|
|
986
|
-
options.gradientLeft = this.gradientLeft;
|
|
987
|
-
options.gradientRight = this.gradientRight;
|
|
988
|
-
} else if (prop != 'start') options[prop] = this[prop];
|
|
989
|
-
}
|
|
1042
|
+
if (!ignore.includes(prop)) options[prop] = this[prop];
|
|
990
1043
|
}
|
|
991
1044
|
return options;
|
|
992
1045
|
}
|
|
993
1046
|
|
|
994
1047
|
/**
|
|
995
|
-
*
|
|
1048
|
+
* Returns the selected theme for the given channel
|
|
1049
|
+
*
|
|
1050
|
+
* @param [{number}] channel - if undefined or invalid, considers channel 0
|
|
1051
|
+
* @param [{boolean}] `true` to include modifiers
|
|
1052
|
+
* @returns {string|object} theme name, or object with `name` and `modifiers`
|
|
1053
|
+
*/
|
|
1054
|
+
getTheme(channel, includeModifiers) {
|
|
1055
|
+
if (channel === true) {
|
|
1056
|
+
channel = 0;
|
|
1057
|
+
includeModifiers = true;
|
|
1058
|
+
} else if (!isValidChannel(channel)) channel = 0;
|
|
1059
|
+
const {
|
|
1060
|
+
name
|
|
1061
|
+
} = this._activeThemes[channel];
|
|
1062
|
+
return includeModifiers ? {
|
|
1063
|
+
name,
|
|
1064
|
+
modifiers: this.getThemeModifiers(channel)
|
|
1065
|
+
} : name;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Returns data for the theme with the given name
|
|
1070
|
+
*
|
|
1071
|
+
* @param {string} theme name
|
|
1072
|
+
* @returns {object|null} theme object or null if name is invalid
|
|
1073
|
+
*/
|
|
1074
|
+
getThemeData(name) {
|
|
1075
|
+
return this.getThemeList().includes(name) ? Object.fromEntries(Object.entries(this._themes[name]).filter(([key]) => key != 'mask')) : null;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Returns the names of available themes
|
|
1080
|
+
*
|
|
1081
|
+
* @returns {array}
|
|
1082
|
+
*/
|
|
1083
|
+
getThemeList() {
|
|
1084
|
+
return Object.keys(this._themes);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Returns the current state of theme modifiers for the given channel
|
|
1089
|
+
*
|
|
1090
|
+
* @param [{string}] desired modifier - if undefined, returns all modifiers
|
|
1091
|
+
* @param [{number}] channel - if undefined or invalid, considers channel 0
|
|
1092
|
+
* @returns {boolean|object} value of requested modifier, or object with all modifiers
|
|
1093
|
+
*/
|
|
1094
|
+
getThemeModifiers(modifier, channel) {
|
|
1095
|
+
if (isNumeric(modifier)) {
|
|
1096
|
+
channel = modifier;
|
|
1097
|
+
modifier = null;
|
|
1098
|
+
}
|
|
1099
|
+
if (!isValidChannel(channel)) channel = 0;
|
|
1100
|
+
const {
|
|
1101
|
+
modifiers
|
|
1102
|
+
} = this._activeThemes[channel];
|
|
1103
|
+
return modifier ? modifiers[modifier] : {
|
|
1104
|
+
...modifiers
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Registers a custom color theme
|
|
996
1110
|
*
|
|
997
1111
|
* @param {string} name
|
|
998
1112
|
* @param {object} options
|
|
1113
|
+
* @returns {boolean} true on success or false on error
|
|
999
1114
|
*/
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1115
|
+
registerTheme(name, options) {
|
|
1116
|
+
const fail = msg => {
|
|
1117
|
+
console.warn(`Cannot register theme "${name}": ${msg}`);
|
|
1118
|
+
return false;
|
|
1119
|
+
};
|
|
1120
|
+
if (typeof name != 'string' || name.trim().length == 0) return fail('name must be a non-empty string');
|
|
1121
|
+
if (!isObject(options)) return fail('options must be an object');
|
|
1003
1122
|
const {
|
|
1004
|
-
colorStops
|
|
1005
|
-
|
|
1006
|
-
|
|
1123
|
+
colorStops,
|
|
1124
|
+
peakColor
|
|
1125
|
+
} = deepCloneObject(options); // avoid modifying user's original object (see discussion #58)
|
|
1126
|
+
|
|
1127
|
+
if (!isArray(colorStops) || !colorStops.length) return fail('colorStops must be a non-empty array');
|
|
1007
1128
|
const count = colorStops.length,
|
|
1008
|
-
isInvalid = val => +val != val
|
|
1129
|
+
isInvalid = val => +val != clamp(val, 0, 1);
|
|
1009
1130
|
|
|
1010
|
-
// normalize all colorStops as objects with `
|
|
1131
|
+
// normalize all colorStops as objects with `color`, `level` and `pos` properties
|
|
1011
1132
|
colorStops.forEach((colorStop, index) => {
|
|
1012
1133
|
const pos = index / Math.max(1, count - 1);
|
|
1013
|
-
if (
|
|
1134
|
+
if (!isObject(colorStop))
|
|
1014
1135
|
// only color string was defined
|
|
1015
1136
|
colorStops[index] = {
|
|
1016
|
-
|
|
1017
|
-
|
|
1137
|
+
color: colorStop,
|
|
1138
|
+
pos
|
|
1018
1139
|
};else if (isInvalid(colorStop.pos)) colorStop.pos = pos;
|
|
1019
1140
|
if (isInvalid(colorStop.level)) colorStops[index].level = 1 - index / count;
|
|
1020
1141
|
});
|
|
1021
1142
|
|
|
1022
|
-
//
|
|
1023
|
-
|
|
1024
|
-
colorStops.sort((a, b) => a.level < b.level ? 1 : a.level > b.level ? -1 : 0);
|
|
1143
|
+
// important: ensure colorStops is in descending `level` order and the first colorStop has `level: 1`
|
|
1144
|
+
colorStops.sort((a, b) => b.level - a.level);
|
|
1025
1145
|
colorStops[0].level = 1;
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1146
|
+
|
|
1147
|
+
// generate the colorstops for the led mask
|
|
1148
|
+
|
|
1149
|
+
const maskColorStops = deepCloneObject(colorStops),
|
|
1150
|
+
[maskAlpha, maskLightness, maskSaturation] = DEFAULT_LEDMASK_PARAMS; // TO-DO: make these customizable
|
|
1151
|
+
|
|
1152
|
+
for (let i = 0; i < count; i++) {
|
|
1153
|
+
const cs = maskColorStops[i],
|
|
1154
|
+
[h, s, l] = cssColorToHSL(cs.color);
|
|
1155
|
+
cs.color = `hsla( ${h}, ${maskSaturation == -1 ? s : maskSaturation}%, ${maskLightness == -1 ? l : maskSaturation}%, ${maskAlpha} )`;
|
|
1156
|
+
}
|
|
1157
|
+
this._themes[name] = {
|
|
1158
|
+
colorStops,
|
|
1159
|
+
mask: {
|
|
1160
|
+
colorStops: maskColorStops
|
|
1161
|
+
},
|
|
1162
|
+
peakColor
|
|
1030
1163
|
};
|
|
1031
1164
|
|
|
1032
|
-
// if the registered
|
|
1033
|
-
if (this.
|
|
1165
|
+
// if the registered theme is one of the currently selected ones, regenerate the gradients
|
|
1166
|
+
if (this._activeThemes.some(theme => theme.name == name)) this._makeGrad();
|
|
1167
|
+
return true;
|
|
1034
1168
|
}
|
|
1035
1169
|
|
|
1036
1170
|
/**
|
|
@@ -1052,28 +1186,26 @@
|
|
|
1052
1186
|
* @param {number} max highest frequency represented in the x-axis
|
|
1053
1187
|
*/
|
|
1054
1188
|
setFreqRange(min, max) {
|
|
1055
|
-
if (min
|
|
1189
|
+
if (!(min > 0 && max > 0))
|
|
1190
|
+
// also catches undefined and strings that evaluate to NaN
|
|
1191
|
+
warnInvalid('setFreqRange', [min, max]);else {
|
|
1056
1192
|
this._minFreq = Math.min(min, max);
|
|
1057
1193
|
this.maxFreq = Math.max(min, max); // use the setter for maxFreq
|
|
1058
1194
|
}
|
|
1059
1195
|
}
|
|
1060
1196
|
|
|
1061
1197
|
/**
|
|
1062
|
-
* Set custom parameters for
|
|
1063
|
-
* If called with no arguments or if any
|
|
1198
|
+
* Set custom parameters for ledBars effect
|
|
1199
|
+
* If called with no arguments or if any value is invalid, resets both parameters to the defaults
|
|
1064
1200
|
*
|
|
1065
|
-
* @param {
|
|
1201
|
+
* @param {number} height of each led element (in pixels)
|
|
1202
|
+
* @param {number} vertical gap between led elements (in pixels)
|
|
1066
1203
|
*/
|
|
1067
|
-
|
|
1068
|
-
let maxLeds, spaceV, spaceH;
|
|
1069
|
-
|
|
1204
|
+
setLeds(ledHeight, gapHeight) {
|
|
1070
1205
|
// coerce parameters to Number; `NaN` results are rejected in the condition below
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
spaceV = +params.spaceV, spaceH = +params.spaceH;
|
|
1075
|
-
}
|
|
1076
|
-
this._ledParams = maxLeds > 0 && spaceV > 0 && spaceH >= 0 ? [maxLeds, spaceV, spaceH] : undefined;
|
|
1206
|
+
ledHeight = +ledHeight;
|
|
1207
|
+
gapHeight = +gapHeight;
|
|
1208
|
+
this._ledParams = ledHeight >= 0 && gapHeight >= 0 ? [ledHeight, gapHeight] : undefined;
|
|
1077
1209
|
this._calcBars();
|
|
1078
1210
|
}
|
|
1079
1211
|
|
|
@@ -1099,6 +1231,136 @@
|
|
|
1099
1231
|
}
|
|
1100
1232
|
}
|
|
1101
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* Set color theme
|
|
1236
|
+
*
|
|
1237
|
+
* @param {string|object|array} theme name, theme object as returned by getTheme(), or array of such types
|
|
1238
|
+
* @param [{object}] theme modifiers, as returned by getThemeModifiers() (only when first argument is a string)
|
|
1239
|
+
* @param [{number}] desired channel (0 or 1) - if empty or invalid, sets both channels (ignored when first argument is an array)
|
|
1240
|
+
*/
|
|
1241
|
+
setTheme(...args) {
|
|
1242
|
+
// if first argument is an array, make recursive calls for each channel
|
|
1243
|
+
if (isArray(args[0])) {
|
|
1244
|
+
for (let ch = 0; ch < Math.max(2, args[0].length); ch++) this.setTheme(args[0][ch], ch);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const {
|
|
1248
|
+
name,
|
|
1249
|
+
modifiers
|
|
1250
|
+
} = isObject(args[0]) ? args[0] : {
|
|
1251
|
+
name: args[0],
|
|
1252
|
+
modifiers: isObject(args[1]) ? args[1] : null
|
|
1253
|
+
},
|
|
1254
|
+
channel = args[2] ?? args[1],
|
|
1255
|
+
themeNames = this.getThemeList(),
|
|
1256
|
+
isNameValid = themeNames.includes(name);
|
|
1257
|
+
for (const ch of validateChannelArray(channel)) {
|
|
1258
|
+
if (!this._activeThemes[ch]) this._activeThemes[ch] = {
|
|
1259
|
+
modifiers: {
|
|
1260
|
+
...DEFAULT_THEME_MODIFIERS
|
|
1261
|
+
}
|
|
1262
|
+
}; // creates new entry (during constructor initialization)
|
|
1263
|
+
|
|
1264
|
+
this._activeThemes[ch].name = isNameValid ? name : this._activeThemes[ch].name || themeNames[0];
|
|
1265
|
+
}
|
|
1266
|
+
if (modifiers) this.setThemeModifiers(modifiers, channel);else this._makeGrad();
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Set theme modifiers
|
|
1271
|
+
*
|
|
1272
|
+
* @param [{string|object}] modifier name or modifiers object; if null or undefined resets to defaults
|
|
1273
|
+
* @param [{boolean}] desired value when setting a single modifier
|
|
1274
|
+
* @param [{number}] channel (0 or 1) - if empty or invalid, sets modifiers on both channels
|
|
1275
|
+
*/
|
|
1276
|
+
setThemeModifiers(modifier, value, channel) {
|
|
1277
|
+
const validKeys = Object.keys(DEFAULT_THEME_MODIFIERS);
|
|
1278
|
+
if (modifier === null || modifier === undefined) {
|
|
1279
|
+
modifier = {}; // will reset to defaults
|
|
1280
|
+
channel = value; // optional
|
|
1281
|
+
} else if (isNumeric(modifier)) {
|
|
1282
|
+
channel = modifier; // only channel passed
|
|
1283
|
+
modifier = {}; // will reset to defaults
|
|
1284
|
+
} else if (isObject(modifier)) {
|
|
1285
|
+
channel = value;
|
|
1286
|
+
modifier = deepCloneObject(modifier); // make a copy, so we don't change user's original object
|
|
1287
|
+
|
|
1288
|
+
// remove invalid modifiers and ensure all values are boolean
|
|
1289
|
+
for (const key of Object.keys(modifier)) {
|
|
1290
|
+
if (validKeys.includes(key)) modifier[key] = !!modifier[key];else delete modifier[key];
|
|
1291
|
+
}
|
|
1292
|
+
} else if (!validKeys.includes(modifier))
|
|
1293
|
+
// validates single modifier
|
|
1294
|
+
return;
|
|
1295
|
+
for (const ch of validateChannelArray(channel)) {
|
|
1296
|
+
const activeThemeData = this._activeThemes[ch];
|
|
1297
|
+
if (isObject(modifier)) {
|
|
1298
|
+
// when passed an object, any modifier not present will be reset to its default value!
|
|
1299
|
+
activeThemeData.modifiers = {
|
|
1300
|
+
...DEFAULT_THEME_MODIFIERS,
|
|
1301
|
+
...modifier
|
|
1302
|
+
};
|
|
1303
|
+
} else activeThemeData.modifiers[modifier] = !!value;
|
|
1304
|
+
}
|
|
1305
|
+
this._makeGrad();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Customize X-Axis display
|
|
1310
|
+
*
|
|
1311
|
+
* @param {object} options
|
|
1312
|
+
*/
|
|
1313
|
+
setXAxis(options) {
|
|
1314
|
+
const defaultOptions = {
|
|
1315
|
+
addLabels: false,
|
|
1316
|
+
backgroundColor: '#0008',
|
|
1317
|
+
color: '#fff',
|
|
1318
|
+
height: .03,
|
|
1319
|
+
highlightColor: '#4f4',
|
|
1320
|
+
labels: [],
|
|
1321
|
+
overlay: false
|
|
1322
|
+
};
|
|
1323
|
+
this._xAxis = {
|
|
1324
|
+
...defaultOptions,
|
|
1325
|
+
// if `options` is valid, add its properties on top of current settings; otherwise keep just the defaults
|
|
1326
|
+
...(isObject(options) ? {
|
|
1327
|
+
...this._xAxis,
|
|
1328
|
+
...options
|
|
1329
|
+
} : [])
|
|
1330
|
+
};
|
|
1331
|
+
this._calcBars(); // note that changes to `height` and `overlay` affect usable canvas height
|
|
1332
|
+
this._makeGrad();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Customize Y-axis display
|
|
1337
|
+
*
|
|
1338
|
+
* @param {object} options
|
|
1339
|
+
*/
|
|
1340
|
+
setYAxis(options) {
|
|
1341
|
+
const defaultOptions = {
|
|
1342
|
+
color: '#888',
|
|
1343
|
+
dbInterval: 10,
|
|
1344
|
+
linearInterval: 20,
|
|
1345
|
+
lineDash: [2, 4],
|
|
1346
|
+
operation: 'destination-over',
|
|
1347
|
+
showSubdivisions: true,
|
|
1348
|
+
showUnit: true,
|
|
1349
|
+
subLineColor: '#555',
|
|
1350
|
+
subLineDash: [2, 8],
|
|
1351
|
+
width: .03
|
|
1352
|
+
};
|
|
1353
|
+
this._yAxis = {
|
|
1354
|
+
...defaultOptions,
|
|
1355
|
+
// if `options` is valid, add its properties on top of current settings; otherwise keep just the defaults
|
|
1356
|
+
...(isObject(options) ? {
|
|
1357
|
+
...this._yAxis,
|
|
1358
|
+
...options
|
|
1359
|
+
} : [])
|
|
1360
|
+
};
|
|
1361
|
+
this._calcBars(); // only to compute yAxisWidth - TO-DO: improve this?
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1102
1364
|
/**
|
|
1103
1365
|
* Start the analyzer
|
|
1104
1366
|
*/
|
|
@@ -1122,14 +1384,11 @@
|
|
|
1122
1384
|
toggleAnalyzer(force) {
|
|
1123
1385
|
const hasStarted = this.isOn;
|
|
1124
1386
|
if (force === undefined) force = !hasStarted;
|
|
1125
|
-
|
|
1126
|
-
// Stop the analyzer if it was already running and must be disabled
|
|
1127
1387
|
if (hasStarted && !force) {
|
|
1128
|
-
|
|
1129
|
-
this._runId =
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
else if (!hasStarted && force && !this._destroyed) {
|
|
1388
|
+
// Stop the analyzer if it was already running and must be disabled
|
|
1389
|
+
this._runId = cancelAnimationFrame(this._runId);
|
|
1390
|
+
} else if (!hasStarted && force && !this._destroyed) {
|
|
1391
|
+
// Start the analyzer if it was stopped and must be enabled
|
|
1133
1392
|
this._frames = 0;
|
|
1134
1393
|
this._time = performance.now();
|
|
1135
1394
|
this._runId = requestAnimationFrame(timestamp => this._draw(timestamp)); // arrow function preserves the scope of *this*
|
|
@@ -1150,6 +1409,27 @@
|
|
|
1150
1409
|
}
|
|
1151
1410
|
}
|
|
1152
1411
|
|
|
1412
|
+
/**
|
|
1413
|
+
* Toggle a theme modifier
|
|
1414
|
+
*
|
|
1415
|
+
* @param {string} modifier name
|
|
1416
|
+
* @param [{number}] channel (0 or 1) - if empty or invalid, toggles modifier on both channels
|
|
1417
|
+
*/
|
|
1418
|
+
toggleThemeModifier(modifier, channel) {
|
|
1419
|
+
for (const ch of validateChannelArray(channel)) this.setThemeModifiers(modifier, !this.getThemeModifiers(modifier, channel), channel);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Unregisters a color theme
|
|
1424
|
+
*
|
|
1425
|
+
* @param {string} name
|
|
1426
|
+
* @return {boolean} `true` on success or `false` if theme is not registered or in use
|
|
1427
|
+
*/
|
|
1428
|
+
unregisterTheme(name) {
|
|
1429
|
+
if (!this.getThemeList().includes(name) || this._activeThemes.some(theme => theme.name == name)) return false;
|
|
1430
|
+
return delete this._themes[name];
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1153
1433
|
/**
|
|
1154
1434
|
* ==========================================================================
|
|
1155
1435
|
*
|
|
@@ -1169,23 +1449,11 @@
|
|
|
1169
1449
|
* Compute all internal data required for the analyzer, based on its current settings
|
|
1170
1450
|
*/
|
|
1171
1451
|
_calcBars() {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
if (!this._ready) {
|
|
1175
|
-
this._flg = {
|
|
1176
|
-
isAlpha: false,
|
|
1177
|
-
isBands: false,
|
|
1178
|
-
isLeds: false,
|
|
1179
|
-
isLumi: false,
|
|
1180
|
-
isOctaves: false,
|
|
1181
|
-
isOutline: false,
|
|
1182
|
-
isRound: false,
|
|
1183
|
-
noLedGap: false
|
|
1184
|
-
};
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1452
|
+
if (!this._ready) return;
|
|
1187
1453
|
const {
|
|
1454
|
+
_alphaBars,
|
|
1188
1455
|
_ansiBands,
|
|
1456
|
+
_bandRes,
|
|
1189
1457
|
_barSpace,
|
|
1190
1458
|
canvas,
|
|
1191
1459
|
_chLayout,
|
|
@@ -1193,39 +1461,45 @@
|
|
|
1193
1461
|
_minFreq,
|
|
1194
1462
|
_mirror,
|
|
1195
1463
|
_mode,
|
|
1464
|
+
_pixelRatio,
|
|
1196
1465
|
_radial,
|
|
1197
|
-
|
|
1198
|
-
|
|
1466
|
+
_reflexRatio,
|
|
1467
|
+
_xAxis,
|
|
1468
|
+
_yAxis
|
|
1199
1469
|
} = this,
|
|
1470
|
+
bars = [],
|
|
1200
1471
|
centerX = canvas.width >> 1,
|
|
1201
1472
|
centerY = canvas.height >> 1,
|
|
1202
|
-
isDualVertical = _chLayout ==
|
|
1203
|
-
isDualHorizontal = _chLayout ==
|
|
1473
|
+
isDualVertical = _chLayout == LAYOUT_VERTICAL && !_radial,
|
|
1474
|
+
isDualHorizontal = _chLayout == LAYOUT_HORIZONTAL,
|
|
1475
|
+
minCanvasDimension = Math.min(canvas.width, canvas.height),
|
|
1476
|
+
xAxisHeight = Math.max(MIN_AXIS_DIMENSION * _pixelRatio, _xAxis.height * (_xAxis.height > 1 ? _pixelRatio : minCanvasDimension) | 0),
|
|
1477
|
+
yAxisWidth = Math.max(MIN_AXIS_DIMENSION * _pixelRatio, _yAxis.width * (_yAxis.width > 1 ? _pixelRatio : minCanvasDimension) | 0),
|
|
1478
|
+
scaleGap = xAxisHeight * (!_xAxis.overlay && this._sxshow),
|
|
1204
1479
|
// COMPUTE FLAGS
|
|
1205
1480
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1481
|
+
isAlpha = _alphaBars == ALPHABARS_ON && _mode == MODE_BARS,
|
|
1482
|
+
isBands = _bandRes > 0,
|
|
1208
1483
|
isOctaves = isBands && this._frequencyScale == SCALE_LOG,
|
|
1209
|
-
isLeds = this.
|
|
1210
|
-
isLumi =
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
noLedGap = _chLayout != CHANNEL_VERTICAL || _reflexRatio > 0 && !isLumi,
|
|
1484
|
+
isLeds = this._ledBars != LEDS_OFF && isBands && !_radial && _mode == MODE_BARS,
|
|
1485
|
+
isLumi = _alphaBars == ALPHABARS_FULL && _mode == MODE_BARS,
|
|
1486
|
+
isOutline = this._outlineBars && _mode == MODE_BARS && isBands && !isLumi && !isLeds,
|
|
1487
|
+
isRound = this._roundBars && _mode == MODE_BARS && isBands && !isLumi && !isLeds,
|
|
1488
|
+
noLedGap = _chLayout != LAYOUT_VERTICAL || _reflexRatio > 0 && !isLumi || scaleGap > 0,
|
|
1215
1489
|
// COMPUTE AUXILIARY VALUES
|
|
1216
1490
|
|
|
1217
1491
|
// channelHeight is the total canvas height dedicated to each channel, including the reflex area, if any)
|
|
1218
1492
|
channelHeight = canvas.height - (isDualVertical && !isLeds ? .5 : 0) >> isDualVertical,
|
|
1219
1493
|
// analyzerHeight is the effective height used to render the analyzer, excluding the reflex area
|
|
1220
|
-
analyzerHeight = channelHeight * (isLumi || _radial ? 1 : 1 - _reflexRatio) | 0,
|
|
1494
|
+
analyzerHeight = (channelHeight - scaleGap) * (isLumi || _radial ? 1 : 1 - _reflexRatio) | 0,
|
|
1221
1495
|
analyzerWidth = canvas.width - centerX * (isDualHorizontal || _mirror != 0),
|
|
1222
1496
|
// channelGap is **0** if isLedDisplay == true (LEDs already have spacing); **1** if canvas height is odd (windowed); **2** if it's even
|
|
1223
|
-
//
|
|
1497
|
+
// TO-DO: improve this, make it configurable?
|
|
1224
1498
|
channelGap = isDualVertical ? canvas.height - channelHeight * 2 : 0,
|
|
1225
1499
|
initialX = centerX * (_mirror == -1 && !isDualHorizontal && !_radial);
|
|
1226
|
-
let innerRadius =
|
|
1500
|
+
let innerRadius = minCanvasDimension * .375 * (_chLayout == LAYOUT_VERTICAL ? 1 : this._radius) | 0,
|
|
1227
1501
|
outerRadius = Math.min(centerX, centerY);
|
|
1228
|
-
if (
|
|
1502
|
+
if (_radial == RADIAL_INNER && _chLayout != LAYOUT_VERTICAL) [innerRadius, outerRadius] = [outerRadius, innerRadius];
|
|
1229
1503
|
|
|
1230
1504
|
/**
|
|
1231
1505
|
* CREATE ANALYZER BANDS
|
|
@@ -1256,7 +1530,7 @@
|
|
|
1256
1530
|
// ratioHi,
|
|
1257
1531
|
// peak, // peak value
|
|
1258
1532
|
// hold, // peak hold frames (negative value indicates peak falling / fading)
|
|
1259
|
-
// alpha, // peak alpha (used
|
|
1533
|
+
// alpha, // peak alpha (used to fade peaks)
|
|
1260
1534
|
// value // current bar value
|
|
1261
1535
|
// }
|
|
1262
1536
|
const barsPush = args => bars.push({
|
|
@@ -1314,7 +1588,7 @@
|
|
|
1314
1588
|
|
|
1315
1589
|
// ANSI standard octave bands use the base-10 frequency ratio, as preferred by [ANSI S1.11-2004, p.2]
|
|
1316
1590
|
// The equal-tempered scale uses the base-2 ratio
|
|
1317
|
-
const bands = [0,
|
|
1591
|
+
const bands = [0, 1, 2, 3, 4, 6, 8, 12, 24][_bandRes],
|
|
1318
1592
|
bandWidth = _ansiBands ? 10 ** (3 / (bands * 10)) : 2 ** (1 / bands),
|
|
1319
1593
|
// 10^(3/10N) or 2^(1/N)
|
|
1320
1594
|
halfBand = bandWidth ** .5;
|
|
@@ -1369,7 +1643,7 @@
|
|
|
1369
1643
|
} else if (isBands) {
|
|
1370
1644
|
// a bands mode is selected, but frequency scale is not logarithmic
|
|
1371
1645
|
|
|
1372
|
-
const bands = [0,
|
|
1646
|
+
const bands = [0, 1, 2, 3, 4, 6, 8, 12, 24][_bandRes] * 10;
|
|
1373
1647
|
const invFreqScaling = x => {
|
|
1374
1648
|
switch (this._frequencyScale) {
|
|
1375
1649
|
case SCALE_BARK:
|
|
@@ -1435,6 +1709,8 @@
|
|
|
1435
1709
|
}
|
|
1436
1710
|
}
|
|
1437
1711
|
}
|
|
1712
|
+
const barSpacePx = Math.min(barWidth - 1, _barSpace * (_barSpace > 0 && _barSpace < 1 ? barWidth : 1));
|
|
1713
|
+
if (isBands) barWidth -= Math.max(0, barSpacePx);
|
|
1438
1714
|
|
|
1439
1715
|
/**
|
|
1440
1716
|
* COMPUTE ATTRIBUTES FOR THE LED BARS
|
|
@@ -1445,68 +1721,46 @@
|
|
|
1445
1721
|
* noLedGap
|
|
1446
1722
|
*
|
|
1447
1723
|
* GENERATES:
|
|
1448
|
-
* spaceH
|
|
1449
|
-
* spaceV
|
|
1450
1724
|
* this._leds
|
|
1451
1725
|
*/
|
|
1452
1726
|
|
|
1453
|
-
let spaceH = 0,
|
|
1454
|
-
spaceV = 0;
|
|
1455
1727
|
if (isLeds) {
|
|
1456
|
-
// adjustment for high pixel-ratio values on low-resolution screens (Android TV)
|
|
1457
|
-
const dPR =
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
let blockHeight;
|
|
1483
|
-
ledCount = maxLeds + 1;
|
|
1484
|
-
do {
|
|
1485
|
-
ledCount--;
|
|
1486
|
-
blockHeight = maxHeight / ledCount / (1 + spaceVRatio);
|
|
1487
|
-
spaceV = blockHeight * spaceVRatio;
|
|
1488
|
-
} while ((blockHeight < minHeight || spaceV < minHeight) && ledCount > 1);
|
|
1489
|
-
} else {
|
|
1490
|
-
// calculate vertical spacing - aim for the reference ratio, but make sure it's at least 2px
|
|
1491
|
-
const refRatio = 540 / spaceVRatio;
|
|
1492
|
-
spaceV = Math.min(spaceVRatio * dPR, Math.max(2, maxHeight / refRatio + .1 | 0));
|
|
1728
|
+
// adjustment for high pixel-ratio values reported on low-resolution screens (Android TV)
|
|
1729
|
+
const dPR = _pixelRatio / (window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1);
|
|
1730
|
+
let [ledHeight, ledGap] = (this._ledParams || DEFAULT_LED_PARAMETERS).map(v => v * dPR),
|
|
1731
|
+
isSquareLeds = ledHeight == 0;
|
|
1732
|
+
if (isSquareLeds) ledHeight = barWidth;
|
|
1733
|
+
if (ledGap == 0) ledGap = barSpacePx; // matches gapHeight to bar spacing
|
|
1734
|
+
|
|
1735
|
+
let maxHeight = analyzerHeight + (noLedGap ? ledGap : 0),
|
|
1736
|
+
// if noLedGap is true, add one extra gap height so the last gap is off-screen
|
|
1737
|
+
elemHeight = ledHeight + ledGap,
|
|
1738
|
+
// height of one LED element + gap (defined by user)
|
|
1739
|
+
ledCount = maxHeight / elemHeight | 0,
|
|
1740
|
+
// how many LED elements fit in the available canvas height
|
|
1741
|
+
unitHeight = maxHeight / ledCount; // height of one LED element + gap (adjusted to fit canvas height)
|
|
1742
|
+
|
|
1743
|
+
if (isSquareLeds || ledGap > ledHeight) {
|
|
1744
|
+
// for square LEDs, or when ledGap is higher than ledHeight
|
|
1745
|
+
ledGap = unitHeight * (1 - ledHeight / unitHeight); // adjust ledGap, preserve user-defined ledHeight (only needs minor adjustment when noLedGap is true)
|
|
1746
|
+
if (noLedGap) {
|
|
1747
|
+
// when ledGap changes, more adjustments are necessary for noLedGap
|
|
1748
|
+
const ledBarHeight = (ledHeight + ledGap) * ledCount,
|
|
1749
|
+
// height of full LED bar
|
|
1750
|
+
newMaxHeight = analyzerHeight + ledGap; // height it needs to be
|
|
1751
|
+
ledGap += (newMaxHeight - ledBarHeight) / ledCount; // distribute the difference across all gaps
|
|
1752
|
+
unitHeight = (analyzerHeight + ledGap) / ledCount; // update unitHeight with new ledGap, for ledHeight adjustment below
|
|
1753
|
+
}
|
|
1493
1754
|
}
|
|
1494
1755
|
|
|
1495
|
-
//
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
// recalculate the number of leds, considering the effective spaceV
|
|
1499
|
-
if (!customParams) ledCount = Math.min(maxLeds, maxHeight / (spaceV * 2) | 0);
|
|
1500
|
-
spaceH = spaceHRatio >= 1 ? spaceHRatio : barWidth * spaceHRatio;
|
|
1501
|
-
this._leds = [ledCount, spaceH, spaceV, maxHeight / ledCount - spaceV // ledHeight
|
|
1502
|
-
];
|
|
1756
|
+
// adjust ledHeight (user-defined ledGap is preserved when less than or equal to ledHeight)
|
|
1757
|
+
ledHeight = unitHeight * (1 - ledGap / unitHeight);
|
|
1758
|
+
this._leds = [ledCount, ledHeight, ledGap];
|
|
1503
1759
|
}
|
|
1504
1760
|
|
|
1505
1761
|
// COMPUTE ADDITIONAL BAR POSITIONING, ACCORDING TO THE CURRENT SETTINGS
|
|
1506
|
-
// uses: _barSpace, barWidth
|
|
1762
|
+
// uses: _barSpace, barWidth
|
|
1507
1763
|
|
|
1508
|
-
const barSpacePx = Math.min(barWidth - 1, _barSpace * (_barSpace > 0 && _barSpace < 1 ? barWidth : 1));
|
|
1509
|
-
if (isBands) barWidth -= Math.max(isLeds ? spaceH : 0, barSpacePx);
|
|
1510
1764
|
bars.forEach((bar, index) => {
|
|
1511
1765
|
let posX = bar.posX,
|
|
1512
1766
|
width = barWidth;
|
|
@@ -1522,20 +1776,21 @@
|
|
|
1522
1776
|
posX--;
|
|
1523
1777
|
width++;
|
|
1524
1778
|
}
|
|
1525
|
-
} else posX += Math.max(
|
|
1779
|
+
} else posX += Math.max(0, barSpacePx) / 2;
|
|
1526
1780
|
bar.posX = posX; // update
|
|
1527
1781
|
}
|
|
1528
1782
|
bar.barCenter = posX + (barWidth == 1 ? 0 : width / 2);
|
|
1529
1783
|
bar.width = width;
|
|
1530
1784
|
});
|
|
1531
1785
|
|
|
1532
|
-
// COMPUTE CHANNEL COORDINATES
|
|
1786
|
+
// COMPUTE CHANNEL COORDINATES
|
|
1533
1787
|
|
|
1534
|
-
const channelCoords = []
|
|
1788
|
+
const channelCoords = [],
|
|
1789
|
+
[,, ledGap] = this._leds;
|
|
1535
1790
|
for (const channel of [0, 1]) {
|
|
1536
|
-
const channelTop = _chLayout ==
|
|
1791
|
+
const channelTop = _chLayout == LAYOUT_VERTICAL ? (channelHeight + channelGap) * channel : 0,
|
|
1537
1792
|
channelBottom = channelTop + channelHeight,
|
|
1538
|
-
analyzerBottom = channelTop + analyzerHeight - (!isLeds || noLedGap ? 0 :
|
|
1793
|
+
analyzerBottom = channelTop + analyzerHeight - (!isLeds || noLedGap ? 0 : ledGap);
|
|
1539
1794
|
channelCoords.push({
|
|
1540
1795
|
channelTop,
|
|
1541
1796
|
channelBottom,
|
|
@@ -1544,7 +1799,7 @@
|
|
|
1544
1799
|
}
|
|
1545
1800
|
|
|
1546
1801
|
// SAVE INTERNAL PROPERTIES
|
|
1547
|
-
|
|
1802
|
+
this._bars = bars;
|
|
1548
1803
|
this._aux = {
|
|
1549
1804
|
analyzerHeight,
|
|
1550
1805
|
analyzerWidth,
|
|
@@ -1557,7 +1812,9 @@
|
|
|
1557
1812
|
innerRadius,
|
|
1558
1813
|
outerRadius,
|
|
1559
1814
|
scaleMin,
|
|
1560
|
-
unitWidth
|
|
1815
|
+
unitWidth,
|
|
1816
|
+
xAxisHeight,
|
|
1817
|
+
yAxisWidth
|
|
1561
1818
|
};
|
|
1562
1819
|
this._flg = {
|
|
1563
1820
|
isAlpha,
|
|
@@ -1570,7 +1827,7 @@
|
|
|
1570
1827
|
noLedGap
|
|
1571
1828
|
};
|
|
1572
1829
|
|
|
1573
|
-
// generate
|
|
1830
|
+
// generate X-axis and radial scale labels
|
|
1574
1831
|
this._createScales();
|
|
1575
1832
|
}
|
|
1576
1833
|
|
|
@@ -1584,7 +1841,8 @@
|
|
|
1584
1841
|
initialX,
|
|
1585
1842
|
innerRadius,
|
|
1586
1843
|
scaleMin,
|
|
1587
|
-
unitWidth
|
|
1844
|
+
unitWidth,
|
|
1845
|
+
xAxisHeight
|
|
1588
1846
|
} = this._aux,
|
|
1589
1847
|
{
|
|
1590
1848
|
canvas,
|
|
@@ -1593,44 +1851,58 @@
|
|
|
1593
1851
|
_noteLabels,
|
|
1594
1852
|
_radial,
|
|
1595
1853
|
_scaleX,
|
|
1596
|
-
_scaleR
|
|
1854
|
+
_scaleR,
|
|
1855
|
+
_xAxis
|
|
1597
1856
|
} = this,
|
|
1598
1857
|
canvasX = _scaleX.canvas,
|
|
1599
1858
|
canvasR = _scaleR.canvas,
|
|
1600
|
-
freqLabels = [],
|
|
1601
|
-
isDualHorizontal = this._chLayout ==
|
|
1602
|
-
isDualVertical = this._chLayout ==
|
|
1603
|
-
|
|
1859
|
+
freqLabels = isArray(_xAxis.labels) && !_noteLabels ? [..._xAxis.labels] : [],
|
|
1860
|
+
isDualHorizontal = this._chLayout == LAYOUT_HORIZONTAL,
|
|
1861
|
+
isDualVertical = this._chLayout == LAYOUT_VERTICAL,
|
|
1862
|
+
minCanvasDimension = Math.min(canvas.width, canvas.height),
|
|
1604
1863
|
scale = ['C',, 'D',, 'E', 'F',, 'G',, 'A',, 'B'],
|
|
1605
1864
|
// for note labels (no sharp notes)
|
|
1606
|
-
|
|
1865
|
+
radialScaleHeight = minCanvasDimension / 34 | 0,
|
|
1607
1866
|
// circular scale height (radial mode)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
labelWidthX = fontSizeX * (_noteLabels ? .7 : 1.5),
|
|
1867
|
+
fontSizeR = radialScaleHeight >> 1,
|
|
1868
|
+
fontSizeX = xAxisHeight >> 1,
|
|
1611
1869
|
labelWidthR = fontSizeR * (_noteLabels ? 1 : 2),
|
|
1870
|
+
labelWidthX = fontSizeX * (_noteLabels ? .7 : 1.5),
|
|
1612
1871
|
root12 = 2 ** (1 / 12);
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1872
|
+
|
|
1873
|
+
// helper function - format a value using compact engineering notation (e.g.: 1000 -> 1k, 16700 -> 16k7)
|
|
1874
|
+
const formatLabel = f => f < 1e3 ? f | 0 : (f / 1e3).toFixed(1).replace(/([\.])([\d])$/, (m, p1, p2) => 'k' + (+p2 || ''));
|
|
1875
|
+
|
|
1876
|
+
// generate labels if not customized via setXAxis()
|
|
1877
|
+
if (!freqLabels.length || _xAxis.addLabels) {
|
|
1878
|
+
if (!_noteLabels && (this._ansiBands || _frequencyScale != SCALE_LOG)) {
|
|
1879
|
+
freqLabels.push(16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3);
|
|
1880
|
+
if (_frequencyScale == SCALE_LINEAR) freqLabels.push(6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3);else freqLabels.push(8e3, 16e3);
|
|
1881
|
+
} else {
|
|
1882
|
+
let freq = C_1;
|
|
1883
|
+
for (let octave = -1; octave < 11; octave++) {
|
|
1884
|
+
for (let note = 0; note < 12; note++) {
|
|
1885
|
+
if (freq >= this._minFreq && freq <= this._maxFreq) {
|
|
1886
|
+
const pitch = scale[note],
|
|
1887
|
+
isC = pitch == 'C';
|
|
1888
|
+
if (pitch && _noteLabels && !_mirror && !isDualHorizontal || isC) {
|
|
1889
|
+
const highlight = isC && !_mirror && !isDualHorizontal;
|
|
1890
|
+
freqLabels.push(_noteLabels ? [freq, pitch + (isC ? octave : ''), highlight] : freq);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
freq *= root12;
|
|
1624
1894
|
}
|
|
1625
|
-
freq *= root12;
|
|
1626
1895
|
}
|
|
1627
1896
|
}
|
|
1628
1897
|
}
|
|
1629
1898
|
|
|
1899
|
+
// make sure labels added via setXAxis() are in asceding order
|
|
1900
|
+
freqLabels.sort((a, b) => (isArray(a) ? a[0] : a) - (isArray(b) ? b[0] : b));
|
|
1901
|
+
|
|
1630
1902
|
// in radial dual-vertical layout, the scale is positioned exactly between both channels, by making the canvas a bit larger than the inner diameter
|
|
1631
|
-
canvasR.width = canvasR.height = Math.max(
|
|
1903
|
+
canvasR.width = canvasR.height = Math.max(minCanvasDimension * .15, (innerRadius << 1) + isDualVertical * radialScaleHeight);
|
|
1632
1904
|
const centerR = canvasR.width >> 1,
|
|
1633
|
-
radialY = centerR -
|
|
1905
|
+
radialY = centerR - radialScaleHeight * .7; // vertical position of text labels in the circular scale
|
|
1634
1906
|
|
|
1635
1907
|
// helper function
|
|
1636
1908
|
const radialLabel = (x, label) => {
|
|
@@ -1646,28 +1918,31 @@
|
|
|
1646
1918
|
_scaleR.restore();
|
|
1647
1919
|
};
|
|
1648
1920
|
|
|
1649
|
-
//
|
|
1650
|
-
canvasX.width
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1921
|
+
// update scale canvas dimensions and clear it
|
|
1922
|
+
canvasX.width = canvas.width;
|
|
1923
|
+
canvasX.height = xAxisHeight;
|
|
1924
|
+
if (_xAxis.backgroundColor) {
|
|
1925
|
+
_scaleX.fillStyle = _scaleR.strokeStyle = _xAxis.backgroundColor;
|
|
1926
|
+
_scaleX.fillRect(0, 0, canvasX.width, canvasX.height);
|
|
1927
|
+
_scaleR.arc(centerR, centerR, centerR - radialScaleHeight / 2, 0, TAU);
|
|
1928
|
+
_scaleR.lineWidth = radialScaleHeight;
|
|
1929
|
+
_scaleR.stroke();
|
|
1930
|
+
}
|
|
1931
|
+
_scaleX.fillStyle = _scaleR.fillStyle = _xAxis.color;
|
|
1657
1932
|
_scaleX.font = `${fontSizeX}px ${FONT_FAMILY}`;
|
|
1658
1933
|
_scaleR.font = `${fontSizeR}px ${FONT_FAMILY}`;
|
|
1659
1934
|
_scaleX.textAlign = _scaleR.textAlign = 'center';
|
|
1660
1935
|
let prevX = -labelWidthX / 4,
|
|
1661
1936
|
prevR = -labelWidthR;
|
|
1662
1937
|
for (const item of freqLabels) {
|
|
1663
|
-
const [freq, label] =
|
|
1938
|
+
const [freq, label, highlight] = isArray(item) ? item : [item, formatLabel(item)],
|
|
1664
1939
|
x = unitWidth * (this._freqScaling(freq) - scaleMin),
|
|
1665
1940
|
y = canvasX.height * .75,
|
|
1666
1941
|
isC = label[0] == 'C',
|
|
1667
1942
|
maxW = fontSizeX * (_noteLabels && !_mirror && !isDualHorizontal ? isC ? 1.2 : .6 : 3);
|
|
1668
1943
|
|
|
1669
1944
|
// set label color - no highlight when mirror effect is active (only Cs displayed)
|
|
1670
|
-
_scaleX.fillStyle = _scaleR.fillStyle =
|
|
1945
|
+
_scaleX.fillStyle = _scaleR.fillStyle = highlight ? _xAxis.highlightColor : _xAxis.color;
|
|
1671
1946
|
|
|
1672
1947
|
// prioritizes which note labels are displayed, due to the restricted space on some ranges/scales
|
|
1673
1948
|
if (_noteLabels) {
|
|
@@ -1728,13 +2003,10 @@
|
|
|
1728
2003
|
|
|
1729
2004
|
const {
|
|
1730
2005
|
isAlpha,
|
|
1731
|
-
isBands,
|
|
1732
2006
|
isLeds,
|
|
1733
2007
|
isLumi,
|
|
1734
|
-
isOctaves,
|
|
1735
2008
|
isOutline,
|
|
1736
|
-
isRound
|
|
1737
|
-
noLedGap
|
|
2009
|
+
isRound
|
|
1738
2010
|
} = this._flg,
|
|
1739
2011
|
{
|
|
1740
2012
|
analyzerHeight,
|
|
@@ -1745,17 +2017,18 @@
|
|
|
1745
2017
|
channelGap,
|
|
1746
2018
|
initialX,
|
|
1747
2019
|
innerRadius,
|
|
1748
|
-
outerRadius
|
|
2020
|
+
outerRadius,
|
|
2021
|
+
xAxisHeight,
|
|
2022
|
+
yAxisWidth
|
|
1749
2023
|
} = this._aux,
|
|
1750
2024
|
{
|
|
2025
|
+
_activeThemes,
|
|
1751
2026
|
_bars,
|
|
1752
2027
|
canvas,
|
|
1753
|
-
_canvasGradients,
|
|
1754
2028
|
_chLayout,
|
|
1755
2029
|
_colorMode,
|
|
1756
2030
|
_ctx,
|
|
1757
2031
|
_energy,
|
|
1758
|
-
_fadePeaks,
|
|
1759
2032
|
fillAlpha,
|
|
1760
2033
|
_fps,
|
|
1761
2034
|
_linearAmplitude,
|
|
@@ -1764,32 +2037,34 @@
|
|
|
1764
2037
|
minDecibels,
|
|
1765
2038
|
_mirror,
|
|
1766
2039
|
_mode,
|
|
1767
|
-
|
|
2040
|
+
_peakLine,
|
|
2041
|
+
_peaks,
|
|
1768
2042
|
_radial,
|
|
1769
|
-
|
|
1770
|
-
|
|
2043
|
+
showLedMask,
|
|
2044
|
+
_sxshow,
|
|
1771
2045
|
useCanvas,
|
|
1772
|
-
_weightingFilter
|
|
2046
|
+
_weightingFilter,
|
|
2047
|
+
_xAxis,
|
|
2048
|
+
_yAxis
|
|
1773
2049
|
} = this,
|
|
2050
|
+
[ledCount, ledHeight, ledGap] = this._leds,
|
|
1774
2051
|
canvasX = this._scaleX.canvas,
|
|
1775
2052
|
canvasR = this._scaleR.canvas,
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
isSingle = _chLayout == CHANNEL_SINGLE,
|
|
1784
|
-
isTrueLeds = isLeds && this._trueLeds && _colorMode == COLOR_GRADIENT,
|
|
2053
|
+
holdFrames = _fps * this._peakHoldTime,
|
|
2054
|
+
isDualCombined = _chLayout == LAYOUT_COMBINED,
|
|
2055
|
+
isDualHorizontal = _chLayout == LAYOUT_HORIZONTAL,
|
|
2056
|
+
isDualVertical = _chLayout == LAYOUT_VERTICAL,
|
|
2057
|
+
isGraphMode = _mode == MODE_GRAPH,
|
|
2058
|
+
isSingle = _chLayout == LAYOUT_SINGLE,
|
|
2059
|
+
isVintageLeds = isLeds && this._ledBars == LEDS_VINTAGE && _colorMode == COLORMODE_GRADIENT,
|
|
1785
2060
|
analyzerWidth = _radial ? canvas.width : this._aux.analyzerWidth,
|
|
1786
2061
|
finalX = initialX + analyzerWidth,
|
|
1787
|
-
|
|
2062
|
+
showPeaks = _peaks != PEAKS_OFF,
|
|
2063
|
+
showPeakLine = showPeaks && _peakLine > 0 && isGraphMode,
|
|
1788
2064
|
maxBarHeight = _radial ? outerRadius - innerRadius : analyzerHeight,
|
|
1789
|
-
nominalMaxHeight = maxBarHeight / this._pixelRatio,
|
|
1790
|
-
// for consistent gravity on lo-res or hi-dpi
|
|
1791
2065
|
dbRange = maxDecibels - minDecibels,
|
|
1792
|
-
|
|
2066
|
+
decayRate = 2 / this._peakDecayTime ** 2 / _fps ** 2,
|
|
2067
|
+
ledUnitHeight = ledHeight + ledGap;
|
|
1793
2068
|
if (_energy.val > 0 && _fps > 0) this._spinAngle += this._spinSpeed * TAU / 60 / _fps; // spinSpeed * angle increment per frame for 1 RPM
|
|
1794
2069
|
|
|
1795
2070
|
/* HELPER FUNCTIONS */
|
|
@@ -1797,13 +2072,14 @@
|
|
|
1797
2072
|
// create Reflex effect
|
|
1798
2073
|
const doReflex = channel => {
|
|
1799
2074
|
if (this._reflexRatio > 0 && !isLumi && !_radial) {
|
|
2075
|
+
const scaleHeight = xAxisHeight * (!_xAxis.overlay && _sxshow);
|
|
1800
2076
|
let posY, height;
|
|
1801
2077
|
if (this.reflexFit || isDualVertical) {
|
|
1802
|
-
// always fit reflex in vertical
|
|
2078
|
+
// always fit reflex in dual-vertical mode
|
|
1803
2079
|
posY = isDualVertical && channel == 0 ? channelHeight + channelGap : 0;
|
|
1804
|
-
height = channelHeight - analyzerHeight;
|
|
2080
|
+
height = channelHeight - analyzerHeight - scaleHeight;
|
|
1805
2081
|
} else {
|
|
1806
|
-
posY = canvas.height - analyzerHeight * 2;
|
|
2082
|
+
posY = canvas.height - analyzerHeight * 2 - scaleHeight;
|
|
1807
2083
|
height = analyzerHeight;
|
|
1808
2084
|
}
|
|
1809
2085
|
_ctx.save();
|
|
@@ -1813,7 +2089,7 @@
|
|
|
1813
2089
|
if (this.reflexBright != 1) _ctx.filter = `brightness(${this.reflexBright})`;
|
|
1814
2090
|
|
|
1815
2091
|
// create the reflection
|
|
1816
|
-
_ctx.setTransform(1, 0, 0, -1, 0, canvas.height);
|
|
2092
|
+
_ctx.setTransform(1, 0, 0, -1, 0, canvas.height - scaleHeight);
|
|
1817
2093
|
_ctx.drawImage(canvas, 0, channelCoords[channel].channelTop, canvas.width, analyzerHeight, 0, posY, canvas.width, height);
|
|
1818
2094
|
_ctx.restore();
|
|
1819
2095
|
}
|
|
@@ -1821,15 +2097,80 @@
|
|
|
1821
2097
|
|
|
1822
2098
|
// draw scale on X-axis
|
|
1823
2099
|
const drawScaleX = () => {
|
|
1824
|
-
if (
|
|
2100
|
+
if (_sxshow) {
|
|
1825
2101
|
if (_radial) {
|
|
1826
2102
|
_ctx.save();
|
|
1827
2103
|
_ctx.translate(centerX, centerY);
|
|
1828
2104
|
if (this._spinSpeed) _ctx.rotate(this._spinAngle + HALF_PI);
|
|
1829
2105
|
_ctx.drawImage(canvasR, -canvasR.width >> 1, -canvasR.width >> 1);
|
|
1830
2106
|
_ctx.restore();
|
|
1831
|
-
} else
|
|
2107
|
+
} else {
|
|
2108
|
+
_ctx.drawImage(canvasX, 0, canvas.height - canvasX.height);
|
|
2109
|
+
if (isDualVertical) _ctx.drawImage(canvasX, 0, (canvas.height >> 1) - canvasX.height);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2114
|
+
// draw scale on Y-axis - TO-DO: handle reflex!
|
|
2115
|
+
const drawScaleY = () => {
|
|
2116
|
+
if (!this.showScaleY || isLumi || _radial) return;
|
|
2117
|
+
const {
|
|
2118
|
+
color,
|
|
2119
|
+
dbInterval,
|
|
2120
|
+
linearInterval,
|
|
2121
|
+
lineDash,
|
|
2122
|
+
operation,
|
|
2123
|
+
showSubdivisions,
|
|
2124
|
+
showUnit,
|
|
2125
|
+
subLineColor,
|
|
2126
|
+
subLineDash
|
|
2127
|
+
} = _yAxis,
|
|
2128
|
+
fontSize = yAxisWidth >> 1,
|
|
2129
|
+
increment = (_linearAmplitude ? linearInterval : dbInterval) / (showSubdivisions ? 2 : 1),
|
|
2130
|
+
left = yAxisWidth * .85,
|
|
2131
|
+
max = _linearAmplitude ? 100 : maxDecibels,
|
|
2132
|
+
min = _linearAmplitude ? 0 : minDecibels,
|
|
2133
|
+
right = canvas.width - yAxisWidth * .1,
|
|
2134
|
+
unit = _linearAmplitude ? '%' : 'dB',
|
|
2135
|
+
unitHeight = analyzerHeight / (max - min);
|
|
2136
|
+
_ctx.save();
|
|
2137
|
+
_ctx.globalCompositeOperation = operation;
|
|
2138
|
+
_ctx.fillStyle = color;
|
|
2139
|
+
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
2140
|
+
_ctx.textAlign = 'right';
|
|
2141
|
+
_ctx.lineWidth = 1;
|
|
2142
|
+
for (let channel = 0; channel < 1 + isDualVertical; channel++) {
|
|
2143
|
+
const {
|
|
2144
|
+
channelTop
|
|
2145
|
+
} = channelCoords[channel];
|
|
2146
|
+
for (let val = max, minor = false; val > min; val -= increment) {
|
|
2147
|
+
const posY = channelTop + (max - val) * unitHeight;
|
|
2148
|
+
if (minor && showSubdivisions) {
|
|
2149
|
+
_ctx.strokeStyle = subLineColor;
|
|
2150
|
+
_ctx.setLineDash(subLineDash);
|
|
2151
|
+
_ctx.lineDashOffset = 1;
|
|
2152
|
+
} else {
|
|
2153
|
+
let labelY = posY + fontSize * (posY == channelTop ? .8 : .35);
|
|
2154
|
+
_ctx.fillText(val, left, labelY);
|
|
2155
|
+
_ctx.fillText(val, right, labelY);
|
|
2156
|
+
if (showUnit && val - increment * (showSubdivisions ? 2 : 1) <= min) {
|
|
2157
|
+
// display unit (dB or %) below the bottom label on both sides
|
|
2158
|
+
labelY += fontSize * 1.5;
|
|
2159
|
+
_ctx.fillText(unit, left, labelY);
|
|
2160
|
+
_ctx.fillText(unit, right, labelY);
|
|
2161
|
+
}
|
|
2162
|
+
_ctx.strokeStyle = color;
|
|
2163
|
+
_ctx.setLineDash(lineDash);
|
|
2164
|
+
_ctx.lineDashOffset = 0;
|
|
2165
|
+
}
|
|
2166
|
+
_ctx.beginPath();
|
|
2167
|
+
_ctx.moveTo(yAxisWidth * !minor, ~~posY + .5); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
2168
|
+
_ctx.lineTo(canvas.width - yAxisWidth * !minor, ~~posY + .5);
|
|
2169
|
+
_ctx.stroke();
|
|
2170
|
+
if (showSubdivisions) minor = !minor;
|
|
2171
|
+
}
|
|
1832
2172
|
}
|
|
2173
|
+
_ctx.restore();
|
|
1833
2174
|
};
|
|
1834
2175
|
|
|
1835
2176
|
// returns the gain (in dB) for a given frequency, considering the currently selected weighting filter
|
|
@@ -1884,16 +2225,18 @@
|
|
|
1884
2225
|
}
|
|
1885
2226
|
};
|
|
1886
2227
|
|
|
1887
|
-
// converts
|
|
1888
|
-
const
|
|
2228
|
+
// converts an amplitude value (0-1) to an integer number of LED elements
|
|
2229
|
+
const ledUnits = value => Math.round(clamp(value, 0, 1) * ledCount);
|
|
2230
|
+
|
|
2231
|
+
// converts an amplitude value (0-1) to a height that, when subtracted from `analyzerBottom`, matches the top position of a LED element
|
|
2232
|
+
const ledPosY = value => Math.max(0, ledUnits(value) * ledUnitHeight - ledGap);
|
|
1889
2233
|
|
|
1890
2234
|
// update energy information
|
|
1891
2235
|
const updateEnergy = newVal => {
|
|
1892
2236
|
_energy.val = newVal;
|
|
1893
2237
|
if (_energy.peak > 0) {
|
|
1894
2238
|
_energy.hold--;
|
|
1895
|
-
if (_energy.hold < 0) _energy.peak += _energy.hold *
|
|
1896
|
-
// TO-DO: replace `canvas.height * this._pixelRatio` with `maxNominalHeight` when implementing dual-channel energy
|
|
2239
|
+
if (_energy.hold < 0) _energy.peak += _energy.hold * decayRate;
|
|
1897
2240
|
}
|
|
1898
2241
|
if (newVal >= _energy.peak) {
|
|
1899
2242
|
_energy.peak = newVal;
|
|
@@ -1903,20 +2246,24 @@
|
|
|
1903
2246
|
|
|
1904
2247
|
/* MAIN FUNCTION */
|
|
1905
2248
|
|
|
1906
|
-
|
|
2249
|
+
// clear canvas
|
|
2250
|
+
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
1907
2251
|
let currentEnergy = 0;
|
|
1908
2252
|
const nBars = _bars.length,
|
|
1909
2253
|
nChannels = isSingle ? 1 : 2;
|
|
1910
2254
|
for (let channel = 0; channel < nChannels; channel++) {
|
|
1911
|
-
const
|
|
2255
|
+
const theme = _activeThemes[channel],
|
|
2256
|
+
{
|
|
2257
|
+
colorStops,
|
|
2258
|
+
gradient,
|
|
2259
|
+
mask
|
|
2260
|
+
} = theme,
|
|
2261
|
+
{
|
|
1912
2262
|
channelTop,
|
|
1913
2263
|
channelBottom,
|
|
1914
2264
|
analyzerBottom
|
|
1915
2265
|
} = channelCoords[channel],
|
|
1916
|
-
channelGradient = this._gradients[this._selectedGrads[channel]],
|
|
1917
|
-
colorStops = channelGradient.colorStops,
|
|
1918
2266
|
colorCount = colorStops.length,
|
|
1919
|
-
bgColor = !showBgColor || isLeds && !overlay ? '#000' : channelGradient.bgColor,
|
|
1920
2267
|
radialDirection = isDualVertical && _radial && channel ? -1 : 1,
|
|
1921
2268
|
// 1 = outwards, -1 = inwards
|
|
1922
2269
|
invertedChannel = !channel && _mirror == -1 || channel && _mirror == 1,
|
|
@@ -1945,43 +2292,6 @@
|
|
|
1945
2292
|
}
|
|
1946
2293
|
}
|
|
1947
2294
|
*/
|
|
1948
|
-
// draw scale on Y-axis (uses: channel, channelTop)
|
|
1949
|
-
const drawScaleY = () => {
|
|
1950
|
-
const scaleWidth = canvasX.height,
|
|
1951
|
-
fontSize = scaleWidth >> 1,
|
|
1952
|
-
max = _linearAmplitude ? 100 : maxDecibels,
|
|
1953
|
-
min = _linearAmplitude ? 0 : minDecibels,
|
|
1954
|
-
incr = _linearAmplitude ? 20 : 5,
|
|
1955
|
-
interval = analyzerHeight / (max - min),
|
|
1956
|
-
atStart = _mirror != -1 && (!isDualHorizontal || channel == 0 || _mirror == 1),
|
|
1957
|
-
atEnd = _mirror != 1 && (!isDualHorizontal || channel != _mirror);
|
|
1958
|
-
_ctx.save();
|
|
1959
|
-
_ctx.fillStyle = SCALEY_LABEL_COLOR;
|
|
1960
|
-
_ctx.font = `${fontSize}px ${FONT_FAMILY}`;
|
|
1961
|
-
_ctx.textAlign = 'right';
|
|
1962
|
-
_ctx.lineWidth = 1;
|
|
1963
|
-
for (let val = max; val > min; val -= incr) {
|
|
1964
|
-
const posY = channelTop + (max - val) * interval,
|
|
1965
|
-
even = val % 2 == 0 | 0;
|
|
1966
|
-
if (even) {
|
|
1967
|
-
const labelY = posY + fontSize * (posY == channelTop ? .8 : .35);
|
|
1968
|
-
if (atStart) _ctx.fillText(val, scaleWidth * .85, labelY);
|
|
1969
|
-
if (atEnd) _ctx.fillText(val, (isDualHorizontal ? analyzerWidth : canvas.width) - scaleWidth * .1, labelY);
|
|
1970
|
-
_ctx.strokeStyle = SCALEY_LABEL_COLOR;
|
|
1971
|
-
_ctx.setLineDash([2, 4]);
|
|
1972
|
-
_ctx.lineDashOffset = 0;
|
|
1973
|
-
} else {
|
|
1974
|
-
_ctx.strokeStyle = SCALEY_MIDLINE_COLOR;
|
|
1975
|
-
_ctx.setLineDash([2, 8]);
|
|
1976
|
-
_ctx.lineDashOffset = 1;
|
|
1977
|
-
}
|
|
1978
|
-
_ctx.beginPath();
|
|
1979
|
-
_ctx.moveTo(initialX + scaleWidth * even * atStart, ~~posY + .5); // for sharp 1px line (https://stackoverflow.com/a/13879402/2370385)
|
|
1980
|
-
_ctx.lineTo(finalX - scaleWidth * even * atEnd, ~~posY + .5);
|
|
1981
|
-
_ctx.stroke();
|
|
1982
|
-
}
|
|
1983
|
-
_ctx.restore();
|
|
1984
|
-
};
|
|
1985
2295
|
|
|
1986
2296
|
// FFT bin data interpolation (uses fftData)
|
|
1987
2297
|
const interpolate = (bin, ratio) => {
|
|
@@ -2016,12 +2326,26 @@
|
|
|
2016
2326
|
_ctx.fill();
|
|
2017
2327
|
};
|
|
2018
2328
|
|
|
2019
|
-
//
|
|
2020
|
-
const
|
|
2329
|
+
// render a bar of LEDs where each element has a single color (uses: analyzerBottom, isLumi)
|
|
2330
|
+
const renderVintageLeds = (colorStops, barCenter, barHeight, barValue) => {
|
|
2331
|
+
const colorIndex = isLumi ? 0 : colorStops.findLastIndex(item => ledUnits(barValue) <= ledUnits(item.level)),
|
|
2332
|
+
savedStrokeStyle = _ctx.strokeStyle;
|
|
2333
|
+
let last = analyzerBottom;
|
|
2334
|
+
for (let i = colorCount - 1; i >= colorIndex; i--) {
|
|
2335
|
+
_ctx.strokeStyle = colorStops[i].color;
|
|
2336
|
+
let y = analyzerBottom - (i == colorIndex ? barHeight : ledPosY(colorStops[i].level));
|
|
2337
|
+
strokeBar(barCenter, last, y);
|
|
2338
|
+
last = y - ledGap;
|
|
2339
|
+
}
|
|
2340
|
+
_ctx.strokeStyle = savedStrokeStyle;
|
|
2341
|
+
};
|
|
2342
|
+
|
|
2343
|
+
// set fillStyle and strokeStyle according to current colorMode (uses: colorStops, colorCount, gradient)
|
|
2344
|
+
const setBarColor = (colorStops, value = 0, barIndex = 0) => {
|
|
2021
2345
|
let color;
|
|
2022
2346
|
// for graph mode, always use the channel gradient (ignore colorMode)
|
|
2023
|
-
if (_colorMode ==
|
|
2024
|
-
const selectedIndex = _colorMode ==
|
|
2347
|
+
if (_colorMode == COLORMODE_GRADIENT && !isVintageLeds || isGraphMode) color = gradient;else {
|
|
2348
|
+
const selectedIndex = _colorMode == COLORMODE_INDEX ? barIndex % colorCount : colorStops.findLastIndex(item => isLeds ? ledUnits(value) <= ledUnits(item.level) : value <= item.level);
|
|
2025
2349
|
color = colorStops[selectedIndex].color;
|
|
2026
2350
|
}
|
|
2027
2351
|
_ctx.fillStyle = _ctx.strokeStyle = color;
|
|
@@ -2037,22 +2361,9 @@
|
|
|
2037
2361
|
_ctx.setTransform(flipX, 0, 0, 1, translateX, 0);
|
|
2038
2362
|
}
|
|
2039
2363
|
|
|
2040
|
-
// fill the analyzer background if needed (not overlay or overlay + showBgColor)
|
|
2041
|
-
if (!overlay || showBgColor) {
|
|
2042
|
-
if (overlay) _ctx.globalAlpha = this.bgAlpha;
|
|
2043
|
-
_ctx.fillStyle = bgColor;
|
|
2044
|
-
|
|
2045
|
-
// exclude the reflection area when overlay is true and reflexAlpha == 1 (avoids alpha over alpha difference, in case bgAlpha < 1)
|
|
2046
|
-
if (channel == 0 || !_radial && !isDualCombined) _ctx.fillRect(initialX, channelTop - channelGap, analyzerWidth, (overlay && this.reflexAlpha == 1 ? analyzerHeight : channelHeight) + channelGap);
|
|
2047
|
-
_ctx.globalAlpha = 1;
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
// draw dB scale (Y-axis) - avoid drawing it twice on 'dual-combined' channel layout
|
|
2051
|
-
if (this.showScaleY && !isLumi && !_radial && (channel == 0 || !isDualCombined)) drawScaleY();
|
|
2052
|
-
|
|
2053
2364
|
// set line width and dash for LEDs effect
|
|
2054
2365
|
if (isLeds) {
|
|
2055
|
-
_ctx.setLineDash([ledHeight,
|
|
2366
|
+
_ctx.setLineDash([ledHeight, ledGap]);
|
|
2056
2367
|
_ctx.lineWidth = _bars[0].width;
|
|
2057
2368
|
} else
|
|
2058
2369
|
// for outline effect ensure linewidth is not greater than half the bar width
|
|
@@ -2111,11 +2422,9 @@
|
|
|
2111
2422
|
bar.hold[channel]--;
|
|
2112
2423
|
// if hold is negative, start peak drop or fade out
|
|
2113
2424
|
if (bar.hold[channel] < 0) {
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
} else bar.peak[channel] += bar.hold[channel] * gravity / fpsSquared / Math.abs(nominalMaxHeight);
|
|
2118
|
-
// make sure the peak value is reset when using fadePeaks
|
|
2425
|
+
const acceleration = bar.hold[channel] * decayRate;
|
|
2426
|
+
if (_peaks == PEAKS_FADE && !showPeakLine) bar.alpha[channel] += acceleration;else bar.peak[channel] += acceleration;
|
|
2427
|
+
// make sure the peak value is reset when peaks fade out
|
|
2119
2428
|
if (bar.alpha[channel] <= 0) bar.peak[channel] = 0;
|
|
2120
2429
|
}
|
|
2121
2430
|
}
|
|
@@ -2135,14 +2444,14 @@
|
|
|
2135
2444
|
_ctx.globalAlpha = isLumi || isAlpha ? barValue : isOutline ? fillAlpha : 1;
|
|
2136
2445
|
|
|
2137
2446
|
// set fillStyle and strokeStyle for the current bar
|
|
2138
|
-
setBarColor(barValue, barIndex);
|
|
2447
|
+
setBarColor(colorStops, barValue, barIndex);
|
|
2139
2448
|
|
|
2140
2449
|
// compute actual bar height on screen
|
|
2141
2450
|
const barHeight = isLumi ? maxBarHeight : isLeds ? ledPosY(barValue) : barValue * maxBarHeight | 0;
|
|
2142
2451
|
|
|
2143
2452
|
// Draw current bar or line segment
|
|
2144
2453
|
|
|
2145
|
-
if (
|
|
2454
|
+
if (isGraphMode) {
|
|
2146
2455
|
// compute the average between the initial bar (barIndex==0) and the next one
|
|
2147
2456
|
// used to smooth the curve when the initial posX is off the screen, in mirror and radial modes
|
|
2148
2457
|
const nextBarAvg = barIndex ? 0 : (this._normalizedB(fftData[_bars[1].binLo]) * maxBarHeight + barHeight) / 2;
|
|
@@ -2153,7 +2462,7 @@
|
|
|
2153
2462
|
}
|
|
2154
2463
|
// draw line to the current point, avoiding overlapping wrap-around frequencies
|
|
2155
2464
|
if (posX >= 0) {
|
|
2156
|
-
const point = [
|
|
2465
|
+
const point = [barCenter, barHeight];
|
|
2157
2466
|
_ctx.lineTo(...radialXY(...point));
|
|
2158
2467
|
points.push(point);
|
|
2159
2468
|
}
|
|
@@ -2168,31 +2477,23 @@
|
|
|
2168
2477
|
}
|
|
2169
2478
|
// draw line to the current point
|
|
2170
2479
|
// avoid X values lower than the origin when mirroring left, otherwise draw them for best graph accuracy
|
|
2171
|
-
if (isDualHorizontal || _mirror != -1 || posX >= initialX) _ctx.lineTo(
|
|
2480
|
+
if (isDualHorizontal || _mirror != -1 || posX >= initialX) _ctx.lineTo(barCenter, analyzerBottom - barHeight);
|
|
2172
2481
|
}
|
|
2173
2482
|
} else {
|
|
2174
2483
|
if (isLeds) {
|
|
2175
|
-
// draw
|
|
2176
|
-
if (
|
|
2177
|
-
const
|
|
2178
|
-
_ctx.
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
}
|
|
2185
|
-
if (isTrueLeds) {
|
|
2186
|
-
// ledPosY() is used below to fit one entire led height into the selected range
|
|
2187
|
-
const colorIndex = isLumi ? 0 : colorStops.findLastIndex(item => ledPosY(barValue) <= ledPosY(item.level));
|
|
2188
|
-
let last = analyzerBottom;
|
|
2189
|
-
for (let i = colorCount - 1; i >= colorIndex; i--) {
|
|
2190
|
-
_ctx.strokeStyle = colorStops[i].color;
|
|
2191
|
-
let y = analyzerBottom - (i == colorIndex ? barHeight : ledPosY(colorStops[i].level));
|
|
2192
|
-
strokeBar(barCenter, last, y);
|
|
2193
|
-
last = y - ledSpaceV;
|
|
2484
|
+
// draw led mask - avoid drawing it twice on 'dual-combined' channel layout
|
|
2485
|
+
if (showLedMask && (!isDualCombined || channel == 0)) {
|
|
2486
|
+
const savedAlpha = _ctx.globalAlpha;
|
|
2487
|
+
_ctx.globalAlpha = 1; // TO-DO: maybe set the led mask alpha here, instead of doing it in each color?
|
|
2488
|
+
if (isVintageLeds) renderVintageLeds(mask.colorStops, barCenter, maxBarHeight, 1);else {
|
|
2489
|
+
const savedColor = _ctx.fillStyle;
|
|
2490
|
+
if (_colorMode == COLORMODE_GRADIENT) _ctx.strokeStyle = mask.gradient;else setBarColor(mask.colorStops, 0, barIndex);
|
|
2491
|
+
strokeBar(barCenter, channelTop, analyzerBottom);
|
|
2492
|
+
_ctx.fillStyle = _ctx.strokeStyle = savedColor;
|
|
2194
2493
|
}
|
|
2195
|
-
|
|
2494
|
+
_ctx.globalAlpha = savedAlpha;
|
|
2495
|
+
}
|
|
2496
|
+
if (isVintageLeds) renderVintageLeds(colorStops, barCenter, barHeight, barValue);else strokeBar(barCenter, analyzerBottom, analyzerBottom - barHeight);
|
|
2196
2497
|
} else if (posX >= initialX) {
|
|
2197
2498
|
if (_radial) radialPoly(posX, 0, width, barHeight, isOutline);else if (isRound) {
|
|
2198
2499
|
const halfWidth = width / 2,
|
|
@@ -2220,25 +2521,30 @@
|
|
|
2220
2521
|
peakAlpha = bar.alpha[channel];
|
|
2221
2522
|
if (peakValue > 0 && peakAlpha > 0 && showPeaks && !showPeakLine && !isLumi && posX >= initialX && posX < finalX) {
|
|
2222
2523
|
// set opacity for peak
|
|
2223
|
-
if (
|
|
2524
|
+
if (_peaks == PEAKS_FADE) _ctx.globalAlpha = peakAlpha;else if (isOutline && _lineWidth > 0)
|
|
2224
2525
|
// when lineWidth == 0 ctx.globalAlpha remains set to `fillAlpha`
|
|
2225
2526
|
_ctx.globalAlpha = 1;else if (isAlpha)
|
|
2226
2527
|
// isAlpha (alpha based on peak value) supersedes fillAlpha if lineWidth == 0
|
|
2227
2528
|
_ctx.globalAlpha = peakValue;
|
|
2228
2529
|
|
|
2229
|
-
//
|
|
2230
|
-
if (
|
|
2530
|
+
// use peakColor when defined by the theme in use
|
|
2531
|
+
if (theme.peakColor) {
|
|
2532
|
+
_ctx.fillStyle = _ctx.strokeStyle = theme.peakColor;
|
|
2533
|
+
} else if (_colorMode == COLORMODE_LEVEL || isVintageLeds) {
|
|
2534
|
+
// select the proper peak color for 'bar-level' colorMode or 'vintage' ledBars
|
|
2535
|
+
setBarColor(colorStops, peakValue);
|
|
2536
|
+
}
|
|
2231
2537
|
|
|
2232
2538
|
// render peak according to current mode / effect
|
|
2233
2539
|
if (isLeds) {
|
|
2234
2540
|
const ledPeak = ledPosY(peakValue);
|
|
2235
|
-
if (ledPeak >=
|
|
2541
|
+
if (ledPeak >= ledGap)
|
|
2236
2542
|
// avoid peak below first led
|
|
2237
2543
|
_ctx.fillRect(posX, analyzerBottom - ledPeak, width, ledHeight);
|
|
2238
|
-
} else if (!_radial) _ctx.fillRect(posX, analyzerBottom - peakValue * maxBarHeight, width, 2);else if (
|
|
2544
|
+
} else if (!_radial) _ctx.fillRect(isGraphMode ? barCenter : posX, analyzerBottom - peakValue * maxBarHeight, isGraphMode ? 1 : width, 2);else if (!isGraphMode) {
|
|
2239
2545
|
// radial (peaks for graph mode are done by the peakLine code)
|
|
2240
2546
|
const y = peakValue * maxBarHeight;
|
|
2241
|
-
radialPoly(posX, y, width,
|
|
2547
|
+
radialPoly(posX, y, width, _radial != RADIAL_INNER || isDualVertical || y + innerRadius >= 2 ? -2 : 2);
|
|
2242
2548
|
}
|
|
2243
2549
|
}
|
|
2244
2550
|
} // for ( let barIndex = 0; barIndex < nBars; barIndex++ )
|
|
@@ -2250,7 +2556,7 @@
|
|
|
2250
2556
|
_ctx.globalAlpha = 1;
|
|
2251
2557
|
|
|
2252
2558
|
// Fill/stroke drawing path for graph mode
|
|
2253
|
-
if (
|
|
2559
|
+
if (isGraphMode) {
|
|
2254
2560
|
setBarColor(); // select channel gradient
|
|
2255
2561
|
|
|
2256
2562
|
if (_radial && !isDualHorizontal) {
|
|
@@ -2281,26 +2587,29 @@
|
|
|
2281
2587
|
// draw peak line (and standard peaks on radial)
|
|
2282
2588
|
if (showPeakLine || _radial && showPeaks) {
|
|
2283
2589
|
points = []; // for mirror line on radial
|
|
2590
|
+
if (theme.peakColor) _ctx.fillStyle = _ctx.strokeStyle = theme.peakColor;
|
|
2284
2591
|
_ctx.beginPath();
|
|
2285
2592
|
_bars.forEach((b, i) => {
|
|
2286
|
-
let x = b.
|
|
2593
|
+
let x = b.barCenter,
|
|
2287
2594
|
h = b.peak[channel],
|
|
2288
2595
|
m = i ? 'lineTo' : 'moveTo';
|
|
2289
2596
|
if (_radial && x < 0) {
|
|
2290
2597
|
const nextBar = _bars[i + 1];
|
|
2291
|
-
h = findY(x, h, nextBar.
|
|
2598
|
+
h = findY(x, h, nextBar.barCenter, nextBar.peak[channel], 0);
|
|
2292
2599
|
x = 0;
|
|
2293
2600
|
}
|
|
2294
2601
|
h *= maxBarHeight;
|
|
2295
2602
|
if (showPeakLine) {
|
|
2296
2603
|
_ctx[m](...(_radial ? radialXY(x, h) : [x, analyzerBottom - h]));
|
|
2297
2604
|
if (_radial && _mirror && !isDualHorizontal) points.push([x, h]);
|
|
2298
|
-
} else if (
|
|
2605
|
+
} else if (b.peak[channel] > 0)
|
|
2606
|
+
// note: `h` is negative in inner radial
|
|
2607
|
+
radialPoly(x, h, 1, -2); // standard peaks (also does mirror)
|
|
2299
2608
|
});
|
|
2300
2609
|
if (showPeakLine) {
|
|
2301
2610
|
let p;
|
|
2302
2611
|
while (p = points.pop()) _ctx.lineTo(...radialXY(...p, -1)); // mirror line points
|
|
2303
|
-
_ctx.lineWidth =
|
|
2612
|
+
_ctx.lineWidth = _peakLine;
|
|
2304
2613
|
_ctx.stroke(); // stroke peak line
|
|
2305
2614
|
}
|
|
2306
2615
|
}
|
|
@@ -2325,7 +2634,8 @@
|
|
|
2325
2634
|
// restore solid lines
|
|
2326
2635
|
_ctx.setLineDash([]);
|
|
2327
2636
|
|
|
2328
|
-
// draw
|
|
2637
|
+
// draw scales
|
|
2638
|
+
drawScaleY();
|
|
2329
2639
|
drawScaleX();
|
|
2330
2640
|
}
|
|
2331
2641
|
|
|
@@ -2341,10 +2651,9 @@
|
|
|
2341
2651
|
// call callback function, if defined
|
|
2342
2652
|
if (this.onCanvasDraw) {
|
|
2343
2653
|
_ctx.save();
|
|
2344
|
-
_ctx.fillStyle = _ctx.strokeStyle = _canvasGradients[0];
|
|
2345
2654
|
this.onCanvasDraw(this, {
|
|
2346
2655
|
timestamp,
|
|
2347
|
-
|
|
2656
|
+
themes: _activeThemes
|
|
2348
2657
|
});
|
|
2349
2658
|
_ctx.restore();
|
|
2350
2659
|
}
|
|
@@ -2376,79 +2685,150 @@
|
|
|
2376
2685
|
}
|
|
2377
2686
|
|
|
2378
2687
|
/**
|
|
2379
|
-
*
|
|
2688
|
+
* Generates canvas gradients and updates _activeThemes properties
|
|
2689
|
+
*
|
|
2690
|
+
* _activeThemes = [
|
|
2691
|
+
* // one object per channel:
|
|
2692
|
+
* {
|
|
2693
|
+
* // theme name and modifiers are set by setTheme()
|
|
2694
|
+
* name: <string>,
|
|
2695
|
+
* modifiers: {
|
|
2696
|
+
* horizontal: <boolean>,
|
|
2697
|
+
* reverse: <boolean>
|
|
2698
|
+
* },
|
|
2699
|
+
*
|
|
2700
|
+
* // colorStops and peakColor come from the theme registration
|
|
2701
|
+
* colorStops: <array>,
|
|
2702
|
+
* peakColor: <string>,
|
|
2703
|
+
*
|
|
2704
|
+
* // gradient and mask.gradient are generated here
|
|
2705
|
+
* gradient: <CanvasGradient>,
|
|
2706
|
+
* mask: {
|
|
2707
|
+
* colorStops: <array>,
|
|
2708
|
+
* gradient: <CanvasGradient>
|
|
2709
|
+
* }
|
|
2710
|
+
* }
|
|
2711
|
+
* ]
|
|
2712
|
+
*
|
|
2380
2713
|
*/
|
|
2381
2714
|
_makeGrad() {
|
|
2382
2715
|
if (!this._ready) return;
|
|
2383
2716
|
const {
|
|
2384
2717
|
canvas,
|
|
2718
|
+
_chLayout,
|
|
2385
2719
|
_ctx,
|
|
2720
|
+
_horizGrad,
|
|
2721
|
+
_mirror,
|
|
2386
2722
|
_radial,
|
|
2387
|
-
_reflexRatio
|
|
2723
|
+
_reflexRatio,
|
|
2724
|
+
_spread,
|
|
2725
|
+
_xAxis
|
|
2388
2726
|
} = this,
|
|
2389
2727
|
{
|
|
2728
|
+
analyzerHeight,
|
|
2390
2729
|
analyzerWidth,
|
|
2391
2730
|
centerX,
|
|
2392
2731
|
centerY,
|
|
2732
|
+
channelHeight,
|
|
2393
2733
|
initialX,
|
|
2394
2734
|
innerRadius,
|
|
2395
|
-
outerRadius
|
|
2735
|
+
outerRadius,
|
|
2736
|
+
xAxisHeight
|
|
2396
2737
|
} = this._aux,
|
|
2397
2738
|
{
|
|
2398
2739
|
isLumi
|
|
2399
2740
|
} = this._flg,
|
|
2400
|
-
isDualVertical =
|
|
2401
|
-
|
|
2402
|
-
gradientHeight = isLumi ? canvas.height : canvas.height * (1 - _reflexRatio * !isDualVertical) | 0;
|
|
2403
|
-
// for vertical stereo we keep the full canvas height and handle the reflex areas while generating the color stops
|
|
2404
|
-
|
|
2741
|
+
isDualVertical = _chLayout == LAYOUT_VERTICAL,
|
|
2742
|
+
isDualHorizontal = _chLayout == LAYOUT_HORIZONTAL;
|
|
2405
2743
|
for (const channel of [0, 1]) {
|
|
2406
|
-
const
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2744
|
+
const {
|
|
2745
|
+
name,
|
|
2746
|
+
modifiers
|
|
2747
|
+
} = this._activeThemes[channel],
|
|
2748
|
+
analyzerRatio = _radial || modifiers.horizontal ? 1 : analyzerHeight / channelHeight,
|
|
2749
|
+
sourceTheme = deepCloneObject(this._themes[name]),
|
|
2750
|
+
{
|
|
2751
|
+
colorStops,
|
|
2752
|
+
mask
|
|
2753
|
+
} = sourceTheme,
|
|
2754
|
+
maskColorStops = mask.colorStops,
|
|
2755
|
+
maxIndex = colorStops.length - 1;
|
|
2756
|
+
|
|
2757
|
+
// compute start and end coordinates for the gradient on each channel
|
|
2758
|
+
|
|
2759
|
+
let [startX, endX, startY, endY, outer, inner] = [0, 0, 0, 0, outerRadius, innerRadius];
|
|
2760
|
+
if (_radial) {
|
|
2761
|
+
// handle radial
|
|
2762
|
+
if (isDualVertical) {
|
|
2763
|
+
// on dual-vertical radial, innerRadius is actually the center radius between both channels
|
|
2764
|
+
// so we need to compute the actual innermost gradient radius
|
|
2765
|
+
if (_spread) inner -= outer - inner;else if (channel == 1) outer = inner - (outer - inner); // top of channel 1 (outer < inner, so inverts gradient)
|
|
2766
|
+
}
|
|
2767
|
+
} else if (_spread && (isDualHorizontal && modifiers.horizontal || isDualVertical && !modifiers.horizontal)) {
|
|
2768
|
+
// handle spread gradient (both horizontal and vertical)
|
|
2769
|
+
if (modifiers.horizontal) {
|
|
2770
|
+
// on dual-horizontal layout, both channels only use the *first half* of the gradient, due to flip and translation
|
|
2771
|
+
// for spread on channel 1 we need to start of the gradient halfway off-screen, so as to use the second half of it
|
|
2772
|
+
startX = channel == 1 ? -analyzerWidth : 0;
|
|
2773
|
+
endX = startX + analyzerWidth * 2;
|
|
2774
|
+
} else endY = canvas.height;
|
|
2775
|
+
} else {
|
|
2776
|
+
if (modifiers.horizontal) {
|
|
2777
|
+
startX = isDualHorizontal && channel == 1 || _mirror == -1 ? initialX : 0;
|
|
2778
|
+
endX = startX + analyzerWidth;
|
|
2779
|
+
} else {
|
|
2780
|
+
startY = isDualVertical && channel == 1 ? channelHeight : 0;
|
|
2781
|
+
endY = startY + analyzerHeight;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2427
2784
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
offset += .5;
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2785
|
+
// helper function
|
|
2786
|
+
const createNewGradient = _ => _ctx.createLinearGradient(startX, startY, endX, endY);
|
|
2787
|
+
if (modifiers.reverse) {
|
|
2788
|
+
// reverse colors only (preserve offsets and level thresholds of each colorstop)
|
|
2789
|
+
for (let i = 0; i <= maxIndex >> 1; i++) {
|
|
2790
|
+
[colorStops[i].color, colorStops[maxIndex - i].color] = [colorStops[maxIndex - i].color, colorStops[i].color];
|
|
2791
|
+
[maskColorStops[i].color, maskColorStops[maxIndex - i].color] = [maskColorStops[maxIndex - i].color, maskColorStops[i].color];
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
let gradient = _radial ? _ctx.createRadialGradient(centerX, centerY, outer, centerX, centerY, inner) : createNewGradient(),
|
|
2795
|
+
maskGradient = _radial ? null : createNewGradient(); // no LEDs in radial
|
|
2442
2796
|
|
|
2443
|
-
|
|
2444
|
-
|
|
2797
|
+
colorStops.forEach((colorStop, index) => {
|
|
2798
|
+
let offset = colorStop.pos;
|
|
2445
2799
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2800
|
+
// additional offset processing to account for spread gradient combined with reflex and/or X-axis display on dual-vertical layout
|
|
2801
|
+
// TO-DO: add support for no scale overlay on radial too? Requires changes to outerRadius and innerRadius computation in calcBars()
|
|
2802
|
+
if (!_radial && _spread && isDualVertical && !modifiers.horizontal) {
|
|
2803
|
+
// "shrink" each offset to fit into the usable analyzer area
|
|
2804
|
+
offset *= analyzerRatio;
|
|
2805
|
+
|
|
2806
|
+
// skip top reflex + X-axis areas, on all offsets below it (>.5)
|
|
2807
|
+
if (offset > .5 * analyzerRatio) offset += (1 - analyzerRatio) / 2;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// add computed color stop to the gradient
|
|
2811
|
+
gradient.addColorStop(clamp(offset, 0, 1), colorStop.color);
|
|
2812
|
+
if (maskGradient) maskGradient.addColorStop(clamp(offset, 0, 1), maskColorStops[index].color);
|
|
2813
|
+
});
|
|
2814
|
+
this._activeThemes[channel] = {
|
|
2815
|
+
name,
|
|
2816
|
+
// set by setTheme()
|
|
2817
|
+
modifiers,
|
|
2818
|
+
...sourceTheme,
|
|
2819
|
+
// preserves properties from the source theme, not changed here, like `peakColor`
|
|
2820
|
+
colorStops,
|
|
2821
|
+
// from the source theme, but modified by this method if `flipGrad` is on
|
|
2822
|
+
gradient,
|
|
2823
|
+
// generated by this method
|
|
2824
|
+
mask: {
|
|
2825
|
+
...sourceTheme.mask,
|
|
2826
|
+
// preserves any original properties (future-proof!)
|
|
2827
|
+
colorStops: maskColorStops,
|
|
2828
|
+
// from the source theme, but modified by this method if `flipGrad` is on
|
|
2829
|
+
gradient: maskGradient // generated by this method
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2452
2832
|
} // for ( const channel of [0,1] )
|
|
2453
2833
|
}
|
|
2454
2834
|
|
|
@@ -2458,7 +2838,6 @@
|
|
|
2458
2838
|
_normalizedB(value) {
|
|
2459
2839
|
const isLinear = this._linearAmplitude,
|
|
2460
2840
|
boost = isLinear ? 1 / this._linearBoost : 1,
|
|
2461
|
-
clamp = (val, min, max) => val <= min ? min : val >= max ? max : val,
|
|
2462
2841
|
dBToLinear = val => 10 ** (val / 20);
|
|
2463
2842
|
let maxValue = this.maxDecibels,
|
|
2464
2843
|
minValue = this.minDecibels;
|
|
@@ -2479,7 +2858,6 @@
|
|
|
2479
2858
|
canvas,
|
|
2480
2859
|
_ctx
|
|
2481
2860
|
} = this,
|
|
2482
|
-
canvasX = this._scaleX.canvas,
|
|
2483
2861
|
pixelRatio = window.devicePixelRatio / (this._loRes + 1);
|
|
2484
2862
|
let screenWidth = window.screen.width * pixelRatio,
|
|
2485
2863
|
screenHeight = window.screen.height * pixelRatio;
|
|
@@ -2503,49 +2881,23 @@
|
|
|
2503
2881
|
canvas.width = newWidth;
|
|
2504
2882
|
canvas.height = newHeight;
|
|
2505
2883
|
|
|
2506
|
-
// if not in overlay mode, paint the canvas black
|
|
2507
|
-
if (!this.overlay) {
|
|
2508
|
-
_ctx.fillStyle = '#000';
|
|
2509
|
-
_ctx.fillRect(0, 0, newWidth, newHeight);
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
2884
|
// set lineJoin property for area fill mode (this is reset whenever the canvas size changes)
|
|
2513
2885
|
_ctx.lineJoin = 'bevel';
|
|
2514
2886
|
|
|
2515
|
-
// update dimensions of the scale canvas
|
|
2516
|
-
canvasX.width = newWidth;
|
|
2517
|
-
canvasX.height = Math.max(20 * pixelRatio, Math.min(newWidth, newHeight) / 32 | 0);
|
|
2518
|
-
|
|
2519
2887
|
// calculate bar positions and led options
|
|
2520
2888
|
this._calcBars();
|
|
2521
2889
|
|
|
2522
|
-
// (re)generate
|
|
2890
|
+
// (re)generate gradients
|
|
2523
2891
|
this._makeGrad();
|
|
2524
2892
|
|
|
2525
2893
|
// detect fullscreen changes (for Safari)
|
|
2526
|
-
if (this._fsStatus !== undefined && this._fsStatus !== isFullscreen) reason =
|
|
2894
|
+
if (this._fsStatus !== undefined && this._fsStatus !== isFullscreen) reason = REASON_FULLSCREENCHANGE;
|
|
2527
2895
|
this._fsStatus = isFullscreen;
|
|
2528
2896
|
|
|
2529
2897
|
// call the callback function, if defined
|
|
2530
2898
|
if (this.onCanvasResize) this.onCanvasResize(reason, this);
|
|
2531
2899
|
}
|
|
2532
2900
|
|
|
2533
|
-
/**
|
|
2534
|
-
* Select a gradient for one or both channels
|
|
2535
|
-
*
|
|
2536
|
-
* @param {string} name gradient name
|
|
2537
|
-
* @param [{number}] desired channel (0 or 1) - if empty or invalid, sets both channels
|
|
2538
|
-
*/
|
|
2539
|
-
_setGradient(name, channel) {
|
|
2540
|
-
if (!this._gradients.hasOwnProperty(name)) throw new AudioMotionError(ERR_UNKNOWN_GRADIENT, name);
|
|
2541
|
-
if (![0, 1].includes(channel)) {
|
|
2542
|
-
this._selectedGrads[1] = name;
|
|
2543
|
-
channel = 0;
|
|
2544
|
-
}
|
|
2545
|
-
this._selectedGrads[channel] = name;
|
|
2546
|
-
this._makeGrad();
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
2901
|
/**
|
|
2550
2902
|
* Set object properties
|
|
2551
2903
|
*/
|
|
@@ -2553,11 +2905,11 @@
|
|
|
2553
2905
|
// callback functions properties
|
|
2554
2906
|
const callbacks = ['onCanvasDraw', 'onCanvasResize'];
|
|
2555
2907
|
|
|
2556
|
-
// properties not in the defaults
|
|
2557
|
-
const extraProps = ['
|
|
2908
|
+
// allow other properties not in the defaults
|
|
2909
|
+
const extraProps = ['themeLeft', 'themeRight'];
|
|
2558
2910
|
|
|
2559
|
-
// build an array of valid properties
|
|
2560
|
-
const validProps = Object.keys(DEFAULT_SETTINGS).
|
|
2911
|
+
// build an array of valid properties
|
|
2912
|
+
const validProps = Object.keys(DEFAULT_SETTINGS).concat(callbacks, extraProps);
|
|
2561
2913
|
if (useDefaults || options === undefined) options = {
|
|
2562
2914
|
...DEFAULT_SETTINGS,
|
|
2563
2915
|
...options
|
|
@@ -2570,9 +2922,6 @@
|
|
|
2570
2922
|
// set only valid properties
|
|
2571
2923
|
this[prop] = options[prop];
|
|
2572
2924
|
}
|
|
2573
|
-
|
|
2574
|
-
// deprecated - move this to the constructor in the next major release (`start` should be constructor-specific)
|
|
2575
|
-
if (options.start !== undefined) this.toggleAnalyzer(options.start);
|
|
2576
2925
|
}
|
|
2577
2926
|
}
|
|
2578
2927
|
_exports.AudioMotionAnalyzer = AudioMotionAnalyzer;
|