/*
* This file is part of AUX.
*
* AUX is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* AUX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General
* Public License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*/
import { addClass } from '../utils/dom.js';
import { warn } from '../utils/log.js';
import { forEachArrayDiff } from '../utils/array_diff.js';
import { FrequencyResponse } from './frequencyresponse.js';
import { EqBand } from './eqband.js';
import { Graph } from './graph.js';
import { inheritChildOptions } from '../child_widget.js';
import { defineRecalculation } from '../renderer.js';
function fastDrawPLinear(X, Y) {
const ret = [];
const len = X.length;
let dy, x, y, tmp;
const accuracy = 20;
if (len < 2) return '';
x = +X[0];
y = +Y[0];
ret.push('M', x.toFixed(2), ',', y.toFixed(2));
x = +X[1];
y = +Y[1];
dy = ((y - Y[0]) * accuracy) | 0;
for (let i = 2; i < len; i++) {
tmp = ((Y[i] - y) * accuracy) | 0;
if (tmp !== dy) {
ret.push('L', x.toFixed(2), ',', y.toFixed(2));
dy = tmp;
}
x = +X[i];
y = +Y[i];
}
ret.push('L', x.toFixed(2), ',', y.toFixed(2));
return ret.join('');
}
function drawGraph(bands) {
const O = this.options;
let c = 0;
const end = this.range_x.get('basis') | 0;
const step = O.accuracy;
const over = O.oversampling;
const thres = O.threshold;
const x_px_to_val = this.range_x.pixelToValue.bind(this.range_x);
const y_val_to_px = this.range_y.valueToPixel.bind(this.range_y);
let i, j, k;
let x, y;
let pursue;
const X = new Array(end / step + 4);
X[0] = -10;
X[1] = -10;
for (i = 2; i < X.length - 2; i++) {
X[i] = c;
c += step;
}
X[X.length - 2] = end + 10;
X[X.length - 1] = end + 10;
const Y = new Array(end / step + 4);
Y[0] = y_val_to_px(0);
Y[Y.length - 1] = y_val_to_px(0);
for (i = 2; i < X.length - 2; i++) {
x = x_px_to_val(X[i]);
y = 0.0;
for (j = 0; j < bands.length; j++) y += bands[j](x);
Y[i] = y_val_to_px(y);
const diff = Math.abs(Y[i] - Y[i - 1]) >= thres;
if (i && over > 1 && (diff || pursue)) {
if (diff) pursue = true;
else if (!diff && pursue) pursue = false;
for (k = 1; k < over; k++) {
x = X[i - k] + (step / over) * k;
X.splice(i, 0, x);
x = x_px_to_val(x);
y = 0.0;
for (j = 0; j < bands.length; j++) y += bands[j](x);
Y.splice(i, 0, y_val_to_px(y));
i++;
}
}
Y[1] = Y[2];
Y[Y.length - 2] = Y[Y.length - 3];
if (!isFinite(Y[i])) {
warn('Singular filter in Equalizer.');
return void 0;
}
}
return fastDrawPLinear(X, Y);
}
/**
* EqualizerGraph is a special {@link Graph}, which contains a list of {@link EqBand}s and draws the
* resulting frequency response curve.
*
* @property {Number} [options.accuracy=1] - The distance between points on
* the x axis. Reduces CPU load in favour of accuracy and smoothness.
* @property {Array} [options.bands=[]] - The list of {@link EqBand}s.
* @property {Number} [options.oversampling=5] - If slope of the curve is too
* steep, oversample n times in order to not miss e.g. notch filters.
* @property {Number} [options.threshold=5] - Steepness of slope to oversample,
* i.e. y pixels difference per x pixel
* @property {Function} [options.rendering_filter=(b) => b.get('active')] - A
* callback function which can be used to customize which equalizer bands
* are included when rendering the frequency response curve. This defaults
* to those bands which have their `active` option set to `true`.
* @class EqualizerGraph
*
* @extends Graph
*/
export class EqualizerGraph extends Graph {
static get _options() {
return [
Graph.getOptionTypes(),
{
accuracy: 'number',
oversampling: 'number',
threshold: 'number',
bands: 'array',
rendering_filter: 'function',
},
];
}
static get options() {
return {
accuracy: 1, // the distance between points of curves on the x axis
oversampling: 4, // if slope of the curve is too steep, oversample
// n times in order to not miss a notch filter
threshold: 10, // steepness of slope, i.e. amount of y pixels difference
bands: [], // list of bands to create on init
rendering_filter: function (band) {
return band.get('active');
},
role: 'application',
};
}
static get static_events() {
return {
set_bands: function (value, key, previousValue) {
forEachArrayDiff(
previousValue,
value,
(band) => band.off('set', this._invalidate_bands),
(band) => band.on('set', this._invalidate_bands)
);
},
};
}
static get renderers() {
return [
defineRecalculation(
['bands', 'accuracy', 'rendering_filter', 'oversampling', 'threshold'],
function () {
this.set('dots', this.drawPath());
}
),
];
}
initialize(options) {
super.initialize(options);
this._invalidate_bands = this.invalidate.bind(this, 'bands');
this.get('bands').forEach((band) => band.on('set', this._invalidate_bands));
}
/**
* Returns the functions representing the frequency response of all
* active filters.
*/
getFilterFunctions() {
const bands = this.options.bands.filter(this.options.rendering_filter);
return bands.map((b) => b.filter.getFrequencyToGain());
}
/**
* Draws an SVG path for the current frequency response curve.
*/
drawPath() {
return drawGraph.call(this, this.getFilterFunctions());
}
resize() {
this.invalidate('bands');
}
addBand(band) {
this.set('bands', this.get('bands').concat([band]));
}
removeBand(band) {
this.set(
'bands',
this.get('bands').filter((b) => {
return b !== band;
})
);
}
}
function createEqBand(options, type) {
if (options instanceof EqBand) return options;
if (!type) type = EqBand;
return new type(options);
}
/**
* Equalizer is a {@link FrequencyResponse}, utilizing {@link EqBand}s instead of
* simple {@link ChartHandle}s. An Equalizer - by default - has one
* {@link EqualizerGraph} which contains all bands. Additional {@link
* EqualizerGraph}s can be added. The Equalizer inherits all options of
* {@link EqualizerGraph}.
*
* @property {Boolean} [options.show_bands=true] - Show or hide all bands.
*
* @class Equalizer
*
* @extends FrequencyResponse
*/
export class Equalizer extends FrequencyResponse {
static get _options() {
return {
show_bands: 'boolean',
};
}
static get options() {
return {
show_bands: true,
};
}
static get static_events() {
return {
set_bands: function (value, key, previousValue) {
forEachArrayDiff(
previousValue,
value,
(band) => this.removeChild(band),
(band) => this.addChild(band)
);
},
set_show_bands: function (value) {
this.set('show_handles', value);
},
};
}
initialize(options) {
super.initialize(options);
/**
* @member {HTMLDivElement} Equalizer#element - The main DIV container.
* Has class <code>.aux-equalizer</code>.
*/
/**
* @member {SVGGroup} Equalizer#_bands - The SVG group containing all the bands SVG elements.
* Has class <code>.aux-eqbands</code>.
*/
this._bands = this._handles;
addClass(this._bands, 'aux-eqbands');
/**
* @member {Graph} Equalizer#baseline - The graph drawing the zero line.
* Has class <code>.aux-baseline</code>
*/
this.baseline = new EqualizerGraph({
range_x: this.range_x,
range_y: this.range_y,
class: 'aux-baseline',
});
this.addGraph(this.baseline);
const bands = (options.bands || []).map((options) => createEqBand(options));
this.options.bands = bands;
this.addBands(bands);
}
destroy() {
this.empty();
this._bands.remove();
super.destroy();
}
draw(O, element) {
addClass(element, 'aux-equalizer');
super.draw(O, element);
}
getBands() {
return this.getChildren().filter((child) => child instanceof EqBand);
}
/**
* Add a new band to the equalizer. Options is an object containing
* options for the {@link EqBand}
*
* @method Equalizer#addBand
*
* @param {Object} [options={ }] - An object containing initial options for the {@link EqBand}.
* @param {Object} [type=EqBand] - A widget class to be used for the new band.
*
* @emits Equalizer#bandadded
*/
addChild(child) {
super.addChild(child);
if (child instanceof EqBand) {
/**
* Is fired when a new band was added.
*
* @event Equalizer#bandadded
*
* @param {EqBand} band - The {@link EqBand} which was added.
*/
this.emit('bandadded', child);
this.baseline.addBand(child);
}
}
removeChild(child) {
if (this.isDestructed()) return;
if (child instanceof EqBand) {
/**
* Is fired when a band was removed.
*
* @event Equalizer#bandremoved
*
* @param {EqBand} band - The {@link EqBand} which was removed.
*/
this.emit('bandremoved', child);
this.baseline.removeBand(child);
}
super.removeChild(child);
}
addBand(options, type) {
const band = createEqBand(options, type);
this.addChild(band);
return band;
}
/**
* Add multiple new {@link EqBand}s to the equalizer. Options is an array
* of objects containing options for the new instances of {@link EqBand}
*
* @method Equalizer#addBands
*
* @param {Array<Object>} options - An array of options objects for the {@link EqBand}.
* @param {Object} [type=EqBand] - A widget class to be used for the new band.
*/
addBands(bands, type) {
for (let i = 0; i < bands.length; i++) this.addBand(bands[i], type);
}
/**
* Remove a band from the widget.
*
* @method Equalizer#removeBand
*
* @param {EqBand} band - The {@link EqBand} to remove.
*
* @emits Equalizer#bandremoved
*/
removeBand(h) {
this.removeChild(h);
}
/**
* Remove multiple {@link EqBand} from the equalizer. Options is an array
* of {@link EqBand} instances.
*
* @method Equalizer#removeBands
*
* @param {Array<EqBand>} bands - An array of {@link EqBand} instances.
*/
removeBands(bands) {
if (!bands) bands = this.getBands();
bands.forEach((band) => this.removeBand(band));
/**
* Is fired when all bands are removed.
*
* @event Equalizer#emptied
*/
if (!this.getBands().length) this.emit('emptied');
}
set(key, value) {
if (key === 'bands') {
if (!value) {
value = [];
} else if (!Array.isArray(value)) {
throw new TypeError('Expected array of bands.');
} else {
value = value.slice(0);
}
}
return super.set(key, value);
}
}
inheritChildOptions(Equalizer, 'baseline', EqualizerGraph);