/*
* 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 { Chart } from './chart.js';
import { Graph } from './graph.js';
import { ChartHandle } from './charthandle.js';
import { addClass } from '../utils/dom.js';
import { defineChildWidget } from '../child_widget.js';
import { sprintf } from '../utils/sprintf.js';
import { defineRecalculation } from '../define_recalculation.js';
import { defineMeasure, defineRender } from '../renderer.js';
function setInputMode() {
const O = this.options;
let mode = 'circular';
if (O.delay === false) mode = 'line-horizontal';
if (O.input === false) mode = 'line-vertical';
this.set('input_handle.mode', mode);
this.input.set(
'visible',
O.show_input && O.delay !== false && O.input !== false
);
}
function initValues(type, O) {
this.set(type, O[type]);
this.set(type + '_min', O[type + '_min']);
this.set(type + '_max', O[type + '_max']);
}
function setReflections(reflections) {
let R = [];
if (Array.isArray(reflections)) {
// reflections already is an array
R = reflections;
} else if (reflections) {
reflections = Object.assign(
{},
this.getDefault('reflections'),
reflections
);
// build reflections array from options object
for (let i = 0, m = reflections.amount; i < m; ++i) {
if (i) {
R.push({
time: reflections.spread * Math.random(),
level: -reflections.randomness * Math.random(),
});
} else {
R.push({
time: reflections.spread,
level: -reflections.randomness * Math.random(),
});
}
}
} else {
// no reflections given
R = [];
}
adjustReflections.call(this, R);
}
function adjustReflections(reflections) {
const O = this.options;
const R = O._reflections;
for (let i = reflections.length, m = R.length; i < m; ++i) {
const G = R[i].graph;
this.removeGraph(G);
G.destroyAndRemove();
}
R.length = reflections.length;
for (let i = 0, m = R.length; i < m; ++i) {
if (typeof R[i] !== 'object') {
R[i] = {
level: 0,
time: 0,
graph: null,
};
}
if (!R[i].graph) {
R[i].graph = new Graph({
range_x: this.range_x,
range_y: this.range_y,
class: 'aux-reflection',
});
this.addGraph(R[i].graph);
}
R[i].level = reflections[i].level;
R[i].time = reflections[i].time;
}
this.invalidate('_reflections');
}
/**
* Reverb is a {@link Chart} with various handles to set and display
* parameters of a typical classic reverb.
*
* @param {Object} [options={ }] - An object containing initial options.
*
* @property {Number} [options.timeframe=10000] - An alias for `range_x.max`, defining the maximum time of the chart.
* @property {Number} [options.delay=0] - The initial delay of the input signal, not to be confused with predelay.
* @property {Number} [options.delay_min=0] - The minimum delay.
* @property {Number} [options.delay_max=2000] - The maximum delay.
* @property {Number} [options.gain=0] - The gain for the input signal.
* @property {Number} [options.gain_min=-60] - The minimum gain.
* @property {Number} [options.gain_max=0] - The maximum gain.
* @property {Number} [options.predelay=0] - The predelay of the diffuse reverb.
* @property {Number} [options.predelay_min=0] - The minimum predelay.
* @property {Number} [options.predelay_max=2000] - The maximum predelay.
* @property {Number} [options.rlevel=0] - The level of the diffuse reverb.
* @property {Number} [options.rlevel_min=-60] - The minimum reverb level.
* @property {Number} [options.rlevel_max=0] - The maximum reverb level.
* @property {Number} [options.rtime=0] - The duration of the diffuse reverb. This acts in conjunction with the `reference` option.
* @property {Number} [options.rtime_min=0] - The minimum reverb time.
* @property {Number} [options.rtime_max=5000] - The maximum reverb time.
* @property {Number} [options.erlevel=0] - The level of the early reflections.
* @property {Number} [options.erlevel_min=-60] - The minimum level of early reflections.
* @property {Number} [options.erlevel_max=0] - The maximum level of early reflections.
* @property {Number} [options.attack=0] - The attack time for the diffuse reverb.
* @property {Number} [options.noisefloor=-96] - The noisefloor at which attack starts from.
* @property {Number} [options.reference=-60] - The reference level for calculating the reverb time.
* @property {Boolean} [options.show_input=true] - Draw the line showing the input signal.
* @property {Boolean} [options.show_input_handle=true] - Show the handle defining input level and initial delay.
* @property {Boolean} [options.show_rlevel_handle=true] - Show the handle defining reverb level and predelay.
* @property {Boolean} [options.show_rtime_handle=true] - Show the handle defining the reverb time.
* @property {Array|Object|Boolean} [options.reflections={amount: 0, spread: 0, randomness: 4}] - Defines reflections
* to be displayed. Either an array of objects `{time: n, level:n}` where time is in milliseconds,
* level in decibel or an object `{amount: n, spread: n, randomness: n}` where spread is a time
* in milliseconds to spread the reflections, randomness in decibels to randomize the levels and
* amount the number of reflections to be created. `false` disables drawing of the reflections.
* @extends Chart
*
* @class Reverb
*/
export class Reverb extends Chart {
static get _options() {
return {
timeframe: 'number',
delay: 'number',
delay_min: 'number',
delay_max: 'number',
gain: 'number',
gain_min: 'number',
gain_max: 'number',
predelay: 'number',
predelay_min: 'number',
predelay_max: 'number',
rlevel: 'number',
rlevel_min: 'number',
rlevel_max: 'number',
rtime: 'number',
rtime_min: 'number',
rtime_max: 'number',
erlevel: 'number',
erlevel_min: 'number',
erlevel_max: 'number',
attack: 'number',
noisefloor: 'number',
reference: 'number',
show_input: 'boolean',
reflections: 'boolean|array|object',
_reflections: 'array',
};
}
static get options() {
return {
range_x: { min: 0, max: 5000 },
range_y: { min: -90, max: 10 },
range_z: { min: 1, max: 1 },
grid_x: [
{ pos: 0, label: '0ms' },
{ pos: 500, label: '500ms' },
{ pos: 1000, label: '1s' },
{ pos: 1500, label: '1.5s' },
{ pos: 2000, label: '2s' },
{ pos: 2500, label: '2.5s' },
{ pos: 3000, label: '3s' },
{ pos: 3500, label: '3.5s' },
{ pos: 4000, label: '4s' },
{ pos: 4500, label: '4.5s' },
{ pos: 5000, label: '5s' },
{ pos: 5500, label: '5.5s' },
{ pos: 6000, label: '6s' },
{ pos: 6500, label: '6.5s' },
{ pos: 7000, label: '7s' },
{ pos: 7500, label: '7.5s' },
{ pos: 8000, label: '8s' },
{ pos: 8500, label: '8.5s' },
{ pos: 9000, label: '9s' },
{ pos: 9500, label: '9.5s' },
{ pos: 10000, label: '10s' },
],
grid_y: [
{ pos: -120, label: '-120dB' },
{ pos: -110, label: '-110dB' },
{ pos: -100, label: '-100dB' },
{ pos: -90, label: '-90dB' },
{ pos: -80, label: '-80dB' },
{ pos: -70, label: '-70dB' },
{ pos: -60, label: '-60dB' },
{ pos: -50, label: '-50dB' },
{ pos: -40, label: '-40dB' },
{ pos: -30, label: '-30dB' },
{ pos: -20, label: '-20dB' },
{ pos: -10, label: '-10dB' },
{ pos: 0, label: '0dB' },
],
timeframe: 10000,
delay: 0,
delay_min: 0,
delay_max: 2000,
gain: 0,
gain_min: -60,
gain_max: 0,
predelay: 0,
predelay_min: 0,
predelay_max: 2000,
rlevel: 0,
rlevel_min: -60,
rlevel_max: 0,
rtime: 0,
rtime_min: 0,
rtime_max: 5000,
erlevel: 0,
erlevel_min: -60,
erlevel_max: 0,
attack: 0,
noisefloor: -96,
reference: -60,
show_predelay_handle: true,
show_input: true,
show_input_handle: true,
show_rtime_handle: true,
show_rlevel_handle: true,
reflections: { amount: 0, spread: 0, randomness: 0 },
_reflections: [],
role: 'application',
};
}
static get static_events() {
return {
set_timeframe: (v) => this.range_x.set('max', v),
set_reflections: setReflections,
set_delay: function (v) {
setInputMode.call(this);
},
set_gain: function (v) {
setInputMode.call(this);
},
set_predelay: function (v) {
setInputMode.call(this);
},
set_rlevel: function (v) {
setInputMode.call(this);
},
set_rtime: function (v) {
setInputMode.call(this);
},
};
}
static get renderers() {
return [
defineMeasure(
[
'delay',
'delay_min',
'delay_max',
'predelay',
'attack',
'noisefloor',
'rlevel',
'gain',
'rtime',
'reference',
'range_y',
],
function (
delay,
delay_min,
delay_max,
predelay,
attack,
noisefloor,
rlevel,
gain,
rtime,
reference,
range_y
) {
const rstart = delay + predelay;
let x0 = rstart;
attack = Math.min(attack, predelay);
range_y = this.range_y;
delay = Math.min(delay_max, Math.max(delay_min, delay));
if (attack) {
const rate = noisefloor / attack;
x0 -= range_y.get('min') / rate;
}
const y0 = range_y.get('min');
const x1 = rstart;
const y1 = rlevel + gain;
const rate = reference / rtime;
const x2 =
(range_y.get('min') - gain - rlevel) / rate + delay + predelay;
const y2 = range_y.get('min');
this.reverb.set('dots', [
{ x: x0, y: y0 },
{ x: x1, y: y1 },
{ x: x2, y: y2 },
]);
}
),
defineRender('show_input', function (show_input) {
this.input.set('visible', show_input);
}),
defineMeasure(
['_reflections', 'range_y', 'delay', 'gain', 'erlevel'],
function (_reflections, range_y, delay, gain, erlevel) {
range_y = this.range_y;
_reflections.forEach((reflection) => {
const y = range_y.get('min');
const x = reflection.time + delay;
reflection.graph.set('dots', [
{ x: x, y: y },
{ x: x, y: reflection.level + gain + erlevel },
]);
});
}
),
];
}
initialize(options) {
super.initialize(options);
/**
* @member {Graph} Reverb#input - The {@link Graph} displaying the
* input signal as a vertical bar.
*/
this.input = new Graph({
range_x: this.range_x,
range_y: this.range_y,
class: 'aux-input',
});
this.addGraph(this.input);
/**
* @member {Graph} Reverb#reverb - The {@link Graph} displaying the
* reverb signal as a triagle.
*/
this.reverb = new Graph({
range_x: this.range_x,
range_y: this.range_y,
class: 'aux-reverb',
mode: 'bottom',
});
this.addGraph(this.reverb);
}
draw(O, element) {
addClass(element, 'aux-reverb');
super.draw(O, element);
initValues.call(this, 'delay', O);
initValues.call(this, 'gain', O);
initValues.call(this, 'predelay', O);
initValues.call(this, 'rlevel', O);
initValues.call(this, 'rtime', O);
initValues.call(this, 'erlevel', O);
this.set('reflections', O.reflections);
this.set('show_input', O.show_input);
}
}
function onInteractingChanged(value) {
if (value) {
this.parent.startInteracting();
} else {
this.parent.stopInteracting();
}
}
/**
* @member {ChartHandle} Reverb#input_handle - The {@link ChartHandle}
* displaying/setting the initial delay and gain.
*/
defineChildWidget(Reverb, 'input_handle', {
create: ChartHandle,
show: true,
default_options: {
format_label: function (label, x, y, z) {
const O = this.options;
const output = [];
if (label) output.push(label);
if (O.delay !== false) {
if (x >= 1000) output.push(sprintf('%.2fs', x / 1000));
else output.push(sprintf('%dms', x));
}
if (O.input !== false) {
output.push(sprintf('%.2fdB', y));
}
return output.join('\n');
},
label: 'Input',
mode: 'circular',
active: true,
},
static_events: {
set_interacting: onInteractingChanged,
userset: function (key, value) {
if (key === 'x') {
this.parent.userset('delay', value);
return false;
}
if (key === 'y') {
this.parent.userset('gain', value);
return false;
}
},
},
});
/**
* @member {ChartHandle} Reverb#rlevel_handle - The {@link ChartHandle}
* displaying/setting the pre delay and reverb level.
*/
defineChildWidget(Reverb, 'rlevel_handle', {
create: ChartHandle,
show: true,
default_options: {
format_label: function (label, x, y, z) {
const O = this.parent.options;
const output = [];
if (label) output.push(label);
if (O.delay !== false) {
if (x >= 1000) output.push(sprintf('%.2fs', (x - O.delay) / 1000));
else output.push(sprintf('%dms', x - O.delay));
}
if (O.rlevel !== false) {
output.push(sprintf('%.2fdB', y - O.gain));
}
return output.join('\n');
},
label: 'Reverb',
mode: 'circular',
active: true,
},
static_events: {
set_interacting: onInteractingChanged,
userset: function (key, value) {
const O = this.parent.options;
if (key === 'x') {
this.parent.userset('predelay', value - O.delay);
return false;
}
if (key === 'y') {
this.parent.userset('rlevel', value - O.gain);
return false;
}
},
},
});
/**
* @member {ChartHandle} Reverb#rtime_handle - The {@link ChartHandle}
* displaying/setting the reverb time.
*/
defineChildWidget(Reverb, 'rtime_handle', {
create: ChartHandle,
show: true,
default_options: {
format_label: function (label, x, y, z) {
const O = this.parent.options;
const output = [];
if (label) output.push(label);
if (O.delay !== false) {
if (x >= 1000)
output.push(sprintf('%.2fs', (x - O.delay - O.predelay) / 1000));
else output.push(sprintf('%dms', x - O.delay - O.predelay));
}
return output.join('\n');
},
label: 'Time',
mode: 'line-vertical',
active: true,
},
static_events: {
set_interacting: onInteractingChanged,
userset: function (key, value) {
const O = this.parent.options;
if (key === 'x') {
this.parent.userset('rtime', value - O.delay - O.predelay);
return false;
}
},
},
});
function clip(min, max, value) {
if (!(value >= min)) return min;
if (!(value <= max)) return max;
return value;
}
function defineClipCalculation(name) {
defineRecalculation(Reverb, [name + '_min', name + '_max', name], function (
O
) {
this.update(name, clip(O[name + '_min'], O[name + '_max'], O[name]));
});
}
defineClipCalculation('delay');
defineClipCalculation('predelay');
defineClipCalculation('rtime');
defineClipCalculation('gain');
defineClipCalculation('rlevel');
defineRecalculation(Reverb, ['delay', 'predelay', 'rtime'], function (O) {
const { delay, predelay, rtime } = O;
this.update('input_handle.x', delay);
this.update('rlevel_handle.x', delay + predelay);
this.update('rtime_handle.x', delay + predelay + rtime);
});
defineRecalculation(Reverb, ['gain', 'rlevel'], function (O) {
const { gain, rlevel } = O;
this.update('input_handle.y', gain);
this.update('rlevel_handle.y', gain + rlevel);
});