Documentation/Source example
Example source

samples/complete/js/vu_meter.js

Referenced by:fb.GetAudioChunk
function Clamp(value, min, max) {
	if (value < min)
		return min;
	else if (value > max)
		return max;
	else
		return value;
}

function EnableMenuIf(condition) {
	return condition ? MF_STRING : MF_GRAYED;
}

function _vu_meter(x, y, w, h) {
	this.clear_graph = function () {
		for (var c = 0; c < this.channels.count; ++c) {
			this.RMS_levels[c] = 0;
			this.Peak_levels[c] = 0;
			this.Peak_falldown[c] = 0;
		}

		window.Repaint();
	}

	this.init = function () {
		this.dBrange = this.maxDB - this.minDB;

		if (fb.IsPaused)
			this.update_graph();
		else if (fb.IsPlaying)
			this.start_timer();
	}

	this.start_timer = function () {
		if (!this.timer_id) {
			this.timer_id = window.SetInterval((function () { this.update_graph(); }).bind(this), this.timer_interval);
		}
	}

	this.stop_timer = function () {
		if (this.timer_id) {
			window.ClearInterval(this.timer_id);
		}

		this.timer_id = 0;
	}

	this.to_db = function (num) {
		return 20 * Math.log(num) / Math.LN10;
	}

	this.update_graph = function () {
		var cur_time = fb.PlaybackTime;

		if (cur_time < this.rms_window)
			return;

		var chunk = fb.GetAudioChunk(this.rms_window);

		if (!chunk)
			return;

		this.channels.count = chunk.ChannelCount;
		this.channels.config = chunk.ChannelConfig;
		var data = chunk.Data;
		var frame_len = chunk.SampleCount;

		if (data && this.channels.count > 0 && frame_len > 0) {
			var old_count = this.Peak_levels.length;
			this.RMS_levels.length = this.channels.count;
			this.Peak_levels.length = this.channels.count;
			this.Peak_falldown.length = this.channels.count;

			if (old_count < this.channels.count) {
				for (var c = old_count; c < this.channels.count; ++c) {
					this.Peak_levels[c] = 0;
					this.Peak_falldown[c] = 0;
				}
			}

			for (var c = 0; c < this.channels.count; ++c) {
				var sum = 0, peak = 0;

				for (var i = c; i < data.length; i += this.channels.count) {
					var s = Math.abs(data[i]);

					if (s > peak) {
						peak = s;
					}

					sum += s * s;
				}

				this.RMS_levels[c] = Math.sqrt(sum / frame_len);

				if (peak >= this.Peak_levels[c]) {
					this.Peak_levels[c] = peak;
					this.Peak_falldown[c] = 0;
				} else {
					if (++this.Peak_falldown[c] > this.peak_hold) {
						this.Peak_levels[c] *= this.peak_fall_mul;
					}
				}
			}

			window.Repaint();
		}
	}

// callbacks begin

	this.paint = function (gr) {
		if (this.w < 1 || this.h < 1)
			return;

		var smooth_mode = this.properties.meter_style.value == 0;

		if (this.w > this.h) { // horizontal

			var bar_width = this.w;
			var bar_height = Math.floor(this.h / this.channels.count);

			if (!smooth_mode) {
				var block_count = Math.max(Math.floor(this.dBrange / this.properties.rms_block_db.value), 1);
				var block_width = bar_width / block_count;
				var block_pad = Math.max(Math.ceil(block_width * 0.05), 1);
			}

			for (var c = 0; c < this.channels.count; ++c) {
				if (this.RMS_levels[c]) {
					var rms_db = Clamp(this.to_db(this.RMS_levels[c]), this.minDB, this.maxDB);

					if (smooth_mode) {
						var width = Math.round(bar_width * (rms_db - this.minDB) / this.dBrange);
						gr.FillSolidRect(this.x, this.y + (bar_height * c), width, bar_height - 1, this.properties.custom_bar.value);
					} else {
						var blocks = Math.round(block_count * (rms_db - this.minDB) / this.dBrange);
						var width = blocks * block_width;
						gr.FillSolidRect(this.x, this.y + (bar_height * c), width, bar_height - 1, this.properties.custom_bar.value);

						for (var i = 1; i <= blocks; ++i) {
							gr.FillSolidRect(this.x - Math.ceil(block_pad / 2) + (i * block_width), this.y + (bar_height * c), block_pad, bar_height - 1, this.properties.custom_background.value);
						}
					}
				}

				if (this.peak_bar_width > 0 && this.Peak_levels[c] > 0) {
					var peak_db = Clamp(this.to_db(this.Peak_levels[c]), this.minDB, this.maxDB);

					if (peak_db > this.minDB) {
						var peak_pos = Math.round(bar_width * (peak_db - this.minDB) / this.dBrange);
						gr.FillSolidRect(this.x + peak_pos - this.peak_bar_width / 2, this.y + (bar_height * c), this.peak_bar_width, bar_height - 1, this.properties.custom_peak.value);
					}
				}
			}
		} else { // vertical

			var bar_width = Math.floor(this.w / this.channels.count);
			var bar_height = this.h;

			if (!smooth_mode) {
				var block_count = Math.max(Math.floor(this.dBrange / this.properties.rms_block_db.value), 1);
				var block_height = bar_height / block_count;
				var block_pad = Math.max(Math.ceil(block_height * 0.05), 1);
			}

			for (var c = 0; c < this.channels.count; ++c) {
				if (this.RMS_levels[c]) {
					var rms_db = Clamp(this.to_db(this.RMS_levels[c]), this.minDB, this.maxDB);

					if (smooth_mode) {
						var height = Math.round(bar_height * (rms_db - this.minDB) / this.dBrange);
					} else {
						var blocks = Math.round(block_count * (rms_db - this.minDB) / this.dBrange);
						var height = blocks * block_height;
					}

					gr.FillSolidRect(this.x + (bar_width * c), this.y, bar_width - 1, this.h, this.properties.custom_bar.value);
					gr.FillSolidRect(this.x + (bar_width * c), this.y, bar_width - 1, this.h - height, this.properties.custom_background.value);

					if (!smooth_mode) {
						for (var i = 1; i <= blocks; ++i) {
							gr.FillSolidRect(bar_width * c, this.h - height + Math.ceil(block_pad / 2) + (i * block_height), bar_width - 1, block_pad, this.properties.custom_background.value);
						}
					}
				}

				if (this.peak_bar_width > 0 && this.Peak_levels[c] > 0) {
					var peak_db = Clamp(this.to_db(this.Peak_levels[c]), this.minDB, this.maxDB);

					if (peak_db > this.minDB) {
						var peak_pos = this.h - Math.round(bar_height * (peak_db - this.minDB) / this.dBrange);
						gr.FillSolidRect(this.x + (bar_width * c), this.y + peak_pos, bar_width - 1, this.peak_bar_width, this.properties.custom_peak.value);
					}
				}
			}
		}
	}

	this.playback_new_track = function () {
		this.start_timer();
	}

	this.playback_pause = function (state) {
		state ? this.stop_timer() : this.start_timer();
	}

	this.playback_stop = function (reason) {
		if (reason != 2) {
			this.stop_timer();
		}

		this.clear_graph();
	}

	this.rbtn_up = function (x, y) {
		var menu = window.CreatePopupMenu();
		var style_menu = window.CreatePopupMenu();

		menu.AppendMenuItem(MF_STRING, 3, 'Background...');
		menu.AppendMenuItem(MF_STRING, 4, 'Bar...');
		menu.AppendMenuItem(EnableMenuIf(this.peak_bar_width > 0), 5, 'Peak...');

		style_menu.AppendMenuItem(MF_STRING, 10, 'Smooth');
		style_menu.AppendMenuItem(MF_STRING, 11, 'Blocks');
		style_menu.CheckMenuRadioItem(10, 11, this.properties.meter_style.value + 10);

		if (this.properties.meter_style.value == 1) {
			style_menu.AppendMenuSeparator();
			style_menu.AppendMenuItem(MF_GRAYED, 0, 'Block width (dB)');

			this.rms_block_dbs.forEach(function (item, index) {
				style_menu.AppendMenuItem(MF_STRING, 20 + index, item);
			});

			var rms_block_db_index = this.rms_block_dbs.indexOf(this.properties.rms_block_db.value);
			style_menu.CheckMenuRadioItem(20, 20 + this.rms_block_dbs.length, 20 + rms_block_db_index);
		}

		style_menu.AppendTo(menu, MF_STRING, 'Meter style');

		menu.AppendMenuSeparator();
		menu.AppendMenuItem(MF_STRING, 50, 'Configure...');

		var idx = menu.TrackPopupMenu(x, y);

		switch (idx) {
		case 0:
			break;
		case 3:
			var tmp = utils.ColourPicker(window.ID, this.properties.custom_background.value);

			if (tmp != this.properties.custom_background.value) {
				this.properties.custom_background.value = tmp;
				window.Repaint();
			}
			break;
		case 4:
			var tmp = utils.ColourPicker(window.ID, this.properties.custom_bar.value);

			if (tmp != this.properties.custom_bar.value) {
				this.properties.custom_bar.value = tmp;
				window.Repaint();
			}
			break;
		case 5:
			var tmp = utils.ColourPicker(window.ID, this.properties.custom_peak.value);

			if (tmp != this.properties.custom_peak.value) {
				this.properties.custom_peak.value = tmp;
				window.Repaint();
			}
			break;
		case 10:
		case 11:
			this.properties.meter_style.value = idx - 10;
			window.Repaint();
			break;
		case 20:
		case 21:
		case 22:
		case 23:
			this.properties.rms_block_db.value = this.rms_block_dbs[idx - 20];
			window.Repaint();
			break;
		case 50:
			window.ShowConfigure();
			break;
		}

		return true;
	}
// callbacks end


	this.x = x;
	this.y = y;
	this.w = w;
	this.h = h;

	this.minDB = -60; // minimum dB on the meter (meter range)

	this.maxDB = 5; // maximum dB on the meter (meter range)


	this.rms_block_dbs = [0.3125, 0.625, 1.25, 2.5];
	this.RMS_levels = [];
	this.Peak_levels = [];
	this.Peak_falldown = [];
	this.timer_id = 0;
	this.dBrange = 0;

	this.channels = {
		count : 2,
		config : 0,
	};

	this.properties = {
		custom_background : new _p("2K3.METER.BACKGROUND.COLOUR", _RGB(30, 30, 30)),
		custom_bar : new _p("2K3.METER.BAR.COLOUR", _RGB(200, 200, 200)),
		custom_peak : new _p("2K3.METER.PEAK.COLOUR", _RGB(255, 0, 0)), // no default

		meter_style : new _p("2K3.METER.STYLE", 1), // 0: smooth, 1: blocks

		rms_block_db : new _p("2K3.METER.BLOCK.DB", 0.625),
	};
}