Source: addons/mixer.js

"use strict";

/**
 * Audio mixer add-on.
 * Implements volume faders, positional params, parametric equalizers per
 * channel and volume fader and compressor to the master section.
 * @module mixer
 */
b4w.module["mixer"] = function(exports, require) {

var m_ctl    = require("controls");
var m_hud    = require("hud");
var m_scenes = require("scenes");
var m_sfx    = require("sfx");
var m_util   = require("__util");

var TIMER_SLOW_PERIOD = 0.15;
var TIMER_FAST_PERIOD = 0.05;

var MIXER_CONTROLS_MANIFOLD = ["SWITCH_STRIP", "SWITCH_STRIP_HOLD",
        "SWITCH_PARAM", "INC_DEC", "INC_DEC_HOLD", "MUTE_SOLO"];

var _mixer_strips = [];
var _active_strip = 0;

var _filter_freq_arr = null;
var _filter_mag_arr = null;
var _filter_phase_arr = null;

/**
 * Enable mixer controls.
 */
exports.enable_mixer_controls = function() {
    init();

    // switch mixer strip
    var key_num_left = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM4);
    var key_num_right = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM6);

    // switch mixer param
    var key_num_up = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM8);
    var key_num_down = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM2);

    // change mixer param value
    var key_num_add = m_ctl.create_keyboard_sensor(m_ctl.KEY_ADD);
    var key_num_sub = m_ctl.create_keyboard_sensor(m_ctl.KEY_SUB);

    // mute-solo
    var key_num_home = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM7);
    var key_num_pgup = m_ctl.create_keyboard_sensor(m_ctl.KEY_NUM9);

    var timer_slow = m_ctl.create_timer_sensor(TIMER_SLOW_PERIOD, true);
    var timer_fast = m_ctl.create_timer_sensor(TIMER_FAST_PERIOD, true);

    var switch_strip_keys = [key_num_left, key_num_right, timer_slow];

    var switch_strip_logic = function(s) {
        return (s[0] || s[1]);
    }
    var switch_strip_logic_hold = function(s) {
        return ((s[0] || s[1]) && s[2]);
    }

    var switch_spk_cb = function(obj, id, pulse) {
        if (pulse == 1) {
            var dir = Boolean(m_ctl.get_sensor_value(obj, id, 0)) ? -1 : 1;
            switch_strip(dir);
            if (id == "SWITCH_STRIP")
                m_ctl.reset_timer_sensor(obj, id, 2, TIMER_SLOW_PERIOD + 0.3);
            else
                m_ctl.reset_timer_sensor(obj, id, 2, TIMER_SLOW_PERIOD);
        } else {
            // hold on some time
            m_ctl.reset_timer_sensor(obj, id, 2, 10);
        }
    }
    m_ctl.create_sensor_manifold(null, "SWITCH_STRIP", m_ctl.CT_TRIGGER, 
            switch_strip_keys, switch_strip_logic, switch_spk_cb);
    m_ctl.create_sensor_manifold(null, "SWITCH_STRIP_HOLD", m_ctl.CT_SHOT, 
            switch_strip_keys, switch_strip_logic_hold, switch_spk_cb);

    var switch_param_keys = [key_num_up, key_num_down];
    var switch_param_logic = function(s) {
        return (s[0] || s[1]);
    }
    var switch_param_cb = function(obj, id, pulse) {
        var dir = Boolean(m_ctl.get_sensor_value(obj, id, 0)) ? -1 : 1;
        switch_param(dir);
    }
    m_ctl.create_sensor_manifold(null, "SWITCH_PARAM", m_ctl.CT_SHOT, 
            switch_param_keys, switch_param_logic, switch_param_cb);

    var inc_dec_keys = [key_num_sub, key_num_add, timer_fast];

    var inc_dec_cb = function(obj, id, pulse) {

        if (pulse == 1) {
            var dir = Boolean(m_ctl.get_sensor_value(obj, id, 0)) ? -1 : 1;
            param_inc_dec(dir);

            if (id == "INC_DEC")
                m_ctl.reset_timer_sensor(obj, id, 2, TIMER_FAST_PERIOD + 0.3);
            else
                m_ctl.reset_timer_sensor(obj, id, 2, TIMER_FAST_PERIOD);
        } else {
            // hold on some time
            m_ctl.reset_timer_sensor(obj, id, 2, 10);
        }
    }
    var inc_dec_logic = function(s) {
        return (s[0] || s[1]);
    }
    var inc_dec_logic_hold = function(s) {
        return ((s[0] || s[1]) && s[2]);
    }
    m_ctl.create_sensor_manifold(null, "INC_DEC", m_ctl.CT_TRIGGER, 
            inc_dec_keys, inc_dec_logic, inc_dec_cb);
    m_ctl.create_sensor_manifold(null, "INC_DEC_HOLD", m_ctl.CT_SHOT, 
            inc_dec_keys, inc_dec_logic_hold, inc_dec_cb);

    var mute_solo_keys = [key_num_home, key_num_pgup];  

    var mute_solo_cb = function(obj, id, pulse) {
        var mute_solo = Boolean(m_ctl.get_sensor_value(obj, id, 0));
        if (mute_solo)
            switch_mute();
        else
            switch_solo();
    }
    var mute_solo_logic = function(s) {
        return (s[0] || s[1]);
    }
    m_ctl.create_sensor_manifold(null, "MUTE_SOLO", m_ctl.CT_SHOT,
            mute_solo_keys, mute_solo_logic, mute_solo_cb);

    var elapsed = m_ctl.create_elapsed_sensor();

    m_ctl.create_sensor_manifold(null, "MIXER_DRAW", m_ctl.CT_CONTINUOUS,
        [elapsed], null, function() {draw()});

    var timer = m_ctl.create_timer_sensor(1, true);
    m_ctl.create_sensor_manifold(null, "MIXER_UPDATE", m_ctl.CT_TRIGGER,
            [timer], null, function() {
        var strip_range = active_strip_range();
        for (var i = strip_range[0]; i <= strip_range[1]; i++)
            update_strip_params(_mixer_strips[i]);
    });
}

