changeset 1430:e3ff8b7d6538

Automatic Loudness normalisation to -23 LUFS
author Nicholas Jillings <nickjillings@users.noreply.github.com>
date Wed, 16 Dec 2015 12:15:18 +0000
parents c06930d14ff7
children 77edd1b2329b
files core.js loudness.js
diffstat 2 files changed, 63 insertions(+), 51 deletions(-) [+]
line wrap: on
line diff
--- a/core.js	Tue Dec 15 16:02:17 2015 +0000
+++ b/core.js	Wed Dec 16 12:15:18 2015 +0000
@@ -19,6 +19,10 @@
 
 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
 AudioBufferSourceNode.prototype.owner = undefined;
+// Add a prototype to the bufferNode to hold the desired LINEAR gain
+AudioBuffer.prototype.gain = undefined;
+// Add a prototype to the bufferNode to hold the computed LUFS loudness
+AudioBuffer.prototype.lufs = undefined;
 
 window.onload = function() {
 	// Function called once the browser has loaded all files.
@@ -113,7 +117,8 @@
 			}
 			if (buffer == null)
 			{
-				buffer = new audioEngineContext.bufferObj(URL);
+				buffer = new audioEngineContext.bufferObj();
+				buffer.getMedia(URL);
 				audioEngineContext.buffers.push(buffer);
 			}
 		});
@@ -757,45 +762,49 @@
 	this.audioObjects = [];
 	
 	this.buffers = [];
-	this.bufferObj = function(url)
+	this.bufferObj = function()
 	{
-		this.url = url;
+		this.url = null;
 		this.buffer = null;
 		this.xmlRequest = new XMLHttpRequest();
 		this.users = [];
-		this.xmlRequest.open('GET',this.url,true);
-		this.xmlRequest.responseType = 'arraybuffer';
-		
-		var bufferObj = this;
-		
-		// Create callback to decode the data asynchronously
-		this.xmlRequest.onloadend = function() {
-			audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
-				bufferObj.buffer = decodedData;
-				for (var i=0; i<bufferObj.users.length; i++)
-				{
-					bufferObj.users[i].state = 1;
-					if (bufferObj.users[i].interfaceDOM != null)
+		this.getMedia = function(url) {
+			this.url = url;
+			this.xmlRequest.open('GET',this.url,true);
+			this.xmlRequest.responseType = 'arraybuffer';
+			
+			var bufferObj = this;
+			
+			// Create callback to decode the data asynchronously
+			this.xmlRequest.onloadend = function() {
+				audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
+					bufferObj.buffer = decodedData;
+					for (var i=0; i<bufferObj.users.length; i++)
 					{
-						bufferObj.users[i].interfaceDOM.enable();
+						bufferObj.users[i].state = 1;
+						if (bufferObj.users[i].interfaceDOM != null)
+						{
+							bufferObj.users[i].interfaceDOM.enable();
+						}
 					}
-				}
-			}, function(){
-				// Should only be called if there was an error, but sometimes gets called continuously
-				// Check here if the error is genuine
-				if (bufferObj.buffer == undefined) {
-					// Genuine error
-					console.log('FATAL - Error loading buffer on '+audioObj.id);
-					if (request.status == 404)
-					{
-						console.log('FATAL - Fragment '+audioObj.id+' 404 error');
-						console.log('URL: '+audioObj.url);
-						errorSessionDump('Fragment '+audioObj.id+' 404 error');
+					calculateLoudness(bufferObj.buffer,"I");
+				}, function(){
+					// Should only be called if there was an error, but sometimes gets called continuously
+					// Check here if the error is genuine
+					if (bufferObj.buffer == undefined) {
+						// Genuine error
+						console.log('FATAL - Error loading buffer on '+audioObj.id);
+						if (request.status == 404)
+						{
+							console.log('FATAL - Fragment '+audioObj.id+' 404 error');
+							console.log('URL: '+audioObj.url);
+							errorSessionDump('Fragment '+audioObj.id+' 404 error');
+						}
 					}
-				}
-			});
+				});
+			};
+			this.xmlRequest.send();
 		};
