Source: widgets/equalizer.js

/*
 * This file is part of Toolkit.
 *
 * Toolkit 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.
 *
 * Toolkit 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
 * Lesser 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
 */
 
"use strict";
(function(w, TK){
function fast_draw_plinear(X, Y) {
    var ret = [];
    var i, len = X.length;
    var dy = 0, x, y, tmp;

    var accuracy = 20;
    var c = 0;

    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 (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;
           c++;
        }
        x = +X[i];
        y = +Y[i];
    }

    ret.push("L", x.toFixed(2), ",", y.toFixed(2));

    return ret.join("");
}
function draw_graph (graph, bands) {
    var O = this.options;
    var c = 0;
    var end = this.range_x.get("basis") | 0;
    var step = O.accuracy;
    var over = O.oversampling;
    var thres = O.threshold;
    var x_px_to_val = this.range_x.px2val;
    var y_val_to_px = this.range_y.val2px;
    var i, j, k;
    var x, y;
    var pursue;
    var diff;
                
    var X = new Array(end / step);
    for (i = 0; i < X.length; i++) {
        X[i] = c;
        c += step;
    }
    var Y = new Array(end / step);
    var y;
    
    for (i = 0; i < X.length; 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);
        var 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++;
            }
        }

        if (!isFinite(Y[i])) {
            TK.warn("Singular filter in Equalizer.");
            graph.set("dots", void(0));
            return;
        }
    }
    graph.set("dots", fast_draw_plinear(X, Y));
}
function invalidate_bands() {
    this.invalid.bands = true;
    this.trigger_draw();
}
function show_bands() {
    var b = this.bands;
    for (var i = 0; i < b.length; i ++) {
        this.add_child(b[i]);
    }
}
function hide_bands() {
    var b = this.bands;
    for (var i = 0; i < b.length; i ++) {
        this.remove_child(b[i]);
    }
}
TK.Equalizer = TK.class({
    /**
     * TK.Equalizer is a {@link TK.ResponseHandler}, utilizing {@link TK.EqBand}s instead of
     * simple {@link TK.ResponseHandle}s.
     *
     * @property {Object} options
     * 
     * @param {Number} [options.accuracy=1] - The distance between points on
     *   the x axis. Reduces CPU load in favour of accuracy and smoothness.
     * @param {Array} [options.bands=[]] - A list of bands to add on init.
     * @param {Boolean} [options.show_bands=true] - Show or hide all bands.
     * @param {Number} [options.oversampling=5] - If slope of the curve is too
     *   steep, oversample n times in order to not miss e.g. notch filters.
     * @param {Number} [options.threshold=5] - Steepness of slope to oversample,
     *   i.e. y pixels difference per x pixel
     * @class TK.Equalizer
     * 
     * @extends TK.ResponseHandler
     */
    _class: "Equalizer",
    Extends: TK.ResponseHandler,
    _options: Object.assign(Object.create(TK.ResponseHandler.prototype._options), {
        accuracy: "number",
        oversampling: "number",
        threshold: "number",
        bands:  "array",
        show_bands: "boolean",
    }),
    options: {
        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
        show_bands: true,
    },
    static_events: {
        set_bands: function(value) {
            if (this.bands.length) this.remove_bands();
            this.add_bands(value);
        },
        set_show_bands: function(value) {
            (value ? show_bands : hide_bands).call(this);
        },
    },
    
    initialize: function (options) {
        TK.ResponseHandler.prototype.initialize.call(this, options);
        /**
         * @member {Array} TK.Equalizer#bands - Array of {@link TK.EqBand} instances.
         */
        this.bands = this.handles;
        
        /**
         * @member {HTMLDivElement} TK.Equalizer#element - The main DIV container.
         *   Has class <code>toolkit-equalizer</code>.
         */
        TK.add_class(this.element, "toolkit-equalizer");
        
        /**
         * @member {SVGGroup} TK.Equalizer#_bands - The SVG group containing all the bands SVG elements.
         *   Has class <code>toolkit-eqbands</code>.
         */
        this._bands = this._handles;
        TK.add_class(this._bands, "toolkit-eqbands");
        
        /**
         * @member {TK.Graph} TK.Equalizer#baseline - The graph drawing the zero line.
         *   Has class <code>toolkit-baseline</code> 
         */
        this.baseline = this.add_graph({
            range_x:   this.range_x,
            range_y:   this.range_y,
            container: this._bands,
            dots: [{x: 20, y: 0}, {x: 20000, y: 0}],
            "class": "toolkit-baseline"
        });
        this.add_bands(this.options.bands);
    },
    
    destroy: function () {
        this.empty(); // Arne: ??? <- Markus: removes all graphs, defined in Chart
        this._bands.remove();
        TK.ResponseHandler.prototype.destroy.call(this);
    },
    redraw: function () {
        var I = this.invalid;
        var O = this.options;
        TK.ResponseHandler.prototype.redraw.call(this);
        if (I.validate("bands", "accuracy")) {
            if (this.baseline) {
                var f = [];
                for (var i = 0; i < this.bands.length; i++) {
                    if (this.bands[i].get("active")) {
                        f.push(this.bands[i].filter.get_freq2gain());
                    }
                }
                draw_graph.call(this, this.baseline, f);
            }
        }

        if (I.show_bands) {
            I.show_bands = false;
            if (O.show_bands) {
                this._bands.style.removeProperty("display");
            } else {
                this._bands.style.display = "none";
            }
        }
    },
    resize: function () {
        invalidate_bands.call(this);
        TK.ResponseHandler.prototype.resize.call(this);
    },
    /**
     * Add a new band to the equalizer. Options is an object containing
     * options for the {@link TK.EqBand}
     * 
     * @method TK.Equalizer#add_band
     * 
     * @param {Object} [options={ }] - An object containing initial options for the {@link TK.EqBand}.
     * @param {Object} [type=TK.EqBand] - A widget class to be used for the new band.
     * 
     * @emits TK.Equalizer#bandadded
     */
    add_band: function (options, type) {
        var b;
        type = type || TK.EqBand;
        if (type.prototype.isPrototypeOf(options)) {
          b = options;
        } else {
          options.container = this._bands;
          if (options.range_x === void(0))
              options.range_x = function () { return this.range_x; }.bind(this);
          if (options.range_y === void(0))
              options.range_y = function () { return this.range_y; }.bind(this);
          if (options.range_z === void(0))
              options.range_z = function () { return this.range_z; }.bind(this);
          
          options.intersect = this.intersect.bind(this);
          b = new type(options);
        }
        
        this.bands.push(b);
        b.add_event("set", invalidate_bands.bind(this));
        /**
         * Is fired when a new band was added.
         * 
         * @event TK.Equalizer#bandadded
         * 
         * @param {TK.Band} band - The {@link TK.EqBand} which was added.
         */
        this.fire_event("bandadded", b);
        if (this.options.show_bands)
            this.add_child(b);
        invalidate_bands.call(this);
        return b;
    },
    /**
     * Add multiple new {@link TK.EqBand}s to the equalizer. Options is an array
     * of objects containing options for the new instances of {@link TK.EqBand}
     * 
     * @method TK.Equalizer#add_bands
     * 
     * @param {Array<Object>} options - An array of options objects for the {@link TK.EqBand}.
     * @param {Object} [type=TK.EqBand] - A widget class to be used for the new band.
     */
    add_bands: function (bands, type) {
        for (var i = 0; i < bands.length; i++)
            this.add_band(bands[i], type);
    },
    /**
     * Remove a band from the widget.
     * 
     * @method TK.Equalizer#remove_handle
     * 
     * @param {TK.EqBand} band - The {@link TK.EqBand} to remove.
     * 
     * @emits TK.Equalizer#bandremoved
     */
    remove_band: function (h) {
        for (var i = 0; i < this.bands.length; i++) {
            if (this.bands[i] === h) {
                if (this.options.show_bands)
                    this.remove_child(h);
                
                this.bands.splice(i, 1);
                /**
                 * Is fired when a band was removed.
                 * 
                 * @event TK.Equalizer#bandremoved
                 * 
                 * @param {TK.EqBand} band - The {@link TK.EqBand} which was removed.
                 */
                this.fire_event("bandremoved", h);
                h.destroy();
                break;
            }
        }
    },
    /**
     * Remove multiple {@link TK.EqBand} from the equalizer. Options is an array
     * of {@link TK.EqBand} instances.
     * 
     * @method TK.Equalizer#remove_bands
     * 
     * @param {Array<TK.EqBand>} bands - An array of {@link TK.EqBand} instances.
     */
    remove_bands: function () {
        while (this.bands.length) {
            this.remove_band(this.bands[0]);
        }
        this.bands = [];
        /**
         * Is fired when all bands are removed.
         * 
         * @event TK.Equalizer#emptied
         */
        this.fire_event("emptied");
        invalidate_bands.call(this);
    },
    _draw_graph: draw_graph,
});
})(this, this.TK);