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/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 4.5.2
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 = '4.5.2';
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 CANVAS_BACKGROUND_COLOR = '#000',
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
- LEDS_UNLIT_COLOR = '#7f7f7f22',
59
- MODE_GRAPH = 10,
60
- REASON_CREATE = 'create',
61
- REASON_FSCHANGE = 'fschange',
62
- REASON_LORES = 'lores',
63
- REASON_RESIZE = EVENT_RESIZE,
64
- REASON_USER = 'user',
65
- SCALEX_BACKGROUND_COLOR = '#000c',
66
- SCALEX_LABEL_COLOR = '#fff',
67
- SCALEX_HIGHLIGHT_COLOR = '#4f4',
68
- SCALEY_LABEL_COLOR = '#888',
69
- SCALEY_MIDLINE_COLOR = '#555',
70
- SCALE_BARK = 'bark',
71
- SCALE_LINEAR = 'linear',
72
- SCALE_LOG = 'log',
73
- SCALE_MEL = 'mel';
74
-
75
- // built-in gradients
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
- GRADIENTS = [['classic', {
106
+ THEMES = [['classic', {
78
107
  colorStops: ['red', {
79
108
  color: 'yellow',
80
- level: .85,
81
- pos: .6
109
+ level: .9
82
110
  }, {
83
111
  color: 'lime',
84
- level: .475
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: false,
129
+ alphaBars: ALPHABARS_OFF,
102
130
  ansiBands: false,
131
+ bandResolution: BANDS_FFT,
103
132
  barSpace: 0.1,
104
- bgAlpha: 0.7,
105
- channelLayout: CHANNEL_SINGLE,
106
- colorMode: COLOR_GRADIENT,
107
- fadePeaks: false,
133
+ channelLayout: LAYOUT_SINGLE,
134
+ colorMode: COLORMODE_GRADIENT,
108
135
  fftSize: 8192,
109
- fillAlpha: 1,
136
+ fillAlpha: 0.5,
110
137
  frequencyScale: SCALE_LOG,
111
- gradient: GRADIENTS[0][0],
112
- gravity: 3.8,
113
138
  height: undefined,
114
- ledBars: false,
139
+ ledBars: LEDS_OFF,
115
140
  linearAmplitude: false,
116
141
  linearBoost: 1,
117
- lineWidth: 0,
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: 0,
150
+ mode: MODE_BARS,
127
151
  noteLabels: false,
128
152
  outlineBars: false,
129
- overlay: false,
130
- peakFadeTime: 750,
153
+ peakDecayTime: 750,
131
154
  peakHoldTime: 500,
132
- peakLine: false,
133
- radial: false,
134
- radialInvert: false,
135
- radius: 0.3,
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
- showPeaks: true,
165
+ showLedMask: true,
144
166
  showScaleX: true,
145
167
  showScaleY: false,
146
168
  smoothing: 0.5,
147
169
  spinSpeed: 0,
148
- splitGradient: false,
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 ERR_AUDIO_CONTEXT_FAIL = ['ERR_AUDIO_CONTEXT_FAIL', 'Could not create audio context. Web Audio API not supported?'],
159
- ERR_INVALID_AUDIO_CONTEXT = ['ERR_INVALID_AUDIO_CONTEXT', 'Provided audio context is not valid'],
160
- ERR_UNKNOWN_GRADIENT = ['ERR_UNKNOWN_GRADIENT', 'Unknown gradient'],
161
- ERR_FREQUENCY_TOO_LOW = ['ERR_FREQUENCY_TOO_LOW', 'Frequency values must be >= 1'],
162
- ERR_INVALID_MODE = ['ERR_INVALID_MODE', 'Invalid mode'],
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(error, value) {
170
- const [code, message] = error;
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
- // helper function - output deprecation warning message on console
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
- // helper function - check if a given object is empty (also returns `true` on null, undefined or any non-object value)
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
- // helper function - validate a given value with an array of strings (by default, all lowercase)
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
- // helper function - find the Y-coordinate of a point located between two other points, given its X-coordinate
191
- const findY = (x1, y1, x2, y2, x) => y1 + (y2 - y1) * (x - x1) / (x2 - x1);
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
- // AudioMotionAnalyzer class
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._canvasGradients = []; // CanvasGradient objects for channels 0 and 1
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 gradients
248
- for (const [name, options] of GRADIENTS) this.registerGradient(name, options);
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(REASON_FSCHANGE);
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
- // Set configuration options and use defaults for any missing properties
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 = !!value;
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, [CHANNEL_SINGLE, CHANNEL_HORIZONTAL, CHANNEL_VERTICAL, CHANNEL_COMBINED]);
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 != CHANNEL_SINGLE ? this._splitter : this._analyzer[0]);
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 != CHANNEL_SINGLE ? this._merger : this._output);
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, [COLOR_GRADIENT, COLOR_BAR_INDEX, COLOR_BAR_LEVEL]);
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._showLeds;
572
+ return this._ledBars;
503
573
  }
504
574
  set ledBars(value) {
505
- this._showLeds = !!value;
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 < 1) throw new AudioMotionError(ERR_FREQUENCY_TOO_LOW);else {
558
- this._maxFreq = Math.min(value, this.audioCtx.sampleRate / 2);
559
- this._calcBars();
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 < 1) throw new AudioMotionError(ERR_FREQUENCY_TOO_LOW);else {
573
- this._minFreq = +value;
574
- this._calcBars();
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
- const mode = value | 0;
590
- if (mode >= 0 && mode <= 10 && mode != 9) {
591
- this._mode = mode;
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 peakFadeTime() {
611
- return this._peakFadeTime;
675
+ get peakDecayTime() {
676
+ return this._peakDecayTime * 1e3;
612
677
  }
613
- set peakFadeTime(value) {
614
- this._peakFadeTime = value >= 0 ? +value : this._peakFadeTime || DEFAULT_SETTINGS.peakFadeTime;
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
- this._peakHoldTime = +value || 0;
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 = !!value;
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 = !!value;
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 = +value || 0;
657
- if (value < 0 || value >= 1) throw new AudioMotionError(ERR_REFLEX_OUT_OF_RANGE);else {
658
- this._reflexRatio = value;
659
- this._calcBars();
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 splitGradient() {
685
- return this._splitGradient;
759
+ get spreadGradient() {
760
+ return this._spread;
686
761
  }
687
- set splitGradient(value) {
688
- this._splitGradient = !!value;
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 == CHANNEL_SINGLE && !i ? this._output : this._merger, 0, i);
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 (!Array.isArray(sources)) sources = [sources];
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 == 'peak') return this._energy.peak;
1011
+ if (startFreq == ENERGY_PEAK) return this._energy.peak;
954
1012
  const presets = {
955
- bass: [20, 250],
956
- lowMid: [250, 500],
957
- mid: [500, 2e3],
958
- highMid: [2e3, 4e3],
959
- treble: [4e3, 16e3]
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 == CHANNEL_SINGLE ? 1 : 2;
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 (!Array.isArray(ignore)) ignore = [ignore];
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
- * Registers a custom gradient
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
- registerGradient(name, options) {
1001
- if (typeof name != 'string' || name.trim().length == 0) throw new AudioMotionError(ERR_GRADIENT_INVALID_NAME);
1002
- if (typeof options != 'object') throw new AudioMotionError(ERR_GRADIENT_NOT_AN_OBJECT);
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
- } = options;
1006
- if (!Array.isArray(colorStops) || !colorStops.length) throw new AudioMotionError(ERR_GRADIENT_MISSING_COLOR);
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 || val < 0 || val > 1;
1129
+ isInvalid = val => +val != clamp(val, 0, 1);
1009
1130
 
1010
- // normalize all colorStops as objects with `pos`, `color` and `level` properties
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 (typeof colorStop != 'object')
1134
+ if (!isObject(colorStop))
1014
1135
  // only color string was defined
1015
1136
  colorStops[index] = {
1016
- pos,
1017
- color: colorStop
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
- // make sure colorStops is in descending `level` order and that the first one has `level == 1`
1023
- // this is crucial for proper operation of 'bar-level' colorMode!
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
- this._gradients[name] = {
1027
- bgColor: options.bgColor || GRADIENT_DEFAULT_BGCOLOR,
1028
- dir: options.dir,
1029
- colorStops: colorStops
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 gradient is one of the currently selected gradients, regenerate them
1033
- if (this._selectedGrads.includes(name)) this._makeGrad();
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 < 1 || max < 1) throw new AudioMotionError(ERR_FREQUENCY_TOO_LOW);else {
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 LED effect
1063
- * If called with no arguments or if any property is invalid, clears any previous custom parameters
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 {object} [params]
1201
+ * @param {number} height of each led element (in pixels)
1202
+ * @param {number} vertical gap between led elements (in pixels)
1066
1203
  */
1067
- setLedParams(params) {
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
- if (params) {
1072
- maxLeds = params.maxLeds | 0,
1073
- // ensure integer
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
- cancelAnimationFrame(this._runId);
1129
- this._runId = 0;
1130
- }
1131
- // Start the analyzer if it was stopped and must be enabled
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
- const bars = this._bars = []; // initialize object property
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
- _radialInvert,
1198
- _reflexRatio
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 == CHANNEL_VERTICAL && !_radial,
1203
- isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
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
- isBands = _mode % 10 != 0,
1207
- // true for modes 1 to 9
1481
+ isAlpha = _alphaBars == ALPHABARS_ON && _mode == MODE_BARS,
1482
+ isBands = _bandRes > 0,
1208
1483
  isOctaves = isBands && this._frequencyScale == SCALE_LOG,
1209
- isLeds = this._showLeds && isBands && !_radial,
1210
- isLumi = this._lumiBars && isBands && !_radial,
1211
- isAlpha = this._alphaBars && !isLumi && _mode != MODE_GRAPH,
1212
- isOutline = this._outlineBars && isBands && !isLumi && !isLeds,
1213
- isRound = this._roundBars && isBands && !isLumi && !isLeds,
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
- // TODO: improve this, make it configurable?
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 = Math.min(canvas.width, canvas.height) * .375 * (_chLayout == CHANNEL_VERTICAL ? 1 : this._radius) | 0,
1500
+ let innerRadius = minCanvasDimension * .375 * (_chLayout == LAYOUT_VERTICAL ? 1 : this._radius) | 0,
1227
1501
  outerRadius = Math.min(centerX, centerY);
1228
- if (_radialInvert && _chLayout != CHANNEL_VERTICAL) [innerRadius, outerRadius] = [outerRadius, innerRadius];
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 by fadePeaks)
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, 24, 12, 8, 6, 4, 3, 2, 1][_mode],
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, 24, 12, 8, 6, 4, 3, 2, 1][_mode] * 10;
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 = this._pixelRatio / (window.devicePixelRatio > 1 && window.screen.height <= 540 ? 2 : 1);
1458
- const params = [[], [128, 3, .45],
1459
- // mode 1
1460
- [128, 4, .225],
1461
- // mode 2
1462
- [96, 6, .225],
1463
- // mode 3
1464
- [80, 6, .225],
1465
- // mode 4
1466
- [80, 6, .125],
1467
- // mode 5
1468
- [64, 6, .125],
1469
- // mode 6
1470
- [48, 8, .125],
1471
- // mode 7
1472
- [24, 16, .125] // mode 8
1473
- ];
1474
-
1475
- // use custom LED parameters if set, or the default parameters for the current mode
1476
- const customParams = this._ledParams,
1477
- [maxLeds, spaceVRatio, spaceHRatio] = customParams || params[_mode];
1478
- let ledCount,
1479
- maxHeight = analyzerHeight;
1480
- if (customParams) {
1481
- const minHeight = 2 * dPR;
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
- // remove the extra spacing below the last line of LEDs
1496
- if (noLedGap) maxHeight += spaceV;
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, spaceH
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(isLeds ? spaceH : 0, barSpacePx) / 2;
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 (uses spaceV)
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 == CHANNEL_VERTICAL ? (channelHeight + channelGap) * channel : 0,
1791
+ const channelTop = _chLayout == LAYOUT_VERTICAL ? (channelHeight + channelGap) * channel : 0,
1537
1792
  channelBottom = channelTop + channelHeight,
1538
- analyzerBottom = channelTop + analyzerHeight - (!isLeds || noLedGap ? 0 : spaceV);
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 the X-axis and radial scales
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 == CHANNEL_HORIZONTAL,
1602
- isDualVertical = this._chLayout == CHANNEL_VERTICAL,
1603
- minDimension = Math.min(canvas.width, canvas.height),
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
- scaleHeight = minDimension / 34 | 0,
1865
+ radialScaleHeight = minCanvasDimension / 34 | 0,
1607
1866
  // circular scale height (radial mode)
1608
- fontSizeX = canvasX.height >> 1,
1609
- fontSizeR = scaleHeight >> 1,
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
- if (!_noteLabels && (this._ansiBands || _frequencyScale != SCALE_LOG)) {
1614
- freqLabels.push(16, 31.5, 63, 125, 250, 500, 1e3, 2e3, 4e3);
1615
- if (_frequencyScale == SCALE_LINEAR) freqLabels.push(6e3, 8e3, 10e3, 12e3, 14e3, 16e3, 18e3, 20e3, 22e3);else freqLabels.push(8e3, 16e3);
1616
- } else {
1617
- let freq = C_1;
1618
- for (let octave = -1; octave < 11; octave++) {
1619
- for (let note = 0; note < 12; note++) {
1620
- if (freq >= this._minFreq && freq <= this._maxFreq) {
1621
- const pitch = scale[note],
1622
- isC = pitch == 'C';
1623
- if (pitch && _noteLabels && !_mirror && !isDualHorizontal || isC) freqLabels.push(_noteLabels ? [freq, pitch + (isC ? octave : '')] : freq);
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(minDimension * .15, (innerRadius << 1) + isDualVertical * scaleHeight);
1903
+ canvasR.width = canvasR.height = Math.max(minCanvasDimension * .15, (innerRadius << 1) + isDualVertical * radialScaleHeight);
1632
1904
  const centerR = canvasR.width >> 1,
1633
- radialY = centerR - scaleHeight * .7; // vertical position of text labels in the circular scale
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
- // clear scale canvas
1650
- canvasX.width |= 0;
1651
- _scaleX.fillStyle = _scaleR.strokeStyle = SCALEX_BACKGROUND_COLOR;
1652
- _scaleX.fillRect(0, 0, canvasX.width, canvasX.height);
1653
- _scaleR.arc(centerR, centerR, centerR - scaleHeight / 2, 0, TAU);
1654
- _scaleR.lineWidth = scaleHeight;
1655
- _scaleR.stroke();
1656
- _scaleX.fillStyle = _scaleR.fillStyle = SCALEX_LABEL_COLOR;
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] = Array.isArray(item) ? item : [item, item < 1e3 ? item | 0 : `${(item / 100 | 0) / 10}k`],
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 = isC && !_mirror && !isDualHorizontal ? SCALEX_HIGHLIGHT_COLOR : SCALEX_LABEL_COLOR;
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
- overlay,
2040
+ _peakLine,
2041
+ _peaks,
1768
2042
  _radial,
1769
- showBgColor,
1770
- showPeaks,
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
- fadeFrames = _fps * this._peakFadeTime / 1e3,
1777
- fpsSquared = _fps ** 2,
1778
- gravity = this._gravity * 1e3,
1779
- holdFrames = _fps * this._peakHoldTime / 1e3,
1780
- isDualCombined = _chLayout == CHANNEL_COMBINED,
1781
- isDualHorizontal = _chLayout == CHANNEL_HORIZONTAL,
1782
- isDualVertical = _chLayout == CHANNEL_VERTICAL,
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
- showPeakLine = showPeaks && this._peakLine && _mode == MODE_GRAPH,
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
- [ledCount, ledSpaceH, ledSpaceV, ledHeight] = this._leds || [];
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 stereo mode
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 (this.showScaleX) {
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 _ctx.drawImage(canvasX, 0, canvas.height - canvasX.height);
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 a value in [0;1] range to a height in pixels that fits into the current LED elements
1888
- const ledPosY = value => Math.max(0, (value * ledCount | 0) * (ledHeight + ledSpaceV) - ledSpaceV);
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 * gravity / fpsSquared / canvas.height * this._pixelRatio;
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
- if (overlay) _ctx.clearRect(0, 0, canvas.width, canvas.height);
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
- // set fillStyle and strokeStyle according to current colorMode (uses: channel, colorStops, colorCount)
2020
- const setBarColor = (value = 0, barIndex = 0) => {
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 == COLOR_GRADIENT && !isTrueLeds || _mode == MODE_GRAPH) color = _canvasGradients[channel];else {
2024
- const selectedIndex = _colorMode == COLOR_BAR_INDEX ? barIndex % colorCount : colorStops.findLastIndex(item => isLeds ? ledPosY(value) <= ledPosY(item.level) : value <= item.level);
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, ledSpaceV]);
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
- if (_fadePeaks && !showPeakLine) {
2115
- const initialAlpha = !isAlpha || isOutline && _lineWidth > 0 ? 1 : isAlpha ? bar.peak[channel] : fillAlpha;
2116
- bar.alpha[channel] = initialAlpha * (1 + bar.hold[channel] / fadeFrames); // hold is negative, so this is <= 1
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 (_mode == MODE_GRAPH) {
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 = [posX, barHeight];
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(posX, analyzerBottom - barHeight);
2480
+ if (isDualHorizontal || _mirror != -1 || posX >= initialX) _ctx.lineTo(barCenter, analyzerBottom - barHeight);
2172
2481
  }
2173
2482
  } else {
2174
2483
  if (isLeds) {
2175
- // draw "unlit" leds - avoid drawing it twice on 'dual-combined' channel layout
2176
- if (showBgColor && !overlay && (channel == 0 || !isDualCombined)) {
2177
- const alpha = _ctx.globalAlpha;
2178
- _ctx.strokeStyle = LEDS_UNLIT_COLOR;
2179
- _ctx.globalAlpha = 1;
2180
- strokeBar(barCenter, channelTop, analyzerBottom);
2181
- // restore properties
2182
- _ctx.strokeStyle = _ctx.fillStyle;
2183
- _ctx.globalAlpha = alpha;
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
- } else strokeBar(barCenter, analyzerBottom, analyzerBottom - barHeight);
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 (_fadePeaks) _ctx.globalAlpha = peakAlpha;else if (isOutline && _lineWidth > 0)
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
- // select the peak color for 'bar-level' colorMode or 'trueLeds'
2230
- if (_colorMode == COLOR_BAR_LEVEL || isTrueLeds) setBarColor(peakValue);
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 >= ledSpaceV)
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 (_mode != MODE_GRAPH) {
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, !this._radialInvert || isDualVertical || y + innerRadius >= 2 ? -2 : 2);
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 (_mode == MODE_GRAPH) {
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.posX,
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.posX, nextBar.peak[channel], 0);
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 (h > 0) radialPoly(x, h, 1, -2); // standard peaks (also does mirror)
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 = 1;
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 frequency scale (X-axis)
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
- canvasGradients: _canvasGradients
2656
+ themes: _activeThemes
2348
2657
  });
2349
2658
  _ctx.restore();
2350
2659
  }
@@ -2376,79 +2685,150 @@
2376
2685
  }
2377
2686
 
2378
2687
  /**
2379
- * Generate currently selected gradient
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 = this._chLayout == CHANNEL_VERTICAL,
2401
- analyzerRatio = 1 - _reflexRatio,
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 currGradient = this._gradients[this._selectedGrads[channel]],
2407
- colorStops = currGradient.colorStops,
2408
- isHorizontal = currGradient.dir == 'h';
2409
- let grad;
2410
- if (_radial) grad = _ctx.createRadialGradient(centerX, centerY, outerRadius, centerX, centerY, innerRadius - (outerRadius - innerRadius) * isDualVertical);else grad = _ctx.createLinearGradient(...(isHorizontal ? [initialX, 0, initialX + analyzerWidth, 0] : [0, 0, 0, gradientHeight]));
2411
- if (colorStops) {
2412
- const dual = isDualVertical && !this._splitGradient && (!isHorizontal || _radial);
2413
- for (let channelArea = 0; channelArea < 1 + dual; channelArea++) {
2414
- const maxIndex = colorStops.length - 1;
2415
- colorStops.forEach((colorStop, index) => {
2416
- let offset = colorStop.pos;
2417
-
2418
- // in dual mode (not split), use half the original offset for each channel
2419
- if (dual) offset /= 2;
2420
-
2421
- // constrain the offset within the useful analyzer areas (avoid reflex areas)
2422
- if (isDualVertical && !isLumi && !_radial && !isHorizontal) {
2423
- offset *= analyzerRatio;
2424
- // skip the first reflex area in split mode
2425
- if (!dual && offset > .5 * analyzerRatio) offset += .5 * _reflexRatio;
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
- // only for dual-vertical non-split gradient (creates full gradient on both halves of the canvas)
2429
- if (channelArea == 1) {
2430
- // add colors in reverse order if radial or lumi are active
2431
- if (_radial || isLumi) {
2432
- const revIndex = maxIndex - index;
2433
- colorStop = colorStops[revIndex];
2434
- offset = 1 - colorStop.pos / 2;
2435
- } else {
2436
- // if the first offset is not 0, create an additional color stop to prevent bleeding from the first channel
2437
- if (index == 0 && offset > 0) grad.addColorStop(.5, colorStop.color);
2438
- // bump the offset to the second half of the gradient
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
- // add gradient color stop
2444
- grad.addColorStop(offset, colorStop.color);
2797
+ colorStops.forEach((colorStop, index) => {
2798
+ let offset = colorStop.pos;
2445
2799
 
2446
- // create additional color stop at the end of first channel to prevent bleeding
2447
- if (isDualVertical && index == maxIndex && offset < .5) grad.addColorStop(.5, colorStop.color);
2448
- });
2449
- } // for ( let channelArea = 0; channelArea < 1 + dual; channelArea++ )
2450
- }
2451
- this._canvasGradients[channel] = grad;
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 gradient
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 = REASON_FSCHANGE;
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 (`stereo` is deprecated)
2557
- const extraProps = ['gradientLeft', 'gradientRight', 'stereo'];
2908
+ // allow other properties not in the defaults
2909
+ const extraProps = ['themeLeft', 'themeRight'];
2558
2910
 
2559
- // build an array of valid properties; `start` is not an actual property and is handled after setting everything else
2560
- const validProps = Object.keys(DEFAULT_SETTINGS).filter(e => e != 'start').concat(callbacks, extraProps);
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;