diff core.js @ 444:9c9fd68693b1

Merge. Pull of revision info from dev_main.
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Wed, 23 Dec 2015 14:36:00 +0000
parents da368f23bcd3
children 63c4163fc705
line wrap: on
line diff
--- a/core.js	Mon Dec 21 23:18:43 2015 +0000
+++ b/core.js	Wed Dec 23 14:36:00 2015 +0000
@@ -16,10 +16,6 @@
 var audioEngineContext; // The custome AudioEngine object
 var projectReturn; // Hold the URL for the return
 
-
-// Add a prototype to the bufferSourceNode to reference to the audioObject holding it
-AudioBufferSourceNode.prototype.owner = undefined;
-
 window.onload = function() {
 	// Function called once the browser has loaded all files.
 	// This should perform any initial commands such as structure / loading documents
@@ -32,9 +28,6 @@
 	// Create test state
 	testState = new stateMachine();
 	
-	// Create the audio engine object
-	audioEngineContext = new AudioEngine();
-	
 	// Create the popup interface object
 	popup = new interfacePopup();
 	
@@ -43,8 +36,224 @@
 	
 	// Create the interface object
 	interfaceContext = new Interface(specification);
+	// Define window callbacks for interface
+	window.onresize = function(event){interfaceContext.resizeWindow(event);};
+	
+	// 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 = 1.0;
+	// Add a prototype to the bufferNode to hold the computed LUFS loudness
+	AudioBuffer.prototype.lufs = -23;
 };
 
+function loadProjectSpec(url) {
+	// Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
+	// If url is null, request client to upload project XML document
+	var r = new XMLHttpRequest();
+	r.open('GET',url,true);
+	r.onload = function() {
+		loadProjectSpecCallback(r.response);
+	};
+	r.send();
+};
+
+function loadProjectSpecCallback(response) {
+	// Function called after asynchronous download of XML project specification
+	//var decode = $.parseXML(response);
+	//projectXML = $(decode);
+	
+	var parse = new DOMParser();
+	projectXML = parse.parseFromString(response,'text/xml');
+	var errorNode = projectXML.getElementsByTagName('parsererror');
+	if (errorNode.length >= 1)
+	{
+		var msg = document.createElement("h3");
+		msg.textContent = "FATAL ERROR";
+		var span = document.createElement("span");
+		span.textContent = "The XML parser returned the following errors when decoding your XML file";
+		document.getElementsByTagName('body')[0].innerHTML = null;
+		document.getElementsByTagName('body')[0].appendChild(msg);
+		document.getElementsByTagName('body')[0].appendChild(span);
+		document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
+		return;
+	}
+	
+	// Build the specification
+	specification.decode(projectXML);
+	
+	// Detect the interface to use and load the relevant javascripts.
+	var interfaceJS = document.createElement('script');
+	interfaceJS.setAttribute("type","text/javascript");
+	if (specification.interfaceType == 'APE') {
+		interfaceJS.setAttribute("src","ape.js");
+		
+		// APE comes with a css file
+		var css = document.createElement('link');
+		css.rel = 'stylesheet';
+		css.type = 'text/css';
+		css.href = 'ape.css';
+		
+		document.getElementsByTagName("head")[0].appendChild(css);
+	} else if (specification.interfaceType == "MUSHRA")
+	{
+		interfaceJS.setAttribute("src","mushra.js");
+		
+		// MUSHRA comes with a css file
+		var css = document.createElement('link');
+		css.rel = 'stylesheet';
+		css.type = 'text/css';
+		css.href = 'mushra.css';
+		
+		document.getElementsByTagName("head")[0].appendChild(css);
+	}
+	document.getElementsByTagName("head")[0].appendChild(interfaceJS);
+	
+	// Create the audio engine object
+	audioEngineContext = new AudioEngine(specification);
+	
+	testState.stateMap.push(specification.preTest);
+	
+	$(specification.audioHolders).each(function(index,elem){
+		testState.stateMap.push(elem);
+		$(elem.audioElements).each(function(i,audioElem){
+			var URL = audioElem.parent.hostURL + audioElem.url;
+			var buffer = null;
+			for (var i=0; i<audioEngineContext.buffers.length; i++)
+			{
+				if (URL == audioEngineContext.buffers[i].url)
+				{
+					buffer = audioEngineContext.buffers[i];
+					break;
+				}
+			}
+			if (buffer == null)
+			{
+				buffer = new audioEngineContext.bufferObj();
+				buffer.getMedia(URL);
+				audioEngineContext.buffers.push(buffer);
+			}
+		});
+	});
+	
+	testState.stateMap.push(specification.postTest);
+}
+
+function createProjectSave(destURL) {
+	// Save the data from interface into XML and send to destURL
+	// If destURL is null then download XML in client
+	// Now time to render file locally
+	var xmlDoc = interfaceXMLSave();
+	var parent = document.createElement("div");
+	parent.appendChild(xmlDoc);
+	var file = [parent.innerHTML];
+	if (destURL == "null" || destURL == undefined) {
+		var bb = new Blob(file,{type : 'application/xml'});
+		var dnlk = window.URL.createObjectURL(bb);
+		var a = document.createElement("a");
+		a.hidden = '';
+		a.href = dnlk;
+		a.download = "save.xml";
+		a.textContent = "Save File";
+		
+		popup.showPopup();
+		popup.popupContent.innerHTML = null;
+		popup.popupContent.appendChild(a);
+	} else {
+		var xmlhttp = new XMLHttpRequest;
+		xmlhttp.open("POST",destURL,true);
+		xmlhttp.setRequestHeader('Content-Type', 'text/xml');
+		xmlhttp.onerror = function(){
+			console.log('Error saving file to server! Presenting download locally');
+			createProjectSave(null);
+		};
+		xmlhttp.onreadystatechange  = function() {
+			console.log(xmlhttp.status);
+			if (xmlhttp.status != 200 && xmlhttp.readyState == 4) {
+				createProjectSave(null);
+			} else {
+				if (xmlhttp.responseXML == null)
+				{
+					return createProjectSave(null);
+				}
+				var response = xmlhttp.responseXML.childNodes[0];
+				if (response.getAttribute('state') == "OK")
+				{
+					var file = response.getElementsByTagName('file')[0];
+					console.log('Save OK: Filename '+file.textContent+','+file.getAttribute('bytes')+'B');
+					popup.showPopup();
+					popup.popupContent.innerHTML = null;
+					popup.popupContent.textContent = "Thank you!";
+				} else {
+					var message = response.getElementsByTagName('message')[0];
+					errorSessionDump(message.textContent);
+				}
+			}
+		};
+		xmlhttp.send(file);
+	}
+}
+
+function errorSessionDump(msg){
+	// Create the partial interface XML save
+	// Include error node with message on why the dump occured
+	popup.showPopup();
+	popup.popupContent.innerHTML = null;
+	var err = document.createElement('error');
+	var parent = document.createElement("div");
+	if (typeof msg === "object")
+	{
+		err.appendChild(msg);
+		popup.popupContent.appendChild(msg);
+		
+	} else {
+		err.textContent = msg;
+		popup.popupContent.innerHTML = "ERROR : "+msg;
+	}
+	var xmlDoc = interfaceXMLSave();
+	xmlDoc.appendChild(err);
+	parent.appendChild(xmlDoc);
+	var file = [parent.innerHTML];
+	var bb = new Blob(file,{type : 'application/xml'});
+	var dnlk = window.URL.createObjectURL(bb);
+	var a = document.createElement("a");
+	a.hidden = '';
+	a.href = dnlk;
+	a.download = "save.xml";
+	a.textContent = "Save File";
+	
+	
+	
+	popup.popupContent.appendChild(a);
+}
+
+// Only other global function which must be defined in the interface class. Determines how to create the XML document.
+function interfaceXMLSave(){
+	// Create the XML string to be exported with results
+	var xmlDoc = document.createElement("BrowserEvaluationResult");
+	var projectDocument = specification.projectXML;
+	projectDocument.setAttribute('file-name',url);
+	xmlDoc.appendChild(projectDocument);
+	xmlDoc.appendChild(returnDateNode());
+	xmlDoc.appendChild(interfaceContext.returnNavigator());
+	for (var i=0; i<testState.stateResults.length; i++)
+	{
+		xmlDoc.appendChild(testState.stateResults[i]);
+	}
+	
+	return xmlDoc;
+}
+
+function linearToDecibel(gain)
+{
+	return 20.0*Math.log10(gain);
+}
+
+function decibelToLinear(gain)
+{
+	return Math.pow(10,gain/20.0);
+}
+
 function interfacePopup() {
 	// Creates an object to manage the popup
 	this.popup = null;
@@ -56,6 +265,14 @@
 	this.popupOptions = null;
 	this.currentIndex = null;
 	this.responses = null;
+	$(window).keypress(function(e){
+			if (e.keyCode == 13 && popup.popup.style.visibility == 'visible')
+			{
+				console.log(e);
+				popup.buttonProceed.onclick();
+				e.preventDefault();
+			}
+		});
 	
 	this.createPopup = function(){
 		// Create popup window interface
@@ -133,12 +350,6 @@
 		var blank = document.getElementsByClassName('testHalt')[0];
 		blank.style.zIndex = 2;
 		blank.style.visibility = 'visible';
-		$(window).keypress(function(e){
-			if (e.keyCode == 13 && popup.popup.style.visibility == 'visible')
-			{
-				popup.buttonProceed.onclick();
-			}
-		});
 	};
 	
 	this.hidePopup = function(){
@@ -148,7 +359,6 @@
 		blank.style.zIndex = -2;
 		blank.style.visibility = 'hidden';
 		this.buttonPrevious.style.visibility = 'inherit';
-		$(window).keypress(function(e){});
 	};
 	
 	this.postNode = function() {
@@ -306,12 +516,14 @@
 		} else if (node.type == "radio") {
 			var optHold = this.popupResponse;
 			var hold = document.createElement('radio');
+			console.log("Checkbox: "+ node.statement);
 			var responseID = null;
 			var i=0;
 			while(responseID == null) {
 				var input = optHold.childNodes[i].getElementsByTagName('input')[0];
 				if (input.checked == true) {
 					responseID = i;
+					console.log("Selected: "+ node.options[i].name);
 				}
 				i++;
 			}
@@ -397,6 +609,16 @@
 			}
 		}
 	};
