view loudness.js @ 753:66d732c2bc14

Index page now links to example APE project, example MUSHRA project, test creator, analysis page, citing info, GNU license, and instructions. Instructions and example project contain info on checkboxes.
author Brecht De Man <BrechtDeMan@users.noreply.github.com>
date Fri, 18 Dec 2015 18:26:46 +0000
parents
children fab881bdeeac 43801b3d6131
line wrap: on
line source
/**
 *  loundess.js
 *  Loudness module for the Web Audio Evaluation Toolbox
 *  Allows for automatic calculation of loudness of Web Audio API Buffer objects,
 * 	return gain values to correct for a target loudness or match loudness between
 *  multiple objects
 */

var interval_cal_loudness_event = null;

function calculateLoudness(buffer, timescale, target, offlineContext)
{
	// This function returns the EBU R 128 specification loudness model and sets the linear gain required to match -23 LUFS
	// buffer -> Web Audio API Buffer object
	// timescale -> M or Momentary (returns Array), S or Short (returns Array),
	//   I or Integrated (default, returns number)
	// target -> default is -23 LUFS but can be any LUFS measurement.
	
	if (buffer == undefined)
	{
		return 0;
	}
	if (timescale == undefined)
	{
		timescale = "I";
	}
	if (target == undefined)
	{
		target = -23;
	}
	if (offlineContext == undefined)
	{
		offlineContext = new OfflineAudioContext(buffer.numberOfChannels, buffer.length, buffer.sampleRate);
	}
	// Create the required filters
	var KFilter = offlineContext.createBiquadFilter();
	KFilter.type = "highshelf";
	KFilter.gain.value = 4;
	KFilter.frequency.value = 1480;
	
	var HPFilter = offlineContext.createBiquadFilter();
	HPFilter.type = "highpass";
	HPFilter.Q.value = 0.707;
	HPFilter.frequency.value = 60;
	// copy Data into the process buffer
	var processSource = offlineContext.createBufferSource();
	processSource.buffer = buffer;
	
	processSource.connect(KFilter);
	KFilter.connect(HPFilter);
	HPFilter.connect(offlineContext.destination);
	processSource.start();
	offlineContext.startRendering().then(function(renderedBuffer) {
		// Have the renderedBuffer information, now continue processing
		switch(timescale)
		{
		case "I":
			var blockEnergy = calculateProcessedLoudness(renderedBuffer, 400, 0.75);
			// Apply the absolute gate
			var loudness = calculateLoudnessFromChannelBlocks(blockEnergy);
			var absgatedEnergy = new Array(blockEnergy.length);
			for (var c=0; c<blockEnergy.length; c++)
			{
				absgatedEnergy[c] = [];
			}
			for (var i=0; i<loudness.length; i++)
			{
				if (loudness[i] >= -70)
				{
					for (var c=0; c<blockEnergy.length; c++)
					{
						absgatedEnergy[c].push(blockEnergy[c][i]);
					}
				}
			}
			var overallAbsLoudness = calculateOverallLoudnessFromChannelBlocks(absgatedEnergy);
			
			//applying the relative gate 8 dB down from overallAbsLoudness
			var relGateLevel = overallAbsLoudness - 8;
			var relgateEnergy = new Array(blockEnergy.length);
			for (var c=0; c<blockEnergy.length; c++)
			{
				relgateEnergy[c] = [];
			}
			for (var i=0; i<loudness.length; i++)
			{
				if (loudness[i] >= relGateLevel)
				{
					for (var c=0; c<blockEnergy.length; c++)
					{
						relgateEnergy[c].push(blockEnergy[c][i]);
					}
				}
			}
			var overallRelLoudness = calculateOverallLoudnessFromChannelBlocks(relgateEnergy);
			buffer.lufs =  overallRelLoudness;
		}
	}).catch(function(err) {
		console.log(err);
		buffer.lufs = 1;
	});
}

function calculateProcessedLoudness(buffer, winDur, overlap)
{
	// Buffer		Web Audio buffer node
	// winDur		Window Duration in milliseconds
	// overlap		Window overlap as normalised (0.5 = 50% overlap);
	if (buffer == undefined)
	{
		return 0;
	}
	if (winDur == undefined)
	{
		winDur = 400;
	}
	if (overlap == undefined)
	{
		overlap = 0.5;
	}
	var winSize = buffer.sampleRate*winDur/1000;
	var olapSize = (1-overlap)*winSize;
	var numberOfFrames = Math.floor(buffer.length/olapSize - winSize/olapSize + 1);
	var blockEnergy = new Array(buffer.numberOfChannels);
	for (var channel = 0; channel < buffer.numberOfChannels; channel++)
	{
		blockEnergy[channel] = new Float32Array(numberOfFrames);
		var data = buffer.getChannelData(channel);
		for (var i=0; i<numberOfFrames; i++)
		{
			var sigma = 0;
			for (var n=i*olapSize; n < i*olapSize+winSize; n++)
			{
				sigma += Math.pow(data[n],2);
			}
			blockEnergy[channel][i] = sigma/winSize;
		}
	}
	return blockEnergy;
}
function calculateLoudnessFromChannelBlocks(blockEnergy)
{
	// Loudness
	var loudness = new Float32Array(blockEnergy[0].length);
	for (var i=0; i<blockEnergy[0].length; i++)
	{
		var sigma = 0;
		for (var channel = 0; channel < blockEnergy.length; channel++)
		{
			var G = 1.0;
			if (channel >= 4) {G = 1.41;}
			sigma += blockEnergy[channel][i]*G;
		}
		loudness[i] = -0.691 + 10*Math.log10(sigma);
	}
	return loudness;
}
function calculateOverallLoudnessFromChannelBlocks(blockEnergy)
{
	// Loudness
	var summation = 0;
	for (var channel = 0; channel < blockEnergy.length; channel++)
	{
		var G = 1.0;
		if (channel >= 4) {G = 1.41;}
		var sigma = 0;
		for (var i=0; i<blockEnergy[0].length; i++)
		{
			blockEnergy[channel][i] *= G;
			sigma += blockEnergy[channel][i];
		}
		sigma /= blockEnergy.length;
		summation+= sigma;
	}
	return -0.691 + 10*Math.log10(summation);;
}