/**
 * Disable mixer controls.
 */
exports.disable_mixer_controls = function() {
    for (var i = 0; MIXER_CONTROLS_MANIFOLD.length; i++)
        m_ctl.remove_sensor_manifold(null, MIXER_CONTROLS_MANIFOLD[i]);
}

/**
 * Initialize mixer
 */
function init() {
    _mixer_strips.length = 0;
    _active_strip = 0;

    _filter_freq_arr = gen_freq_arr(100);
    _filter_mag_arr = new Float32Array(_filter_freq_arr.length);
    _filter_phase_arr = new Float32Array(_filter_freq_arr.length);

    var speakers = m_sfx.get_speaker_objects();
    if (!speakers.length)
        return;

    _mixer_strips.push(create_master_strip());

    for (var i = 0; i < speakers.length; i++) {
        var spk = speakers[i];
        _mixer_strips.push(create_speaker_strip(spk));
    }

    // special strips first, then by name
    _mixer_strips.sort(function(a,b) {
        if (a.id == "MASTER")
            return -1;
        else if (b.id == "MASTER")
            return 1;
        else if (a.id == "COMPRESSOR")
            return -1;
        else if (b.id == "COMPRESSOR")
            return 1;
        else if (a.id.toUpperCase() < b.id.toUpperCase())
            return -1;
        else if (a.id.toUpperCase() > b.id.toUpperCase())
            return 1;
        else
            return 0;
    });
}

function gen_freq_arr(steps) {
    var FMIN = 20;
    var FMAX = 20000;

    var freq_arr = new Float32Array(steps);

    var freq_base = FMAX/FMIN;
    var freq_pow = 0;

    for (var i = 0; i < steps; i++) {
        freq_arr[i] = FMIN * Math.pow(freq_base, freq_pow);
        freq_pow += 1 / (steps - 1);
    }

    return freq_arr;
}

function create_master_strip() {
    var strip = init_strip("MASTER");

    var cparams = m_sfx.get_compressor_params();
    if (cparams) {
        strip.params.push(["THRESHOLD", cparams["threshold"], -100, 0, 100, false]);
        strip.params.push(["KNEE", cparams["knee"], 0, 40, 40, false]);
        strip.params.push(["RATIO", cparams["ratio"], 1, 20, 20, false]);
        strip.params.push(["ATTACK", cparams["attack"], 0, 1, 1000, false]);
        strip.params.push(["RELEASE", cparams["release"], 0, 1, 1000, false]);
    }

    strip.params.push(["VOLUME", m_sfx.get_volume(null), 0, 1, 50, false]);

    strip.mute = m_sfx.is_muted(null) ? 1 : 0;
    strip.solo = -1;

    return strip;
}