+	
+	this.resize = function(event)
+	{
+		// Called on window resize;
+		this.popup.style.left = (window.innerWidth/2)-250 + 'px';
+		this.popup.style.top = (window.innerHeight/2)-125 + 'px';
+		var blank = document.getElementsByClassName('testHalt')[0];
+		blank.style.width = window.innerWidth;
+		blank.style.height = window.innerHeight;
+	};
 }
 
 function advanceState()
@@ -458,7 +680,7 @@
 				this.currentStateMap = this.stateMap[this.stateIndex];
 				if (this.currentStateMap.type == "audioHolder") {
 					console.log('Loading test page');
-					loadTest(this.currentStateMap);
+					interfaceContext.newPage(this.currentStateMap);
 					this.initialiseInnerState(this.currentStateMap);
 				} else if (this.currentStateMap.type == "pretest" || this.currentStateMap.type == "posttest") {
 					if (this.currentStateMap.options.length >= 1) {
@@ -477,8 +699,26 @@
 	
 	this.testPageCompleted = function(store, testXML, testId) {
 		// Function called each time a test page has been completed
-		// Can be used to over-rule default behaviour
-		
+		var metric = document.createElement('metric');
+		if (audioEngineContext.metric.enableTestTimer)
+		{
+			var testTime = document.createElement('metricResult');
+			testTime.id = 'testTime';
+			testTime.textContent = audioEngineContext.timer.testDuration;
+			metric.appendChild(testTime);
+		}
+		store.appendChild(metric);
+		var audioObjects = audioEngineContext.audioObjects;
+		for (var i=0; i<audioObjects.length; i++) 
+		{
+			var audioElement = audioEngineContext.audioObjects[i].exportXMLDOM();
+			audioElement.setAttribute('presentedId',i);
+			store.appendChild(audioElement);
+		}
+		$(interfaceContext.commentQuestions).each(function(index,element){
+			var node = element.exportXMLDOM();
+			store.appendChild(node);
+		});
 		pageXMLSave(store, testXML);
 	};
 	
@@ -518,216 +758,7 @@
 	this.previousState = function(){};
 }
 
-function testEnded(testId)
-{
-	pageXMLSave(testId);
-	if (testXMLSetups.length-1 > testId)
-	{
-		// Yes we have another test to perform
-		testId = (Number(testId)+1);
-		currentState = 'testRun-'+testId;
-		loadTest(testId);
-	} else {
-		console.log('Testing Completed!');
-		currentState = 'postTest';
-		// Check for any post tests
-		var xmlSetup = projectXML.find('setup');
-		var postTest = xmlSetup.find('PostTest')[0];
-		popup.initState(postTest);
-	}
-}
-
-function loadProjectSpec(url) {
-	// Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
-	// If url is null, request client to upload project XML document
-	var r = new XMLHttpRequest();
-	r.open('GET',url,true);
-	r.onload = function() {
-		loadProjectSpecCallback(r.response);
-	};
-	r.send();
-};
-
-function loadProjectSpecCallback(response) {
-	// Function called after asynchronous download of XML project specification
-	//var decode = $.parseXML(response);
-	//projectXML = $(decode);
-	
-	var parse = new DOMParser();
-	projectXML = parse.parseFromString(response,'text/xml');
-	
-	// Build the specification
-	specification.decode();
-	
-	testState.stateMap.push(specification.preTest);
-	
-	$(specification.audioHolders).each(function(index,elem){
-		testState.stateMap.push(elem);
-	});
-	 
-	 testState.stateMap.push(specification.postTest);
-	 
-	// Obtain the metrics enabled
-	$(specification.metrics).each(function(index,node){
-		var enabled = node.textContent;
-		switch(node.enabled)
-		{
-		case 'testTimer':
-			sessionMetrics.prototype.enableTestTimer = true;
-			break;
-		case 'elementTimer':
-			sessionMetrics.prototype.enableElementTimer = true;
-			break;
-		case 'elementTracker':
-			sessionMetrics.prototype.enableElementTracker = true;
-			break;
-		case 'elementListenTracker':
-			sessionMetrics.prototype.enableElementListenTracker = true;
-			break;
-		case 'elementInitialPosition':
-			sessionMetrics.prototype.enableElementInitialPosition = true;
-			break;
-		case 'elementFlagListenedTo':
-			sessionMetrics.prototype.enableFlagListenedTo = true;
-			break;
-		case 'elementFlagMoved':
-			sessionMetrics.prototype.enableFlagMoved = true;
-			break;
-		case 'elementFlagComments':
-			sessionMetrics.prototype.enableFlagComments = true;
-			break;
-		}
-	});
-	
-	
-	
-	// Detect the interface to use and load the relevant javascripts.
-	var interfaceJS = document.createElement('script');
-	interfaceJS.setAttribute("type","text/javascript");
-	if (specification.interfaceType == 'APE') {
-		interfaceJS.setAttribute("src","ape.js");
-		
-		// APE comes with a css file
-		var css = document.createElement('link');
-		css.rel = 'stylesheet';
-		css.type = 'text/css';
-		css.href = 'ape.css';
-		
-		document.getElementsByTagName("head")[0].appendChild(css);
-	} else if (specification.interfaceType == "MUSHRA")
-	{
-		interfaceJS.setAttribute("src","mushra.js");
-		
-		// MUSHRA comes with a css file
-		var css = document.createElement('link');
-		css.rel = 'stylesheet';
-		css.type = 'text/css';
-		css.href = 'mushra.css';
-		
-		document.getElementsByTagName("head")[0].appendChild(css);
-	}
-	document.getElementsByTagName("head")[0].appendChild(interfaceJS);
-	
-	// Define window callbacks for interface
-	window.onresize = function(event){interfaceContext.resizeWindow(event);};
-}
-
-function createProjectSave(destURL) {
-	// Save the data from interface into XML and send to destURL
-	// If destURL is null then download XML in client
-	// Now time to render file locally
-	var xmlDoc = interfaceXMLSave();
-	var parent = document.createElement("div");
-	parent.appendChild(xmlDoc);
-	var file = [parent.innerHTML];
-	if (destURL == "null" || destURL == undefined) {
-		var bb = new Blob(file,{type : 'application/xml'});
-		var dnlk = window.URL.createObjectURL(bb);
-		var a = document.createElement("a");
-		a.hidden = '';
-		a.href = dnlk;
-		a.download = "save.xml";
-		a.textContent = "Save File";
-		
-		popup.showPopup();
-		popup.popupContent.innerHTML = null;
-		popup.popupContent.appendChild(a);
-	} else {
-		var xmlhttp = new XMLHttpRequest;
-		xmlhttp.open("POST",destURL,true);
-		xmlhttp.setRequestHeader('Content-Type', 'text/xml');
-		xmlhttp.onerror = function(){
-			console.log('Error saving file to server! Presenting download locally');
-			createProjectSave(null);
-		};
-		xmlhttp.onreadystatechange  = function() {
-			console.log(xmlhttp.status);
-			if (xmlhttp.status != 200 && xmlhttp.readyState == 4) {
-				createProjectSave(null);
-			} else {
-				if (xmlhttp.responseXML == null)
-				{
-					return createProjectSave(null);
-				}
-				var response = xmlhttp.responseXML.childNodes[0];
-				if (response.getAttribute('state') == "OK")
-				{
-					var file = response.getElementsByTagName('file')[0];
-					console.log('Save OK: Filename '+file.textContent+','+file.getAttribute('bytes')+'B');
-					popup.showPopup();
-					popup.popupContent.innerHTML = null;
-					popup.popupContent.textContent = "Thank you!";
-				} else {
-					var message = response.getElementsByTagName('message')[0];
-					errorSessionDump(message.textContent);
-				}
-			}
-		};
-		xmlhttp.send(file);
-	}
-}
-
-function errorSessionDump(msg){
-	// Create the partial interface XML save
-	// Include error node with message on why the dump occured
-	var xmlDoc = interfaceXMLSave();
-	var err = document.createElement('error');
-	err.textContent = msg;
-	xmlDoc.appendChild(err);
-	var parent = document.createElement("div");
-	parent.appendChild(xmlDoc);
-	var file = [parent.innerHTML];
-	var bb = new Blob(file,{type : 'application/xml'});
-	var dnlk = window.URL.createObjectURL(bb);
-	var a = document.createElement("a");
-	a.hidden = '';
-	a.href = dnlk;
-	a.download = "save.xml";
-	a.textContent = "Save File";
-	
-	popup.showPopup();
-	popup.popupContent.innerHTML = "ERROR : "+msg;
-	popup.popupContent.appendChild(a);
-}
-
-// Only other global function which must be defined in the interface class. Determines how to create the XML document.
-function interfaceXMLSave(){
-	// Create the XML string to be exported with results
-	var xmlDoc = document.createElement("BrowserEvaluationResult");
-	var projectDocument = specification.projectXML;
-	projectDocument.setAttribute('file-name',url);
-	xmlDoc.appendChild(projectDocument);
-	xmlDoc.appendChild(returnDateNode());
-	xmlDoc.appendChild(interfaceContext.returnNavigator());
-	for (var i=0; i<testState.stateResults.length; i++)
-	{
-		xmlDoc.appendChild(testState.stateResults[i]);
-	}
-	
-	return xmlDoc;
-}
-
-function AudioEngine() {
+function AudioEngine(specification) {
 	
 	// Create two output paths, the main outputGain and fooGain.
 	// Output gain is default to 1 and any items for playback route here
@@ -747,13 +778,78 @@
 	// Create the timer Object
 	this.timer = new timer();
 	// Create session metrics
-	this.metric = new sessionMetrics(this);
+	this.metric = new sessionMetrics(this,specification);
 	
 	this.loopPlayback = false;
 	
 	// Create store for new audioObjects
 	this.audioObjects = [];
 	
+	this.buffers = [];
+	this.bufferObj = function()
+	{
+		this.url = null;
+		this.buffer = null;
+		this.xmlRequest = new XMLHttpRequest();
+		this.xmlRequest.parent = this;
+		this.users = [];
+		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].state = 1;
+						if (bufferObj.users[i].interfaceDOM != null)
+						{
+							bufferObj.users[i].bufferLoaded(bufferObj);
+						}
+					}
+					//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.progress = 0;
+			this.progressCallback = function(event){
+				if (event.lengthComputable)
+				{
+					this.parent.progress = event.loaded / event.total;
+					for (var i=0; i<this.parent.users.length; i++)
+					{
+						if(this.parent.users[i].interfaceDOM != null)
+						{
+							if (typeof this.parent.users[i].interfaceDOM.updateLoading === "function")
+							{
+								this.parent.users[i].interfaceDOM.updateLoading(this.parent.progress*100);
+							}
+						}
+					}
+				}
+			};
+			this.xmlRequest.addEventListener("progress", this.progressCallback);
+			this.xmlRequest.send();
+		};
+	};
+	
 	this.play = function(id) {
 		// Start the timer and set the audioEngine state to playing (1)
 		if (this.status == 0 && this.loopPlayback) {
@@ -794,7 +890,7 @@
 						this.audioObjects[i].outputGain.gain.value = 0.0;
 						this.audioObjects[i].stop();
 					} else if (i == id) {
-						this.audioObjects[id].outputGain.gain.value = 1.0;
+						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);
 					}
 				}