-		this.xmlRequest.send();
 	};
 	
 	this.play = function(id) {
@@ -838,7 +847,7 @@
 						this.audioObjects[i].outputGain.gain.value = 0.0;
 						this.audioObjects[i].stop();
 					} else if (i == id) {
-						this.audioObjects[id].outputGain.gain.value = this.audioObjects[id].specification.gain;
+						this.audioObjects[id].outputGain.gain.value = this.audioObjects[id].specification.gain*this.audioObjects[id].buffer.buffer.gain;
 						this.audioObjects[id].play(audioContext.currentTime+0.01);
 					}
 				}
@@ -881,7 +890,8 @@
 		if (buffer == null)
 		{
 			console.log("[WARN]: Buffer was not loaded in pre-test! "+URL);
-			buffer = new this.bufferObj(URL);
+			buffer = new this.bufferObj();
+			buffer.getMedia(URL);
 			this.buffers.push(buffer);
 		}
 		this.audioObjects[audioObjectId].specification = element;
@@ -931,24 +941,17 @@
 	this.setSynchronousLoop = function() {
 		// Pads the signals so they are all exactly the same length
 		var length = 0;
-		var lens = [];
 		var maxId;
 		for (var i=0; i<this.audioObjects.length; i++)
 		{
-			lens.push(this.audioObjects[i].buffer.buffer.length);
 			if (length < this.audioObjects[i].buffer.buffer.length)
 			{
 				length = this.audioObjects[i].buffer.buffer.length;
 				maxId = i;
 			}
 		}
-		// Perform difference
-		for (var i=0; i<lens.length; i++)
-		{
-			lens[i] = length - lens[i];
-		}
 		// Extract the audio and zero-pad
-		for (var i=0; i<lens.length; i++)
+		for (var i=0; i<this.audioObjects.length; i++)
 		{
 			var orig = this.audioObjects[i].buffer.buffer;
 			var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate);
@@ -959,8 +962,9 @@
 				for (var n=0; n<orig.length; n++)
 				{inData[n] = outData[n];}
 			}
+			hold.gain = orig.gain;
+			hold.lufs = orig.lufs;
 			this.audioObjects[i].buffer.buffer = hold;
-			delete orig;
 		}
 	};
 	
@@ -994,7 +998,7 @@
 	this.buffer;
     
 	this.loopStart = function() {
-		this.outputGain.gain.value = 1.0;
+		this.outputGain.gain.value =  this.specification.gain*this.buffer.buffer.gain;
 		this.metric.startListening(audioEngineContext.timer.getTestTime());
 	};
 	
--- a/loudness.js	Tue Dec 15 16:02:17 2015 +0000
+++ b/loudness.js	Wed Dec 16 12:15:18 2015 +0000
@@ -6,15 +6,17 @@
  *  multiple objects
  */
 
-function getLoudness(buffer, result, timescale, offlineContext)
+var interval_cal_loudness_event = null;
+
+function calculateLoudness(buffer, timescale, target, offlineContext)
 {
-	// This function returns the EBU R 128 specification loudness model
+	// 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.
 	
-	// Create the required filters
-	if (buffer == undefined || result == undefined)
+	if (buffer == undefined)
 	{
 		return 0;
 	}
@@ -22,10 +24,15 @@
 	{
 		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;
@@ -45,7 +52,6 @@
 	processSource.start();
 	offlineContext.startRendering().then(function(renderedBuffer) {
 		// Have the renderedBuffer information, now continue processing
-		console.log(renderedBuffer);
 		switch(timescale)
 		{
 		case "I":
@@ -87,11 +93,13 @@
 				}
 			}
 			var overallRelLoudness = calculateOverallLoudnessFromChannelBlocks(relgateEnergy);
-			result[0] =  overallRelLoudness;
+			buffer.lufs =  overallRelLoudness;
+			var diff = -23 -overallRelLoudness;
+			buffer.gain = decibelToLinear(diff);
 		}
 	}).catch(function(err) {
 		console.log(err);
-		result[0] = 1;
+		buffer.lufs = 1;
 	});
 }