function init_strip(id) {
    return {
        id : id,
        params : [],
        active_param : 0,
        mute: -1,
        solo: -1,
        speaker: null
    }
}

function create_speaker_strip(spk) {
    var strip = init_strip(m_scenes.get_object_name(spk));
    strip.mute = m_sfx.is_muted(spk) ? 1 : 0;
    strip.solo = 0;
    strip.speaker = spk;
    return strip;
}

function switch_strip(dir) {
    if (!_mixer_strips.length)
        return;

    if (dir == 1 && _active_strip < (_mixer_strips.length - 1)) {
        _active_strip++;
    } else if (dir == -1 && _active_strip > 0) {
        _active_strip--;
    }

    var strip_range = active_strip_range();
    for (var i = strip_range[0]; i <= strip_range[1]; i++)
        update_strip_params(_mixer_strips[i]);
}

function update_strip_params(strip) {

    if (!strip.speaker)
        return;

    // cleanup
    strip.params.length = 0;

    var pparams = m_sfx.get_positional_params(strip.speaker);
    if (pparams) {
        strip.params.push(["DIST_REF", pparams["dist_ref"], 0, 1000, 10000, false]);
        strip.params.push(["ATTENUATION", pparams["attenuation"], 0, 50, 1000, false]);
        strip.params.push(["DIST_MAX", pparams["dist_max"], 0, 10000, 10000, false]);
    }

    var fparams = m_sfx.get_filter_params(strip.speaker);
    if (fparams) {
        strip.params.push(["EQ_FREQ", fparams["freq"], 20, 20000, 100, true]);
        strip.params.push(["EQ_Q", fparams["Q"], 0, 10, 100, false]);
        strip.params.push(["EQ_GAIN", fparams["gain"], -70, 30, 100, false]);
    }

    strip.params.push(["VOLUME", m_sfx.get_volume(strip.speaker), 0, 1, 50, false]);

    // handle params decrease
    m_util.clamp(strip.active_param, 0, strip.params.length - 1);
}

function switch_param(dir) {
    var strip = _mixer_strips[_active_strip];
    if (!strip)
        return;

    if (dir == 1 && strip.active_param < (strip.params.length - 1)) {
        strip.active_param++;
    } else if (dir == -1 && strip.active_param > 0) {
        strip.active_param--;
    }
}

function param_inc_dec(dir) {
    var strip = _mixer_strips[_active_strip];
    if (!strip)
        return;

    var param = strip.params[strip.active_param];

    if (param[5])
        param[1] *= Math.pow(param[3] / param[2], dir / param[4]);
    else
        param[1] += dir * ((param[3] - param[2]) / param[4]);

    param[1] = m_util.clamp(param[1], param[2], param[3]);

    switch (param[0]) {
    case "VOLUME":
        if (strip.id != "MASTER")
            m_sfx.set_volume(strip.speaker, param[1]);
        else
            m_sfx.set_volume(null, param[1]);
        break;
    case "DIST_REF":
        var pparams = m_sfx.get_positional_params(strip.speaker);
        pparams["dist_ref"] = param[1];
        m_sfx.set_positional_params(strip.speaker, pparams);
        break;
    case "ATTENUATION":
        var pparams = m_sfx.get_positional_params(strip.speaker);
        pparams["attenuation"] = param[1];
        m_sfx.set_positional_params(strip.speaker, pparams);
        break;
    case "DIST_MAX":
        var pparams = m_sfx.get_positional_params(strip.speaker);
        pparams["dist_max"] = param[1];
        m_sfx.set_positional_params(strip.speaker, pparams);
        break;

    case "EQ_FREQ":
        var fparams = m_sfx.get_filter_params(strip.speaker);
        fparams["freq"] = param[1];
        m_sfx.set_filter_params(strip.speaker, fparams);
        break;
    case "EQ_Q":
        var fparams = m_sfx.get_filter_params(strip.speaker);
        fparams["Q"] = param[1];
        m_sfx.set_filter_params(strip.speaker, fparams);
        break;
    case "EQ_GAIN":
        var fparams = m_sfx.get_filter_params(strip.speaker);
        fparams["gain"] = param[1];
        m_sfx.set_filter_params(strip.speaker, fparams);
        break;

    case "THRESHOLD":
        var cparams = m_sfx.get_compressor_params();
        cparams["threshold"] = param[1];
        m_sfx.set_compressor_params(cparams);
        break;
    case "KNEE":
        var cparams = m_sfx.get_compressor_params();
        cparams["knee"] = param[1];
        m_sfx.set_compressor_params(cparams);
        break;
    case "RATIO":
        var cparams = m_sfx.get_compressor_params();
        cparams["ratio"] = param[1];
        m_sfx.set_compressor_params(cparams);
        break;
    case "ATTACK":
        var cparams = m_sfx.get_compressor_params();
        cparams["attack"] = param[1];
        m_sfx.set_compressor_params(cparams);
        break;
    case "RELEASE":
        var cparams = m_sfx.get_compressor_params();
        cparams["release"] = param[1];
        m_sfx.set_compressor_params(cparams);
        break;
    default:
        m_util.panic("Unknown strip param");
        break;
    }
}