@@ -823,9 +919,31 @@
 		audioObjectId = this.audioObjects.length;
 		this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
 
-		// AudioObject will get track itself.
+		// Check if audioObject buffer is currently stored by full URL
+		var URL = element.parent.hostURL + element.url;
+		var buffer = null;
+		for (var i=0; i<this.buffers.length; i++)
+		{
+			if (URL == this.buffers[i].url)
+			{
+				buffer = this.buffers[i];
+				break;
+			}
+		}
+		if (buffer == null)
+		{
+			console.log("[WARN]: Buffer was not loaded in pre-test! "+URL);
+			buffer = new this.bufferObj();
+			buffer.getMedia(URL);
+			this.buffers.push(buffer);
+		}
 		this.audioObjects[audioObjectId].specification = element;
-		this.audioObjects[audioObjectId].constructTrack(element.parent.hostURL + element.url);
+		this.audioObjects[audioObjectId].url = URL;
+		buffer.users.push(this.audioObjects[audioObjectId]);
+		if (buffer.buffer != null)
+		{
+			this.audioObjects[audioObjectId].bufferLoaded(buffer);
+		}
 		return this.audioObjects[audioObjectId];
 	};
 	
@@ -833,6 +951,10 @@
 		this.state = 0;
 		this.audioObjectsReady = false;
 		this.metric.reset();
+		for (var i=0; i < this.buffers.length; i++)
+		{
+			this.buffers[i].users = [];
+		}
 		this.audioObjects = [];
 	};
 	
@@ -861,26 +983,19 @@
 	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.length);
-			if (length < this.audioObjects[i].buffer.length)
+			if (length < this.audioObjects[i].buffer.buffer.length)
 			{
-				length = this.audioObjects[i].buffer.length;
+				length = this.audioObjects[i].buffer.buffer.length;
 				maxId = i;
 			}
 		}
-		// Perform difference
-		for (var i=0; i<lens.length; i++)
+		// Extract the audio and zero-pad
+		for (var i=0; i<this.audioObjects.length; i++)
 		{
-			lens[i] = length - lens[i];
-		}
-		// Extract the audio and zero-pad
-		for (var i=0; i<lens.length; i++)
-		{
-			var orig = this.audioObjects[i].buffer;
+			var orig = this.audioObjects[i].buffer.buffer;
 			var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate);
 			for (var c=0; c<orig.numberOfChannels; c++)
 			{
@@ -889,8 +1004,9 @@
 				for (var n=0; n<orig.length; n++)
 				{inData[n] = outData[n];}
 			}
-			this.audioObjects[i].buffer = hold;
-			delete orig;
+			hold.gain = orig.gain;
+			hold.lufs = orig.lufs;
+			this.audioObjects[i].buffer.buffer = hold;
 		}
 	};
 	
@@ -922,9 +1038,47 @@
 	// the audiobuffer is not designed for multi-start playback
 	// When stopeed, the buffer node is deleted and recreated with the stored buffer.
 	this.buffer;
+	
+	this.bufferLoaded = function(callee)
+	{
+		// Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
+		// audioObject and trigger the interfaceDOM.enable() function for user feedback
+		if (audioEngineContext.loopPlayback){
+			// First copy the buffer into this.buffer
+			this.buffer = new audioEngineContext.bufferObj();
+			this.buffer.url = callee.url;
+			this.buffer.buffer = audioContext.createBuffer(callee.buffer.numberOfChannels, callee.buffer.length, callee.buffer.sampleRate);
+			for (var c=0; c<callee.buffer.numberOfChannels; c++)
+			{
+				var src = callee.buffer.getChannelData(c);
+				var dst = this.buffer.buffer.getChannelData(c);
+				for (var n=0; n<src.length; n++)
+				{
+					dst[n] = src[n];
+				}
+			}
+		} else {
+			this.buffer = callee;
+		}
+		this.state = 1;
+		this.buffer.buffer.gain = callee.buffer.gain;
+		this.buffer.buffer.lufs = callee.buffer.lufs;
+		/*
+		var targetLUFS = this.specification.parent.loudness;
+		if (typeof targetLUFS === "number")
+		{
+			this.buffer.buffer.gain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
+		} else {
+			this.buffer.buffer.gain = 1.0;
+		}
+		*/
+		if (this.interfaceDOM != null) {
+			this.interfaceDOM.enable();
+		}
+	};
     
 	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());
 	};
 	
@@ -936,11 +1090,11 @@
 	};
 	
 	this.play = function(startTime) {
-		if (this.bufferNode == undefined) {
+		if (this.bufferNode == undefined && this.buffer.buffer != undefined) {
 			this.bufferNode = audioContext.createBufferSource();
 			this.bufferNode.owner = this;
 			this.bufferNode.connect(this.outputGain);
-			this.bufferNode.buffer = this.buffer;
+			this.bufferNode.buffer = this.buffer.buffer;
 			this.bufferNode.loop = audioEngineContext.loopPlayback;
 			this.bufferNode.onended = function(event) {
 				// Safari does not like using 'this' to reference the calling object!
@@ -968,7 +1122,7 @@
 		if (this.bufferNode != undefined) {
 			if (this.bufferNode.loop == true) {
 				if (audioEngineContext.status  == 1) {
-					return (time-this.metric.listenStart)%this.buffer.duration;
+					return (time-this.metric.listenStart)%this.buffer.buffer.duration;
 				} else {
 					return 0;
 				}
@@ -983,52 +1137,27 @@
 			return 0;
 		}
 	};
-
-	this.constructTrack = function(url) {
-		var request = new XMLHttpRequest();
-		this.url = url;
-		request.open('GET',url,true);
-		request.responseType = 'arraybuffer';
-		
-		var audioObj = this;
-		
-		// Create callback to decode the data asynchronously
-		request.onloadend = function() {
-			audioContext.decodeAudioData(request.response, function(decodedData) {
-				audioObj.buffer = decodedData;
-				audioObj.state = 1;
-				if (audioObj.specification.type != 'outsidereference')
-					{audioObj.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 (audioObj.state == 0 || audioObj.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');
-					}
-				}
-			});
-		};
-		request.send();
-	};
 	
 	this.exportXMLDOM = function() {
 		var root = document.createElement('audioElement');
 		root.id = this.specification.id;
-		root.setAttribute('url',this.url);
+		root.setAttribute('url',this.specification.url);
 		var file = document.createElement('file');
-		file.setAttribute('sampleRate',this.buffer.sampleRate);
-		file.setAttribute('channels',this.buffer.numberOfChannels);
-		file.setAttribute('sampleCount',this.buffer.length);
-		file.setAttribute('duration',this.buffer.duration);
+		file.setAttribute('sampleRate',this.buffer.buffer.sampleRate);
+		file.setAttribute('channels',this.buffer.buffer.numberOfChannels);
+		file.setAttribute('sampleCount',this.buffer.buffer.length);
+		file.setAttribute('duration',this.buffer.buffer.duration);
 		root.appendChild(file);
 		if (this.specification.type != 'outsidereference') {
-			root.appendChild(this.interfaceDOM.exportXMLDOM(this));
+			var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
+			if (interfaceXML.length == undefined) {
+				root.appendChild(interfaceXML);
+			} else {
+				for (var i=0; i<interfaceXML.length; i++)
+				{
+					root.appendChild(interfaceXML[i]);
+				}
+			}
 			root.appendChild(this.commentDOM.exportXMLDOM(this));
 			if(this.specification.type == 'anchor') {
 				root.setAttribute('anchor',true);
@@ -1084,7 +1213,7 @@
 	};
 }
 
-function sessionMetrics(engine)
+function sessionMetrics(engine,specification)
 {
 	/* Used by audioEngine to link to audioObjects to minimise the timer call timers;
 	 */
@@ -1095,6 +1224,46 @@
 		this.lastClicked = -1;
 		this.data = -1;
 	};
+	
+	this.enableElementInitialPosition = false;
+	this.enableElementListenTracker = false;
+	this.enableElementTimer = false;
+	this.enableElementTracker = false;
+	this.enableFlagListenedTo = false;
+	this.enableFlagMoved = false;
+	this.enableTestTimer = false;
+	// Obtain the metrics enabled
+	for (var i=0; i<specification.metrics.length; i++)
+	{
+		var node = specification.metrics[i];
+		switch(node.enabled)
+		{
+		case 'testTimer':
+			this.enableTestTimer = true;
+			break;
+		case 'elementTimer':
+			this.enableElementTimer = true;
+			break;
+		case 'elementTracker':
+			this.enableElementTracker = true;
+			break;
+		case 'elementListenTracker':
+			this.enableElementListenTracker = true;
+			break;
+		case 'elementInitialPosition':
+			this.enableElementInitialPosition = true;
+			break;
+		case 'elementFlagListenedTo':
+			this.enableFlagListenedTo = true;
+			break;
+		case 'elementFlagMoved':
+			this.enableFlagMoved = true;
+			break;
+		case 'elementFlagComments':
+			this.enableFlagComments = true;
+			break;
+		}
+	}
 	this.initialiseTest = function(){};
 }
 
@@ -1304,7 +1473,7 @@
 	
 	hold.appendChild(date);
 	hold.appendChild(time);
-	return hold
+	return hold;
 	
 }
 
@@ -1312,18 +1481,59 @@
 	// Handles the decoding of the project specification XML into a simple JavaScript Object.
 	
 	this.interfaceType = null;
-	this.commonInterface = null;
+	this.commonInterface = new function()
+	{
+		this.options = [];
+		this.optionNode = function(input)
+		{
+			var name = input.getAttribute('name');
+			this.type = name;
+			if(this.type == "option")
+			{
+				this.name = input.id;
+			} else if (this.type == "check")
+			{
+				this.check = input.id;
+			}
+		};
+	};
+	
+	this.randomiseOrder = function(input)
+	{
+		// This takes an array of information and randomises the order
+		var N = input.length;
+		
+		var inputSequence = []; // For safety purposes: keep track of randomisation
+		for (var counter = 0; counter < N; ++counter) 
+			inputSequence.push(counter) // Fill array
+		var inputSequenceClone = inputSequence.slice(0);
+		
+		var holdArr = [];
+		var outputSequence = [];
+		for (var n=0; n<N; n++)
+		{
+			// First pick a random number
+			var r = Math.random();
+			// Multiply and floor by the number of elements left
+			r = Math.floor(r*input.length);
+			// Pick out that element and delete from the array
+			holdArr.push(input.splice(r,1)[0]);
+			// Do the same with sequence
+			outputSequence.push(inputSequence.splice(r,1)[0]);
+		}
+		console.log(inputSequenceClone.toString()); // print original array to console
+		console.log(outputSequence.toString()); 	// print randomised array to console
+		return holdArr;
+	};
 	this.projectReturn = null;
 	this.randomiseOrder = null;
 	this.collectMetrics = null;
 	this.testPages = null;
-	this.preTest = null;
-	this.postTest = null;
-	this.metrics =[];
+	this.audioHolders = [];
+	this.metrics = [];
+	this.loudness = null;
 	
-	this.audioHolders = [];
-	
-	this.decode = function() {
+	this.decode = function(projectXML) {
 		// projectXML - DOM Parsed document
 		this.projectXML = projectXML.childNodes[0];
 		var setupNode = projectXML.getElementsByTagName('setup')[0];
@@ -1343,10 +1553,29 @@
 			this.testPages = Number(this.testPages);
 			if (this.testPages == 0) {this.testPages = null;}
 		}
+		if (setupNode.getAttribute('loudness') != null)
+		{
+			var XMLloudness = setupNode.getAttribute('loudness');
+			if (isNaN(Number(XMLloudness)) == false)
+			{
+				this.loudness = Number(XMLloudness);
+			}
+		}
 		var metricCollection = setupNode.getElementsByTagName('Metric');
 		
-		this.preTest = new this.prepostNode('pretest',setupNode.getElementsByTagName('PreTest'));
-		this.postTest = new this.prepostNode('posttest',setupNode.getElementsByTagName('PostTest'));
+		var setupPreTestNode = setupNode.getElementsByTagName('PreTest');
+		if (setupPreTestNode.length != 0)
+		{
+			setupPreTestNode = setupPreTestNode[0];
+			this.preTest.construct(setupPreTestNode);
+		}
+		
+		var setupPostTestNode = setupNode.getElementsByTagName('PostTest');
+		if (setupPostTestNode.length != 0)
+		{
+			setupPostTestNode = setupPostTestNode[0];
+			this.postTest.construct(setupPostTestNode);
+		}
 		
 		if (metricCollection.length > 0) {
 			metricCollection = metricCollection[0].getElementsByTagName('metricEnable');
@@ -1388,7 +1617,10 @@
 						}
 					}
 				} else if (this.type == 'anchor' || this.type == 'reference') {
-					Console.log("WARNING: Anchor and Reference tags in the <interface> node are depricated");
+					this.value = Number(child.textContent);
+					this.enforce = child.getAttribute('enforce');
+					if (this.enforce == 'true') {this.enforce = true;}
+					else {this.enforce = false;}
 				}
 			};
 			this.options = [];
@@ -1403,11 +1635,13 @@
 		
 		var audioHolders = projectXML.getElementsByTagName('audioHolder');
 		for (var i=0; i<audioHolders.length; i++) {
-			this.audioHolders.push(new this.audioHolderNode(this,audioHolders[i]));
+			var node = new this.audioHolderNode(this);
+			node.decode(this,audioHolders[i]);
+			this.audioHolders.push(node);
 		}
 		
 		// New check if we need to randomise the test order
-		if (this.randomiseOrder)
+		if (this.randomiseOrder && typeof randomiseOrder === "function")
 		{
 	 		this.audioHolders = randomiseOrder(this.audioHolders);
 	 		for (var i=0; i<this.audioHolders.length; i++)
@@ -1432,238 +1666,617 @@
 		}
 	};
 	