function switch_mute() {
    var strip = _mixer_strips[_active_strip];
    if (!strip)
        return;

    if (strip.mute >= 0) {
        flip_strip_mute(strip);
    }
}

function flip_strip_mute(strip) {
    var id = strip.id;

    if (id != "MASTER") {
        if (strip.mute == 0) {
            strip.mute = 1;

            m_sfx.mute(strip.speaker, true);

            if (strip.solo == 1) {
                strip.solo = 0;

                if (!is_other_solo(strip))
                    unmute_other(strip);
            }
        } else {
            strip.mute = 0;

            if (!is_other_solo(strip))
                m_sfx.mute(strip.speaker, false);
        }
    } else {
        if (strip.mute == 0) {
            strip.mute = 1;
            m_sfx.mute(null, true);
        } else {
            strip.mute = 0;
            m_sfx.mute(null, false);
        }
    }
}

function is_other_solo(strip) {
    for (var i = 0; i < _mixer_strips.length; i++) {
        var strip_i = _mixer_strips[i];

        if (strip_i != strip && strip_i.solo == 1)
            return true;
    }
    return false;
}

function mute_other(strip) {
    for (var i = 0; i < _mixer_strips.length; i++) {
        var strip_i = _mixer_strips[i];

        if (strip_i != strip && strip_i.mute == 0 && strip_i.solo == 0)
            m_sfx.mute(strip_i.speaker, true);
    }
}

function unmute_other(strip) {
    for (var i = 0; i < _mixer_strips.length; i++) {
        var strip_i = _mixer_strips[i];

        if (strip_i != strip && strip_i.mute == 0 && strip_i.solo == 0)
            m_sfx.mute(strip_i.speaker, false);
    }
}

function switch_solo() {
    var strip = _mixer_strips[_active_strip];
    if (!strip)
        return;

    if (strip.solo >= 0) {
        flip_strip_solo(strip);
    }
}

function flip_strip_solo(strip) {

    if (strip.solo == 0) {
        strip.solo = 1;

        m_sfx.mute(strip.speaker, false);

        // flip muted current
        if (strip.mute == 1)
            strip.mute = 0;

        mute_other(strip);

    } else {
        strip.solo = 0;

        if (is_other_solo(strip))
            m_sfx.mute(strip.speaker, true);
        else
            unmute_other(strip);
    }
}

function draw() {
    if (!_mixer_strips[_active_strip])
        return;

    var strip_range = active_strip_range();

    for (var i = strip_range[0]; i <= strip_range[1]; i++) {

        var strip = _mixer_strips[i];

        m_hud.draw_mixer_strip(strip.id, i == _active_strip, i % 8,
                strip.params, strip.active_param, strip.mute, strip.solo);

        if (strip.speaker && m_sfx.get_filter_params(strip.speaker)) {
            m_sfx.get_filter_freq_response(strip.speaker, _filter_freq_arr,
                    _filter_mag_arr, _filter_phase_arr);

            for (var j = 0; j < _filter_mag_arr.length; j++) {
                var mag = _filter_mag_arr[j];
                // log10()
                _filter_mag_arr[j] = 20 * Math.log(mag) / Math.LN10;
            }

            m_hud.plot_array("EQ", i % 8, _filter_mag_arr, 20, 20000, -10, 10);
        }
    }
}

function active_strip_range() {

    if (_mixer_strips.length == 0)
        return [0, -1];

    var strip_low = Math.floor(_active_strip / 8) * 8;
    var strip_high = strip_low + 7;

    strip_low = m_util.clamp(strip_low, 0, _mixer_strips.length-1);
    strip_high = m_util.clamp(strip_high, 0, _mixer_strips.length-1);

    return [strip_low, strip_high];
}


}