-	this.prepostNode = function(type,Collection) {
+	this.encode = function()
+	{
+		var root = document.implementation.createDocument(null,"BrowserEvalProjectDocument");
+		// First get all the <setup> tag compiled
+		var setupNode = root.createElement("setup");
+		setupNode.setAttribute('interface',this.interfaceType);
+		setupNode.setAttribute('projectReturn',this.projectReturn);
+		setupNode.setAttribute('randomiseOrder',this.randomiseOrder);
+		setupNode.setAttribute('collectMetrics',this.collectMetrics);
+		setupNode.setAttribute('testPages',this.testPages);
+		if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);}
+		
+		var setupPreTest = root.createElement("PreTest");
+		for (var i=0; i<this.preTest.options.length; i++)
+		{
+			setupPreTest.appendChild(this.preTest.options[i].exportXML(root));
+		}
+		
+		var setupPostTest = root.createElement("PostTest");
+		for (var i=0; i<this.postTest.options.length; i++)
+		{
+			setupPostTest.appendChild(this.postTest.options[i].exportXML(root));
+		}
+		
+		setupNode.appendChild(setupPreTest);
+		setupNode.appendChild(setupPostTest);
+		
+		// <Metric> tag
+		var Metric = root.createElement("Metric");
+		for (var i=0; i<this.metrics.length; i++)
+		{
+			var metricEnable = root.createElement("metricEnable");
+			metricEnable.textContent = this.metrics[i].enabled;
+			Metric.appendChild(metricEnable);
+		}
+		setupNode.appendChild(Metric);
+		
+		// <interface> tag
+		var CommonInterface = root.createElement("interface");
+		for (var i=0; i<this.commonInterface.options.length; i++)
+		{
+			var CIObj = this.commonInterface.options[i];
+			var CINode = root.createElement(CIObj.type);
+			if (CIObj.type == "check") {CINode.setAttribute("name",CIObj.check);}
+			else {CINode.setAttribute("name",CIObj.name);}
+			CommonInterface.appendChild(CINode);
+		}
+		setupNode.appendChild(CommonInterface);
+		
+		root.getElementsByTagName("BrowserEvalProjectDocument")[0].appendChild(setupNode);
+		// Time for the <audioHolder> tags
+		for (var ahIndex = 0; ahIndex < this.audioHolders.length; ahIndex++)
+		{
+			var node = this.audioHolders[ahIndex].encode(root);
+			root.getElementsByTagName("BrowserEvalProjectDocument")[0].appendChild(node);
+		}
+		return root;
+	};
+	
+	this.prepostNode = function(type) {
 		this.type = type;
 		this.options = [];
 		
-		this.OptionNode = function(child) {
+		this.OptionNode = function() {
 			
-			this.childOption = function(element) {
+			this.childOption = function() {
 				this.type = 'option';
-				this.id = element.id;
-				this.name = element.getAttribute('name');
-				this.text = element.textContent;
+				this.id = null;
+				this.name = undefined;
+				this.text = null;
 			};
 			
-			this.type = child.nodeName;
-			if (child.nodeName == "question") {
-				this.id = child.id;
-				this.mandatory;
-				if (child.getAttribute('mandatory') == "true") {this.mandatory = true;}
-				else {this.mandatory = false;}
-				this.question = child.textContent;
-				if (child.getAttribute('boxsize') == null) {
-					this.boxsize = 'normal';
-				} else {
-					this.boxsize = child.getAttribute('boxsize');
+			this.type = undefined;
+			this.id = undefined;
+			this.mandatory = undefined;
+			this.question = undefined;
+			this.statement = undefined;
+			this.boxsize = undefined;
+			this.options = [];
+			this.min = undefined;
+			this.max = undefined;
+			this.step = undefined;
+			
+			this.decode = function(child)
+			{
+				this.type = child.nodeName;
+				if (child.nodeName == "question") {
+					this.id = child.id;
+					this.mandatory;
+					if (child.getAttribute('mandatory') == "true") {this.mandatory = true;}
+					else {this.mandatory = false;}
+					this.question = child.textContent;
+					if (child.getAttribute('boxsize') == null) {
+						this.boxsize = 'normal';
+					} else {
+						this.boxsize = child.getAttribute('boxsize');
+					}
+				} else if (child.nodeName == "statement") {
+					this.statement = child.textContent;
+				} else if (child.nodeName == "checkbox" || child.nodeName == "radio") {
+					var element = child.firstElementChild;
+					this.id = child.id;
+					if (element == null) {
+						console.log('Malformed' +child.nodeName+ 'entry');
+						this.statement = 'Malformed' +child.nodeName+ 'entry';
+						this.type = 'statement';
+					} else {
+						this.options = [];
+						while (element != null) {
+							if (element.nodeName == 'statement' && this.statement == undefined){
+								this.statement = element.textContent;
+							} else if (element.nodeName == 'option') {
+								var node = new this.childOption();
+								node.id = element.id;
+								node.name = element.getAttribute('name');
+								node.text = element.textContent;
+								this.options.push(node);
+							}
+							element = element.nextElementSibling;
+						}
+					}
+				} else if (child.nodeName == "number") {
+					this.statement = child.textContent;
+					this.id = child.id;
+					this.min = child.getAttribute('min');
+					this.max = child.getAttribute('max');
+					this.step = child.getAttribute('step');
 				}
-			} else if (child.nodeName == "statement") {
-				this.statement = child.textContent;
-			} else if (child.nodeName == "checkbox" || child.nodeName == "radio") {
-				var element = child.firstElementChild;
-				this.id = child.id;
-				if (element == null) {
-					console.log('Malformed' +child.nodeName+ 'entry');
-					this.statement = 'Malformed' +child.nodeName+ 'entry';
-					this.type = 'statement';
-				} else {
-					this.options = [];
-					while (element != null) {
-						if (element.nodeName == 'statement' && this.statement == undefined){
-							this.statement = element.textContent;
-						} else if (element.nodeName == 'option') {
-							this.options.push(new this.childOption(element));
-						}
-						element = element.nextElementSibling;
+			};
+			
+			this.exportXML = function(root)
+			{
+				var node = root.createElement(this.type);
+				switch(this.type)
+				{
+				case "statement":
+					node.textContent = this.statement;
+					break;
+				case "question":
+					node.id = this.id;
+					node.setAttribute("mandatory",this.mandatory);
+					node.setAttribute("boxsize",this.boxsize);
+					node.textContent = this.question;
+					break;
+				case "number":
+					node.id = this.id;
+					node.setAttribute("mandatory",this.mandatory);
+					node.setAttribute("min", this.min);
+					node.setAttribute("max", this.max);
+					node.setAttribute("step", this.step);
+					node.textContent = this.statement;
+					break;
+				case "checkbox":
+					node.id = this.id;
+					var statement = root.createElement("statement");
+					statement.textContent = this.statement;
+					node.appendChild(statement);
+					for (var i=0; i<this.options.length; i++)
+					{
+						var option = this.options[i];
+						var optionNode = root.createElement("option");
+						optionNode.id = option.id;
+						optionNode.textContent = option.text;
+						node.appendChild(optionNode);
 					}
+					break;
+				case "radio":
+					node.id = this.id;
+					var statement = root.createElement("statement");
+					statement.textContent = this.statement;
+					node.appendChild(statement);
+					for (var i=0; i<this.options.length; i++)
+					{
+						var option = this.options[i];
+						var optionNode = root.createElement("option");
+						optionNode.setAttribute("name",option.name);
+						optionNode.textContent = option.text;
+						node.appendChild(optionNode);
+					}
+					break;
 				}
-			} else if (child.nodeName == "number") {
-				this.statement = child.textContent;
-				this.id = child.id;
-				this.min = child.getAttribute('min');
-				this.max = child.getAttribute('max');
-				this.step = child.getAttribute('step');
+				return node;
+			};
+		};
+		this.construct = function(Collection)
+		{
+			if (Collection.childElementCount != 0) {
+				var child = Collection.firstElementChild;
+				var node = new this.OptionNode();
+				node.decode(child);
+				this.options.push(node);
+				while (child.nextElementSibling != null) {
+					child = child.nextElementSibling;
+					node = new this.OptionNode();
+					node.decode(child);
+					this.options.push(node);
+				}
 			}
 		};
-		
-		// On construction:
-		if (Collection.length != 0) {
-			Collection = Collection[0];
-			if (Collection.childElementCount != 0) {
-				var child = Collection.firstElementChild;
-				this.options.push(new this.OptionNode(child));
-				while (child.nextElementSibling != null) {
-					child = child.nextElementSibling;
-					this.options.push(new this.OptionNode(child));
-				}
-			}
-		}
 	};
+	this.preTest = new this.prepostNode("pretest");
+	this.postTest = new this.prepostNode("posttest");
 	
 	this.metricNode = function(name) {
 		this.enabled = name;
 	};
 	
-	this.audioHolderNode = function(parent,xml) {
+	this.audioHolderNode = function(parent) {
 		this.type = 'audioHolder';
-		this.presentedId = parent.audioHolders.length;
-		this.interfaceNode = function(DOM) {
-			var title = DOM.getElementsByTagName('title');
-			if (title.length == 0) {this.title = null;}
-			else {this.title = title[0].textContent;}
-			this.options = parent.commonInterface.options;
-			var scale = DOM.getElementsByTagName('scale');
-			this.scale = [];
-			for (var i=0; i<scale.length; i++) {
-				var arr = [null, null];
-				arr[0] = scale[i].getAttribute('position');
-				arr[1] = scale[i].textContent;
-				this.scale.push(arr);
+		this.presentedId = undefined;
+		this.id = undefined;
+		this.hostURL = undefined;
+		this.sampleRate = undefined;
+		this.randomiseOrder = undefined;
+		this.loop = undefined;
+		this.elementComments = undefined;
+		this.outsideReference = null;
+		this.loudness = null;
+		this.initialPosition = null;
+		this.preTest = new parent.prepostNode("pretest");
+		this.postTest = new parent.prepostNode("pretest");
+		this.interfaces = [];
+		this.commentBoxPrefix = "Comment on track";
+		this.audioElements = [];
+		this.commentQuestions = [];
+		
+		this.decode = function(parent,xml)
+		{
+			this.presentedId = parent.audioHolders.length;
+			this.id = xml.id;
+			this.hostURL = xml.getAttribute('hostURL');
+			this.sampleRate = xml.getAttribute('sampleRate');
+			if (xml.getAttribute('randomiseOrder') == "true") {this.randomiseOrder = true;}
+			else {this.randomiseOrder = false;}
+			this.repeatCount = xml.getAttribute('repeatCount');
+			if (xml.getAttribute('loop') == 'true') {this.loop = true;}
+			else {this.loop == false;}
+			if (xml.getAttribute('elementComments') == "true") {this.elementComments = true;}
+			else {this.elementComments = false;}
+			if (typeof parent.loudness === "number")
+			{
+				this.loudness = parent.loudness;
+			}
+			if (typeof xml.getAttribute('initial-position') === "string")
+			{
+				var xmlInitialPosition = Number(xml.getAttribute('initial-position'));
+				if (isNaN(xmlInitialPosition) == false)
+				{
+					if (xmlInitialPosition > 1)
+					{
+						xmlInitialPosition /= 100;
+					}
+					this.initialPosition = xmlInitialPosition;
+				}
+			}
+			if (xml.getAttribute('loudness') != null)
+			{
+				var XMLloudness = xml.getAttribute('loudness');
+				if (isNaN(Number(XMLloudness)) == false)
+				{
+					this.loudness = Number(XMLloudness);
+				}
+			}
+			var setupPreTestNode = xml.getElementsByTagName('PreTest');
+			if (setupPreTestNode.length != 0)
+			{
+				setupPreTestNode = setupPreTestNode[0];
+				this.preTest.construct(setupPreTestNode);
+			}
+			
+			var setupPostTestNode = xml.getElementsByTagName('PostTest');
+			if (setupPostTestNode.length != 0)
+			{
+				setupPostTestNode = setupPostTestNode[0];
+				this.postTest.construct(setupPostTestNode);
+			}
+			
+			var interfaceDOM = xml.getElementsByTagName('interface');
+			for (var i=0; i<interfaceDOM.length; i++) {
+				var node = new this.interfaceNode();
+				node.decode(interfaceDOM[i]);
+				this.interfaces.push(node);
+			}
+			this.commentBoxPrefix = xml.getElementsByTagName('commentBoxPrefix');
+			if (this.commentBoxPrefix.length != 0) {
+				this.commentBoxPrefix = this.commentBoxPrefix[0].textContent;
+			} else {
+				this.commentBoxPrefix = "Comment on track";
+			}
+			var audioElementsDOM = xml.getElementsByTagName('audioElements');
+			var outsideReferenceHolder = null;
+			for (var i=0; i<audioElementsDOM.length; i++) {
+				var node = new this.audioElementNode();
+				node.decode(this,audioElementsDOM[i]);
+				if (audioElementsDOM[i].getAttribute('type') == 'outsidereference') {
+					if (this.outsideReference == null) {
+						outsideReferenceHolder = node;
+						this.outsideReference = i;
+					} else {
+						console.log('Error only one audioelement can be of type outsidereference per audioholder');
+						this.audioElements.push(node);
+						console.log('Element id '+audioElementsDOM[i].id+' made into normal node');
+					}
+				} else {
+					this.audioElements.push(node);
+				}
+			}
+			
+			if (this.randomiseOrder == true && typeof randomiseOrder === "function")
+			{
+				this.audioElements = randomiseOrder(this.audioElements);
+			}
+			if (outsideReferenceHolder != null)
+			{
+				this.audioElements.push(outsideReferenceHolder);
+				this.outsideReference = this.audioElements.length-1;
+			}
+			
+			
+			var commentQuestionsDOM = xml.getElementsByTagName('CommentQuestion');
+			for (var i=0; i<commentQuestionsDOM.length; i++) {
+				var node = new this.commentQuestionNode();
+				node.decode(commentQuestionsDOM[i]);
+				this.commentQuestions.push(node);
 			}
 		};
 		
-		this.audioElementNode = function(parent,xml) {
-			this.url = xml.getAttribute('url');
-			this.id = xml.id;
-			this.parent = parent;
-			this.type = xml.getAttribute('type');
-			if (this.type == null) {this.type = "normal";}
-			if (this.type == 'anchor') {this.anchor = true;}
-			else {this.anchor = false;}
-			if (this.type == 'reference') {this.reference = true;}
-			else {this.reference = false;}
+		this.encode = function(root)
+		{
+			var AHNode = root.createElement("audioHolder");
+			AHNode.id = this.id;
+			AHNode.setAttribute("hostURL",this.hostURL);
+			AHNode.setAttribute("sampleRate",this.sampleRate);
+			AHNode.setAttribute("randomiseOrder",this.randomiseOrder);
+			AHNode.setAttribute("repeatCount",this.repeatCount);
+			AHNode.setAttribute("loop",this.loop);
+			AHNode.setAttribute("elementComments",this.elementComments);
+			if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);}
+			if(this.initialPosition != null) {
+				AHNode.setAttribute("loudness",this.initialPosition*100);
+				}
+			for (var i=0; i<this.interfaces.length; i++)
+			{
+				AHNode.appendChild(this.interfaces[i].encode(root));
+			}
 			
-			if (this.anchor == true || this.reference == true)
+			for (var i=0; i<this.audioElements.length; i++) {
+				AHNode.appendChild(this.audioElements[i].encode(root));
+			}
+			// Create <CommentQuestion>
+			for (var i=0; i<this.commentQuestions.length; i++)
 			{
-				this.marker = xml.getAttribute('marker');
-				if (this.marker != undefined)
+				AHNode.appendChild(this.commentQuestions[i].exportXML(root));
+			}
+			
+			// Create <PreTest>
+			var AHPreTest = root.createElement("PreTest");
+			for (var i=0; i<this.preTest.options.length; i++)
+			{
+				AHPreTest.appendChild(this.preTest.options[i].exportXML(root));
+			}
+			
+			var AHPostTest = root.createElement("PostTest");
+			for (var i=0; i<this.postTest.options.length; i++)
+			{
+				AHPostTest.appendChild(this.postTest.options[i].exportXML(root));
+			}
+			AHNode.appendChild(AHPreTest);
+			AHNode.appendChild(AHPostTest);
+			return AHNode;
+		};
+		
+		this.interfaceNode = function() {
+			this.title = undefined;
+			this.options = [];
+			this.scale = [];
+			this.name = undefined;
+			this.decode = function(DOM)
+			{
+				var title = DOM.getElementsByTagName('title');
+				if (title.length == 0) {this.title = null;}
+				else {this.title = title[0].textContent;}
+				var name = DOM.getAttribute("name");
+				if (name != undefined) {this.name = name;}
+				this.options = parent.commonInterface.options;
+				var scale = DOM.getElementsByTagName('scale');
+				this.scale = [];
+				for (var i=0; i<scale.length; i++) {
+					var arr = [null, null];
+					arr[0] = scale[i].getAttribute('position');
+					arr[1] = scale[i].textContent;
+					this.scale.push(arr);
+				}
+			};
+			this.encode = function(root)
+			{
+				var node = root.createElement("interface");
+				if (this.title != undefined)
 				{
-					this.marker = Number(this.marker);
-					if (isNaN(this.marker) == false)
+					var title = root.createElement("title");
+					title.textContent = this.title;
+					node.appendChild(title);
+				}
+				for (var i=0; i<this.options.length; i++)
+				{
+					var optionNode = root.createElement(this.options[i].type);
+					if (this.options[i].type == "option")
 					{
-						if (this.marker > 1)
-						{	this.marker /= 100.0;}
-						if (this.marker >= 0 && this.marker <= 1)
+						optionNode.setAttribute("name",this.options[i].name);
+					} else if (this.options[i].type == "check") {
+						optionNode.setAttribute("check",this.options[i].check);
+					} else if (this.options[i].type == "scalerange") {
+						optionNode.setAttribute("min",this.options[i].min*100);
+						optionNode.setAttribute("max",this.options[i].max*100);
+					}
+					node.appendChild(optionNode);
+				}
+				for (var i=0; i<this.scale.length; i++) {
+					var scale = root.createElement("scale");
+					scale.setAttribute("position",this.scale[i][0]);
+					scale.textContent = this.scale[i][1];
+					node.appendChild(scale);
+				}
+				return node;
+			};
+		};
+		
+		this.audioElementNode = function() {
+			this.url = null;
+			this.id = null;
+			this.parent = null;
+			this.type = "normal";
+			this.marker = false;
+			this.enforce = false;
+			this.gain = 1.0;
+			this.decode = function(parent,xml)
+			{
+				this.url = xml.getAttribute('url');
+				this.id = xml.id;
+				this.parent = parent;
+				this.type = xml.getAttribute('type');
+				var gain = xml.getAttribute('gain');
+				if (isNaN(gain) == false && gain != null)
+				{
+					this.gain = decibelToLinear(Number(gain));
+				}
+				if (this.type == null) {this.type = "normal";}
+				if (this.type == 'anchor') {this.anchor = true;}
+				else {this.anchor = false;}
+				if (this.type == 'reference') {this.reference = true;}
+				else {this.reference = false;}
+				if (this.anchor == true || this.reference == true)
+				{
+					this.marker = xml.getAttribute('marker');
+					if (this.marker != undefined)
+					{
+						this.marker = Number(this.marker);
+						if (isNaN(this.marker) == false)
 						{
-							this.enforce = true;
-							return;
+							if (this.marker > 1)
+							{	this.marker /= 100.0;}
+							if (this.marker >= 0 && this.marker <= 1)
+							{
+								this.enforce = true;
+								return;
+							} else {
+								console.log("ERROR - Marker of audioElement "+this.id+" is not between 0 and 1 (float) or 0 and 100 (integer)!");
+								console.log("ERROR - Marker not enforced!");
+							}
 						} else {
-							console.log("ERROR - Marker of audioElement "+this.id+" is not between 0 and 1 (float) or 0 and 100 (integer)!");
+							console.log("ERROR - Marker of audioElement "+this.id+" is not a number!");
 							console.log("ERROR - Marker not enforced!");
 						}
-					} else {
-						console.log("ERROR - Marker of audioElement "+this.id+" is not a number!");
-						console.log("ERROR - Marker not enforced!");
 					}
 				}
-			}
-			this.marker = false;
-			this.enforce = false;
+			};
+			this.encode = function(root)
+			{
+				var AENode = root.createElement("audioElements");
+				AENode.id = this.id;
+				AENode.setAttribute("url",this.url);
+				AENode.setAttribute("type",this.type);
+				AENode.setAttribute("gain",linearToDecibel(this.gain));
+				if (this.marker != false)
+				{
+					AENode.setAttribute("marker",this.marker*100);
+				}
+				return AENode;
+			};
 		};
 		
 		this.commentQuestionNode = function(xml) {
-			this.childOption = function(element) {
+			this.id = null;
+			this.type = undefined;
+			this.question = undefined;
+			this.options = [];
+			this.statement = undefined;
+			
+			this.childOption = function() {
 				this.type = 'option';
-				this.name = element.getAttribute('name');
-				this.text = element.textContent;
+				this.name = null;
+				this.text = null;
 			};
-			this.id = xml.id;
-			if (xml.getAttribute('mandatory') == 'true') {this.mandatory = true;}
-			else {this.mandatory = false;}
-			this.type = xml.getAttribute('type');
-			if (this.type == undefined) {this.type = 'text';}
-			switch (this.type) {
-			case 'text':
-				this.question = xml.textContent;
-				break;
-			case 'radio':
-				var child = xml.firstElementChild;
-				this.options = [];
-				while (child != undefined) {
-					if (child.nodeName == 'statement' && this.statement == undefined) {
-						this.statement = child.textContent;
-					} else if (child.nodeName == 'option') {
-						this.options.push(new this.childOption(child));
+			this.exportXML = function(root)
+			{
+				var CQNode = root.createElement("CommentQuestion");
+				CQNode.id = this.id;
+				CQNode.setAttribute("type",this.type);
+				switch(this.type)
+				{
+				case "text":
+					CQNode.textContent = this.question;
+					break;
+				case "radio":
+					var statement = root.createElement("statement");
+					statement.textContent = this.statement;
+					CQNode.appendChild(statement);
+					for (var i=0; i<this.options.length; i++)
+					{
+						var optionNode = root.createElement("option");
+						optionNode.setAttribute("name",this.options[i].name);
+						optionNode.textContent = this.options[i].text;
+						CQNode.appendChild(optionNode);
 					}
-					child = child.nextElementSibling;
+					break;
+				case "checkbox":
+					var statement = root.createElement("statement");
+					statement.textContent = this.statement;
+					CQNode.appendChild(statement);
+					for (var i=0; i<this.options.length; i++)
+					{
+						var optionNode = root.createElement("option");
+						optionNode.setAttribute("name",this.options[i].name);
+						optionNode.textContent = this.options[i].text;
+						CQNode.appendChild(optionNode);
+					}
+					break;
 				}
-				break;
-			case 'checkbox':
-				var child = xml.firstElementChild;
-				this.options = [];
-				while (child != undefined) {
-					if (child.nodeName == 'statement' && this.statement == undefined) {
-						this.statement = child.textContent;
-					} else if (child.nodeName == 'option') {
-						this.options.push(new this.childOption(child));
+				return CQNode;
+			};
+			this.decode = function(xml) {
+				this.id = xml.id;
+				if (xml.getAttribute('mandatory') == 'true') {this.mandatory = true;}
+				else {this.mandatory = false;}
+				this.type = xml.getAttribute('type');
+				if (this.type == undefined) {this.type = 'text';}
+				switch (this.type) {
+				case 'text':
+					this.question = xml.textContent;
+					break;
+				case 'radio':
+					var child = xml.firstElementChild;
+					this.options = [];
+					while (child != undefined) {
+						if (child.nodeName == 'statement' && this.statement == undefined) {
+							this.statement = child.textContent;
+						} else if (child.nodeName == 'option') {
+							var node = new this.childOption();
+							node.name = child.getAttribute('name');
+							node.text = child.textContent;
+							this.options.push(node);
+						}
+						child = child.nextElementSibling;
 					}
-					child = child.nextElementSibling;
+					break;
+				case 'checkbox':
+					var child = xml.firstElementChild;
+					this.options = [];
+					while (child != undefined) {
+						if (child.nodeName == 'statement' && this.statement == undefined) {
+							this.statement = child.textContent;
+						} else if (child.nodeName == 'option') {
+							var node = new this.childOption();
+							node.name = child.getAttribute('name');
+							node.text = child.textContent;
+							this.options.push(node);
+						}
+						child = child.nextElementSibling;
+					}
+					break;
 				}
-				break;
-			}
+			};
 		};
-		
-		this.id = xml.id;
-		this.hostURL = xml.getAttribute('hostURL');
-		this.sampleRate = xml.getAttribute('sampleRate');
-		if (xml.getAttribute('randomiseOrder') == "true") {this.randomiseOrder = true;}
-		else {this.randomiseOrder = false;}
-		this.repeatCount = xml.getAttribute('repeatCount');
-		if (xml.getAttribute('loop') == 'true') {this.loop = true;}
-		else {this.loop == false;}
-		if (xml.getAttribute('elementComments') == "true") {this.elementComments = true;}
-		else {this.elementComments = false;}
-		
-		this.preTest = new parent.prepostNode('pretest',xml.getElementsByTagName('PreTest'));
-		this.postTest = new parent.prepostNode('posttest',xml.getElementsByTagName('PostTest'));
-		
-		this.interfaces = [];
-		var interfaceDOM = xml.getElementsByTagName('interface');
-		for (var i=0; i<interfaceDOM.length; i++) {
-			this.interfaces.push(new this.interfaceNode(interfaceDOM[i]));
-		}
-		
-		this.commentBoxPrefix = xml.getElementsByTagName('commentBoxPrefix');
-		if (this.commentBoxPrefix.length != 0) {
-			this.commentBoxPrefix = this.commentBoxPrefix[0].textContent;
-		} else {
-			this.commentBoxPrefix = "Comment on track";
-		}
-		
-		this.audioElements  =[];
-		var audioElementsDOM = xml.getElementsByTagName('audioElements');
-		this.outsideReference = null;
-		for (var i=0; i<audioElementsDOM.length; i++) {
-			if (audioElementsDOM[i].getAttribute('type') == 'outsidereference') {
-				if (this.outsideReference == null) {
-					this.outsideReference = new this.audioElementNode(this,audioElementsDOM[i]);
-				} else {
-					console.log('Error only one audioelement can be of type outsidereference per audioholder');
-					this.audioElements.push(new this.audioElementNode(this,audioElementsDOM[i]));
-					console.log('Element id '+audioElementsDOM[i].id+' made into normal node');
-				}
-			} else {
-				this.audioElements.push(new this.audioElementNode(this,audioElementsDOM[i]));
-			}
-		}
-		
-		if (this.randomiseOrder) {
-			this.audioElements = randomiseOrder(this.audioElements);
-		}
-		
-		this.commentQuestions = [];
-		var commentQuestionsDOM = xml.getElementsByTagName('CommentQuestion');
-		for (var i=0; i<commentQuestionsDOM.length; i++) {
-			this.commentQuestions.push(new this.commentQuestionNode(commentQuestionsDOM[i]));
-		}
 	};
 }
-
+			
 function Interface(specificationObject) {
 	// This handles the bindings between the interface and the audioEngineContext;
 	this.specification = specificationObject;
 	this.insertPoint = document.getElementById("topLevelBody");
 	
+	this.newPage = function(audioHolderObject)
+	{
+		audioEngineContext.newTestPage();
+		/// CHECK FOR SAMPLE RATE COMPATIBILITY
+		if (audioHolderObject.sampleRate != undefined) {
+			if (Number(audioHolderObject.sampleRate) != audioContext.sampleRate) {
+				var errStr = 'Sample rates do not match! Requested '+Number(audioHolderObject.sampleRate)+', got '+audioContext.sampleRate+'. Please set the sample rate to match before completing this test.';
+				alert(errStr);
+				return;
+			}
+		}
+		
+		audioEngineContext.loopPlayback = audioHolderObject.loop;
+		// Delete any previous audioObjects associated with the audioEngine
+		audioEngineContext.audioObjects = [];
+		interfaceContext.deleteCommentBoxes();
+		interfaceContext.deleteCommentQuestions();
+		loadTest(audioHolderObject);
+	};
+	
 	// Bounded by interface!!
 	// Interface object MUST have an exportXMLDOM method which returns the various DOM levels
 	// For example, APE returns  the slider position normalised in a <value> tag.
@@ -1672,6 +2285,7 @@
 	
 	this.resizeWindow = function(event)
 	{
+		popup.resize(event);
 		for(var i=0; i<this.commentBoxes.length; i++)
 		{this.commentBoxes[i].resize();}
 		for(var i=0; i<this.commentQuestions.length; i++)
@@ -2089,7 +2703,7 @@
 		this.setTimePerPixel = function(audioObject) {
 			//maxTime must be in seconds
 			this.playbackObject = audioObject;
-			this.maxTime = audioObject.buffer.duration;
+			this.maxTime = audioObject.buffer.buffer.duration;
 			var width = 490; //500 - 10, 5 each side of the tracker head
 			this.timePerPixel = this.maxTime/490;
 			if (this.maxTime < 60) {
@@ -2201,7 +2815,7 @@
 		for (var i = 0; i<audioEngineContext.audioObjects.length; i++)
 		{
 			var object = audioEngineContext.audioObjects[i];
-			var time = object.buffer.duration;
+			var time = object.buffer.buffer.duration;
 			var metric = object.metric;
 			var passed = false;
 			for (var j=0; j<metric.listenTracker.length; j++)
@@ -2225,7 +2839,7 @@
 		}
 		if (check_pass == false)
 		{
-			var str_start = "You have not listened to fragments ";
+			var str_start = "You have not completely listened to fragments ";
 			for (var i=0; i<error_obj.length; i++)
 			{
 				str_start += error_obj[i];
@@ -2239,4 +2853,64 @@
 			alert(str_start);
 		}
 	};
+	this.checkAllMoved = function()
+	{
+		var str = "You have not moved ";
+		var failed = [];
+		for (var i in audioEngineContext.audioObjects)
+		{
+			if(audioEngineContext.audioObjects[i].metric.wasMoved == false && audioEngineContext.audioObjects[i].specification.type != 'outsidereference')
+			{
+				failed.push(audioEngineContext.audioObjects[i].id);
+			}
+		}
+		if (failed.length == 0)
+		{
+			return true;
+		} else if (failed.length == 1)
+		{
+			str += 'track '+failed[0];
+		} else {
+			str += 'tracks ';
+			for (var i=0; i<failed.length-1; i++)
+			{
+				str += failed[i]+', ';
+			}
+			str += 'and '+failed[i];
+		}
+		str +='.';
+		alert(str);
+		console.log(str);
+		return false;
+	};
+	this.checkAllPlayed = function()
+	{
+		var str = "You have not played ";
+		var failed = [];
+		for (var i in audioEngineContext.audioObjects)
+		{
+			if(audioEngineContext.audioObjects[i].metric.wasListenedTo == false)
+			{
+				failed.push(audioEngineContext.audioObjects[i].id);
+			}
+		}
+		if (failed.length == 0)
+		{
+			return true;
+		} else if (failed.length == 1)
+		{
+			str += 'track '+failed[0];
+		} else {
+			str += 'tracks ';
+			for (var i=0; i<failed.length-1; i++)
+			{
+				str += failed[i]+', ';
+			}
+			str += 'and '+failed[i];
+		}
+		str +='.';
+		alert(str);
+		console.log(str);
+		return false;
+	};
 }
\ No newline at end of file