changeset 1762:64c0614fe3f2

Merge from branch "Dev_main"
author Nicholas Jillings <nickjillings@users.noreply.github.com>
date Fri, 05 Jun 2015 12:54:52 +0100
parents c33eef57f0b9 (diff) a5890005e289 (current diff)
children 3496198d413c
files ape.js core.js example_eval/project.xml test_create/test_create.html
diffstat 3 files changed, 658 insertions(+), 409 deletions(-) [+]
line wrap: on
line diff
--- a/ape.js	Thu Jun 04 10:06:42 2015 +0100
+++ b/ape.js	Fri Jun 05 12:54:52 2015 +0100
@@ -11,123 +11,29 @@
 
 
 // Once this is loaded and parsed, begin execution
-loadInterface(projectXML);
+loadInterface();
 
-function loadInterface(xmlDoc) {
+function loadInterface() {
 	
 	// Get the dimensions of the screen available to the page
 	var width = window.innerWidth;
 	var height = window.innerHeight;
 	
 	// The injection point into the HTML page
-	var insertPoint = document.getElementById("topLevelBody");
+	interfaceContext.insertPoint = document.getElementById("topLevelBody");
 	var testContent = document.createElement('div');
 	
 	testContent.id = 'testContent';
-	
-	
-	// Decode parts of the xmlDoc that are needed
-	// xmlDoc MUST already be parsed by jQuery!
-	var xmlSetup = xmlDoc.find('setup');
-	// Should put in an error function here incase of malprocessed or malformed XML
-	
-	// Create pre and post test questions
-	
-	var preTest = xmlSetup.find('PreTest');
-	var postTest = xmlSetup.find('PostTest');
-	preTest = preTest[0];
-	postTest = postTest[0];
-	
-	if (preTest == undefined) {preTest = document.createElement("preTest");}
-	if (postTest == undefined){postTest= document.createElement("postTest");}
-	
-	testState.stateMap.push(preTest);
-	
-	// Extract the different test XML DOM trees
-	var audioHolders = xmlDoc.find('audioHolder');
-	var testXMLSetups = [];
-	audioHolders.each(function(index,element) {
-		var repeatN = element.attributes['repeatCount'].value;
-		for (var r=0; r<=repeatN; r++) {
-			testXMLSetups.push(element);
-		}
-	});
-	 
-	// New check if we need to randomise the test order
-	var randomise = xmlSetup[0].attributes['randomiseOrder'];
-	if (randomise != undefined) {
-		if (randomise.value === 'true'){
-			randomise = true;
-		} else {
-			randomise = false;
-		}
-	} else {
-		randomise = false;
-	}
-	
-	if (randomise)
-	{
- 		testXMLSetups = randomiseOrder(testXMLSetups);
-	}
-	
-	$(testXMLSetups).each(function(index,elem){
-		testState.stateMap.push(elem);
-	})
-	 
-	 testState.stateMap.push(postTest);
-	 
-	// Obtain the metrics enabled
-	var metricNode = xmlSetup.find('Metric');
-	var metricNode = metricNode.find('metricEnable');
-	metricNode.each(function(index,node){
-		var enabled = node.textContent;
-		switch(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;
-		}
-	});
+
 	
 	// Create APE specific metric functions
 	audioEngineContext.metric.initialiseTest = function()
 	{
 	};
 	
-	audioEngineContext.metric.sliderMoveStart = function(id)
-	{
-		if (this.data == -1)
-		{
-			this.data = id;
-		} else {
-			console.log('ERROR: Metric tracker detecting two moves!');
-			this.data = -1;
-		}
-	};
 	audioEngineContext.metric.sliderMoved = function()
 	{
-		var time = audioEngineContext.timer.getTestTime();
+		
 		var id = this.data;
 		this.data = -1;
 		var position = convSliderPosToRate(id);
@@ -153,8 +59,10 @@
         console.log('slider ' + id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
 	};
 	
+	// Bindings for audioObjects
+	
 	// Create the top div for the Title element
-	var titleAttr = xmlSetup[0].attributes['title'];
+	var titleAttr = specification.title;
 	var title = document.createElement('div');
 	title.className = "title";
 	title.align = "center";
@@ -162,9 +70,9 @@
 	
 	// Set title to that defined in XML, else set to default
 	if (titleAttr != undefined) {
-		titleSpan.innerHTML = titleAttr.value;
+		titleSpan.textContent = titleAttr;
 	} else {
-		titleSpan.innerHTML =  'Listening test';
+		titleSpan.textContent =  'Listening test';
 	}
 	// Insert the titleSpan element into the title div element.
 	title.appendChild(titleSpan);
@@ -176,24 +84,10 @@
 	titleSpan.id = "pageTitle";
 	pagetitle.appendChild(titleSpan);
 	
-	// Store the return URL path in global projectReturn
-	projectReturn = xmlSetup[0].attributes['projectReturn'];
-	if (projectReturn == undefined) {
-		console.log("WARNING - projectReturn not specified! Will assume null.");
-		projectReturn = "null";
-	} else {
-		projectReturn = projectReturn.value;
-	}
-	
 	// Create Interface buttons!
 	var interfaceButtons = document.createElement('div');
 	interfaceButtons.id = 'interface-buttons';
 	
-	// MANUAL DOWNLOAD POINT
-	// If project return is null, this MUST be specified as the location to create the download link
-	var downloadPoint = document.createElement('div');
-	downloadPoint.id = 'download-point';
-	
 	// Create playback start/stop points
 	var playback = document.createElement("button");
 	playback.innerHTML = 'Stop';
@@ -216,7 +110,6 @@
 	// Append the interface buttons into the interfaceButtons object.
 	interfaceButtons.appendChild(playback);
 	interfaceButtons.appendChild(submit);
-	interfaceButtons.appendChild(downloadPoint);
 	
 	// Now create the slider and HTML5 canvas boxes
 	
@@ -254,9 +147,7 @@
 	feedbackHolder.id = 'feedbackHolder';
 	
 	testContent.style.zIndex = 1;
-	insertPoint.innerHTML = null; // Clear the current schema
-	
-	currentState = 'preTest';
+	interfaceContext.insertPoint.innerHTML = null; // Clear the current schema
 	
 	// Inject into HTML
 	testContent.appendChild(title); // Insert the title
@@ -264,7 +155,7 @@
 	testContent.appendChild(interfaceButtons);
 	testContent.appendChild(sliderBox);
 	testContent.appendChild(feedbackHolder);
-	insertPoint.appendChild(testContent);
+	interfaceContext.insertPoint.appendChild(testContent);
 
 	// Load the full interface
 	testState.initialise();
@@ -272,13 +163,13 @@
 	
 }
 
-function loadTest(textXML)
+function loadTest(audioHolderObject)
 {
 	
 	// Reset audioEngineContext.Metric globals for new test
 	audioEngineContext.newTestPage();
 	
-	var id = textXML.id;
+	var id = audioHolderObject.id;
 	
 	var feedbackHolder = document.getElementById('feedbackHolder');
 	var canvas = document.getElementById('slider');
@@ -286,75 +177,49 @@
 	canvas.innerHTML = null;
 	
 	// Setup question title
-	var interfaceObj = $(textXML).find('interface');
-	var titleNode = interfaceObj.find('title');
-	if (titleNode[0] != undefined)
-	{
-		document.getElementById('pageTitle').textContent = titleNode[0].textContent;
-	}
-	var positionScale = canvas.style.width.substr(0,canvas.style.width.length-2);
-	var offset = Number(document.getElementById('slider').attributes['marginsize'].value);
-	var scale = document.getElementById('sliderScaleHolder');
-	scale.innerHTML = null;
-	interfaceObj.find('scale').each(function(index,scaleObj){
-		var value = document.createAttribute('value');
-		var position = Number(scaleObj.attributes['position'].value)*0.01;
-		value.nodeValue = position;
-		var pixelPosition = (position*positionScale)+offset;
-		var scaleDOM = document.createElement('span');
-		scaleDOM.textContent = scaleObj.textContent;
-		scale.appendChild(scaleDOM);
-		scaleDOM.style.left = Math.floor((pixelPosition-($(scaleDOM).width()/2)))+'px';
-		scaleDOM.setAttributeNode(value);
-	});
-	
-	var commentBoxPrefix = interfaceObj.find('commentBoxPrefix');
-	if (commentBoxPrefix.length != 0) {
-		commentBoxPrefix = commentBoxPrefix[0].textContent;
-	} else {
-		commentBoxPrefix = "Comment on track";
+	var interfaceObj = audioHolderObject.interfaces;
+	var commentBoxPrefix = "Comment on track";
+	if (interfaceObj.length != 0) {
+		interfaceObj = interfaceObj[0];
+		var titleNode = interfaceObj.title;
+		if (titleNode != undefined)
+		{
+			document.getElementById('pageTitle').textContent = titleNode;
+		}
+		var positionScale = canvas.style.width.substr(0,canvas.style.width.length-2);
+		var offset = Number(document.getElementById('slider').attributes['marginsize'].value);
+		var scale = document.getElementById('sliderScaleHolder');
+		scale.innerHTML = null;
+		$(interfaceObj.scale).each(function(index,scaleObj){
+			var value = document.createAttribute('value');
+			var position = Number(scaleObj[0])*0.01;
+			value.nodeValue = position;
+			var pixelPosition = (position*positionScale)+offset;
+			var scaleDOM = document.createElement('span');
+			scaleDOM.textContent = scaleObj[1];
+			scale.appendChild(scaleDOM);
+			scaleDOM.style.left = Math.floor((pixelPosition-($(scaleDOM).width()/2)))+'px';
+			scaleDOM.setAttributeNode(value);
+		});
+		
+		if (interfaceObj.commentBoxPrefix != undefined) {
+			commentBoxPrefix = interfaceObj.commentBoxPrefix;
+		}
 	}
 
-	// Extract the hostURL attribute. If not set, create an empty string.
-	var hostURL = textXML.attributes['hostURL'];
-	if (hostURL == undefined) {
-		hostURL = "";
-	} else {
-		hostURL = hostURL.value;
-	}
-	// Extract the sampleRate. If set, convert the string to a Number.
-	var hostFs = textXML.attributes['sampleRate'];
-	if (hostFs != undefined) {
-		hostFs = Number(hostFs.value);
-	}
-	
 	/// CHECK FOR SAMPLE RATE COMPATIBILITY
-	if (hostFs != undefined) {
-		if (Number(hostFs) != audioContext.sampleRate) {
-			var errStr = 'Sample rates do not match! Requested '+Number(hostFs)+', got '+audioContext.sampleRate+'. Please set the sample rate to match before completing this test.';
+	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;
 		}
 	}
 	
-	var commentShow = textXML.attributes['elementComments'];
-	if (commentShow != undefined) {
-		if (commentShow.value == 'false') {commentShow = false;}
-		else {commentShow = true;}
-	} else {commentShow = true;}
+	var commentShow = audioHolderObject.elementComments;
 	
-	var loopPlayback = textXML.attributes['loop'];
-	if (loopPlayback != undefined)
-	{
-		loopPlayback = loopPlayback.value;
-		if (loopPlayback == 'true') {
-			loopPlayback = true;
-		} else {
-			loopPlayback = false;
-		}
-	} else {
-		loopPlayback = false;
-	}
+	var loopPlayback = audioHolderObject.loop;
+
 	audioEngineContext.loopPlayback = loopPlayback;
 	// Create AudioEngine bindings for playback
 	if (loopPlayback) {
@@ -383,117 +248,59 @@
 	}
 	
 	currentTestHolder = document.createElement('audioHolder');
-	currentTestHolder.id = textXML.id;
-	currentTestHolder.repeatCount = textXML.attributes['repeatCount'].value;
+	currentTestHolder.id = audioHolderObject.id;
+	currentTestHolder.repeatCount = audioHolderObject.repeatCount;
 	
-	var randomise = textXML.attributes['randomiseOrder'];
-	if (randomise != undefined) {randomise = randomise.value;}
-	else {randomise = false;}
+	var randomise = audioHolderObject.randomiseOrder;
 	
-	var audioElements = $(textXML).find('audioElements');
+	var audioElements = audioHolderObject.audioElements;
 	currentTrackOrder = [];
-	audioElements.each(function(index,element){
-		// Find any blind-repeats
-		// Not implemented yet, but just in case
-		currentTrackOrder[index] = element;
-	});
 	if (randomise) {
-		currentTrackOrder = randomiseOrder(currentTrackOrder);
+		audioHolderObject.audioElements = randomiseOrder(audioHolderObject.audioElements);
 	}
 	
 	// Delete any previous audioObjects associated with the audioEngine
 	audioEngineContext.audioObjects = [];
 	
 	// Find all the audioElements from the audioHolder
-	$(currentTrackOrder).each(function(index,element){
+	$(audioHolderObject.audioElements).each(function(index,element){
 		// Find URL of track
 		// In this jQuery loop, variable 'this' holds the current audioElement.
 		
 		// Now load each audio sample. First create the new track by passing the full URL
-		var trackURL = hostURL + this.attributes['url'].value;
-		audioEngineContext.newTrack(trackURL);
+		var trackURL = audioHolderObject.hostURL + element.url;
+		var audioObject = audioEngineContext.newTrack(element);
 		
 		if (commentShow) {
-			// Create document objects to hold the comment boxes
-			var trackComment = document.createElement('div');
-			trackComment.className = 'comment-div';
-			trackComment.id = 'comment-div-'+index;
-			// Create a string next to each comment asking for a comment
-			var trackString = document.createElement('span');
-			trackString.innerHTML = commentBoxPrefix+' '+index;
-			// Create the HTML5 comment box 'textarea'
-			var trackCommentBox = document.createElement('textarea');
-			trackCommentBox.rows = '4';
-			trackCommentBox.cols = '100';
-			trackCommentBox.name = 'trackComment'+index;
-			trackCommentBox.className = 'trackComment';
-			var br = document.createElement('br');
-			// Add to the holder.
-			trackComment.appendChild(trackString);
-			trackComment.appendChild(br);
-			trackComment.appendChild(trackCommentBox);
-			feedbackHolder.appendChild(trackComment);
+			var node = interfaceContext.createCommentBox(audioObject);
 		}
 		
 		// Create a slider per track
-		
-		var trackSliderObj = document.createElement('div');
-		trackSliderObj.className = 'track-slider';
-		trackSliderObj.id = 'track-slider-'+index;
-		
-		var trackSliderAOIndex = document.createAttribute('trackIndex');
-		trackSliderAOIndex.nodeValue = index;
-		trackSliderObj.setAttributeNode(trackSliderAOIndex);
+		audioObject.interfaceDOM = new sliderObject(audioObject);
 		
 		// Distribute it randomnly
 		var w = window.innerWidth - (offset+8)*2;
 		w = Math.random()*w;
 		w = Math.floor(w+(offset+8));
-		trackSliderObj.style.left = w+'px';
-		trackSliderObj.innerHTML = '<span>'+index+'</span>';
-		trackSliderObj.draggable = true;
-		trackSliderObj.ondragend = dragEnd;
-		trackSliderObj.ondragstart = function()
-		{
-			var id = Number(event.srcElement.attributes['trackIndex'].value);
-			audioEngineContext.metric.sliderMoveStart(id);
-		};
+		audioObject.interfaceDOM.trackSliderObj.style.left = w+'px';
 		
-		// Onclick, switch playback to that track
-		trackSliderObj.onclick = function() {
-			// Start the test on first click, that way timings are more accurate.
-			audioEngineContext.play();
-			if (audioEngineContext.audioObjectsReady) {
-				// Cannot continue to issue play command until audioObjects reported as ready!
-				// Get the track ID from the object ID
-				var id = Number(event.srcElement.attributes['trackIndex'].value);
-				//audioEngineContext.metric.sliderPlayed(id);
-				audioEngineContext.selectedTrack(id);
-	            // Currently playing track red, rest green
-	            
-	            //document.getElementById('track-slider-'+index).style.backgroundColor = "#FF0000";
-	            $('.track-slider').removeClass('track-slider-playing');
-	            $(event.srcElement).addClass('track-slider-playing');
-	            $('.comment-div').removeClass('comment-box-playing');
-	            $('#comment-div-'+id).addClass('comment-box-playing');
-			}
-		};
-		
-		canvas.appendChild(trackSliderObj);
-		audioEngineContext.audioObjects[index].metric.initialised(convSliderPosToRate(index));
+		canvas.appendChild(audioObject.interfaceDOM.trackSliderObj);
+		audioObject.metric.initialised(convSliderPosToRate(audioObject.interfaceDOM.trackSliderObj));
         
 	});
+	if (commentShow) {
+		interfaceContext.showCommentBoxes(feedbackHolder,true);
+	}
 	
 	// Append any commentQuestion boxes
-	var commentQuestions = $(textXML).find('CommentQuestion');
-	$(commentQuestions).each(function(index,element) {
+	$(audioHolderObject.commentQuestions).each(function(index,element) {
 		// Create document objects to hold the comment boxes
 		var trackComment = document.createElement('div');
 		trackComment.className = 'comment-div commentQuestion';
-		trackComment.id = element.attributes['id'].value;
+		trackComment.id = element.id;
 		// Create a string next to each comment asking for a comment
 		var trackString = document.createElement('span');
-		trackString.innerHTML = element.textContent;
+		trackString.innerHTML = element.question;
 		// Create the HTML5 comment box 'textarea'
 		var trackCommentBox = document.createElement('textarea');
 		trackCommentBox.rows = '4';
@@ -512,6 +319,45 @@
 	testWaitIndicator();
 }
 
+function sliderObject(audioObject) {
+	// Create a new slider object;
+	this.parent = audioObject;
+	this.trackSliderObj = document.createElement('div');
+	this.trackSliderObj.className = 'track-slider';
+	this.trackSliderObj.id = 'track-slider-'+audioObject.id;
+
+	this.trackSliderObj.setAttribute('trackIndex',audioObject.id);
+	this.trackSliderObj.innerHTML = '<span>'+audioObject.id+'</span>';
+	this.trackSliderObj.draggable = true;
+	this.trackSliderObj.ondragend = dragEnd;
+
+	// Onclick, switch playback to that track
+	this.trackSliderObj.onclick = function() {
+		// Start the test on first click, that way timings are more accurate.
+		audioEngineContext.play();
+		if (audioEngineContext.audioObjectsReady) {
+			// Cannot continue to issue play command until audioObjects reported as ready!
+			// Get the track ID from the object ID
+			var id = Number(event.srcElement.attributes['trackIndex'].value);
+			//audioEngineContext.metric.sliderPlayed(id);
+			audioEngineContext.selectedTrack(id);
+            // Currently playing track red, rest green
+            
+            //document.getElementById('track-slider-'+index).style.backgroundColor = "#FF0000";
+            $('.track-slider').removeClass('track-slider-playing');
+            $(event.srcElement).addClass('track-slider-playing');
+            $('.comment-div').removeClass('comment-box-playing');
+            $('#comment-div-'+id).addClass('comment-box-playing');
+		}
+	};
+	
+	this.exportXMLDOM = function() {
+		// Called by the audioObject holding this element. Must be present
+		var node = document.createElement('value');
+		node.textContent = convSliderPosToRate(this.trackSliderObj);
+		return node;
+	};
+}
 
 function dragEnd(ev) {
 	// Function call when a div has been dropped
@@ -529,7 +375,9 @@
 			this.style.left = (w+marginSize) + 'px';
 		}
 	}
-	audioEngineContext.metric.sliderMoved();
+	var time = audioEngineContext.timer.getTestTime();
+	var id = Number(ev.srcElement.getAttribute('trackindex'));
+	audioEngineContext.audioObjects[id].metric.moved(time,convSliderPosToRate(ev.srcElement));
 }
 
 function buttonSubmitClick() // TODO: Only when all songs have been played!
@@ -569,12 +417,11 @@
     }
 }
 
-function convSliderPosToRate(id)
+function convSliderPosToRate(slider)
 {
 	var w = document.getElementById('slider').style.width;
 	var marginsize = Number(document.getElementById('slider').attributes['marginsize'].value);
 	var maxPix = w.substr(0,w.length-2);
-	var slider = document.getElementsByClassName('track-slider')[id];
 	var pix = slider.style.left;
 	pix = pix.substr(0,pix.length-2);
 	var rate = (pix-marginsize)/maxPix;
@@ -614,17 +461,13 @@
 	});
 }
 
-function pageXMLSave(store, testXML, testId)
+function pageXMLSave(store, testXML)
 {
 	// Saves a specific test page
 	var xmlDoc = store;
 	// Check if any session wide metrics are enabled
 	
-	var commentShow = testXML.attributes['elementComments'];
-	if (commentShow != undefined) {
-		if (commentShow.value == 'false') {commentShow = false;}
-		else {commentShow = true;}
-	} else {commentShow = true;}
+	var commentShow = testXML.elementComments;
 	
 	var metric = document.createElement('metric');
 	if (audioEngineContext.metric.enableTestTimer)
@@ -635,89 +478,10 @@
 		metric.appendChild(testTime);
 	}
 	xmlDoc.appendChild(metric);
-	var trackSliderObjects = document.getElementsByClassName('track-slider');
-	var commentObjects = document.getElementsByClassName('comment-div');
-	for (var i=0; i<trackSliderObjects.length; i++) 
+	var audioObjects = audioEngineContext.audioObjects;
+	for (var i=0; i<audioObjects.length; i++) 
 	{
-		var audioElement = document.createElement('audioElement');
-		audioElement.id = currentTrackOrder[i].attributes['id'].value;
-		audioElement.url = currentTrackOrder[i].attributes['url'].value;
-		var value = document.createElement('value');
-		value.innerHTML = convSliderPosToRate(i);
-		if (commentShow) {
-			var comment = document.createElement("comment");
-			var question = document.createElement("question");
-			var response = document.createElement("response");
-			question.textContent = commentObjects[i].children[0].textContent;
-			response.textContent = commentObjects[i].children[2].value;
-            console.log('Comment ' + i + ': ' + commentObjects[i].children[2].value); // DEBUG/SAFETY
-			comment.appendChild(question);
-			comment.appendChild(response);
-			audioElement.appendChild(comment);
-		}
-		audioElement.appendChild(value);
-		// Check for any per element metrics
-		var metric = document.createElement('metric');
-		var elementMetric = audioEngineContext.audioObjects[i].metric;
-		if (audioEngineContext.metric.enableElementTimer) {
-			var elementTimer = document.createElement('metricResult');
-			elementTimer.id = 'elementTimer';
-			elementTimer.textContent = elementMetric.listenedTimer;
-			metric.appendChild(elementTimer);
-		}
-		if (audioEngineContext.metric.enableElementTracker) {
-			var elementTrackerFull = document.createElement('metricResult');
-			elementTrackerFull.id = 'elementTrackerFull';
-			var data = elementMetric.movementTracker;
-			for (var k=0; k<data.length; k++)
-			{
-				var timePos = document.createElement('timePos');
-				timePos.id = k;
-				var time = document.createElement('time');
-				time.textContent = data[k][0];
-				var position = document.createElement('position');
-				position.textContent = data[k][1];
-				timePos.appendChild(time);
-				timePos.appendChild(position);
-				elementTrackerFull.appendChild(timePos);
-			}
-			metric.appendChild(elementTrackerFull);
-		}
-		if (audioEngineContext.metric.enableElementListenTracker) {
-			var elementListenTracker = document.createElement('metricResult');
-			elementListenTracker.id = 'elementListenTracker';
-			var obj = elementMetric.listenTracker;
-			for (var k=0; k<obj.length; k++) {
-				elementListenTracker.appendChild(obj[k]);
-			}
-			metric.appendChild(elementListenTracker);
-		}
-		if (audioEngineContext.metric.enableElementInitialPosition) {
-			var elementInitial = document.createElement('metricResult');
-			elementInitial.id = 'elementInitialPosition';
-			elementInitial.textContent = elementMetric.initialPosition;
-			metric.appendChild(elementInitial);
-		}
-		if (audioEngineContext.metric.enableFlagListenedTo) {
-			var flagListenedTo = document.createElement('metricResult');
-			flagListenedTo.id = 'elementFlagListenedTo';
-			flagListenedTo.textContent = elementMetric.wasListenedTo;
-			metric.appendChild(flagListenedTo);
-		}
-		if (audioEngineContext.metric.enableFlagMoved) {
-			var flagMoved = document.createElement('metricResult');
-			flagMoved.id = 'elementFlagMoved';
-			flagMoved.textContent = elementMetric.wasMoved;
-			metric.appendChild(flagMoved);
-		}
-		if (audioEngineContext.metric.enableFlagComments) {
-			var flagComments = document.createElement('metricResult');
-			flagComments.id = 'elementFlagComments';
-			if (response.textContent.length == 0) {flag.textContent = 'false';}
-			else {flag.textContet = 'true';}
-			metric.appendChild(flagComments);
-		}
-		audioElement.appendChild(metric);
+		var audioElement = audioEngineContext.audioObjects[i].exportXMLDOM();
 		xmlDoc.appendChild(audioElement);
 	}
 	var commentQuestion = document.getElementsByClassName('commentQuestion');
--- a/core.js	Thu Jun 04 10:06:42 2015 +0100
+++ b/core.js	Fri Jun 05 12:54:52 2015 +0100
@@ -8,9 +8,10 @@
 /* create the web audio API context and store in audioContext*/
 var audioContext; // Hold the browser web audio API
 var projectXML; // Hold the parsed setup XML
+var specification;
+var interfaceContext;
 var popup; // Hold the interfacePopup object
 var testState;
-var currentState; // Keep track of the current state (pre/post test, which test, final test? first test?)
 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
 var audioEngineContext; // The custome AudioEngine object
 var projectReturn; // Hold the URL for the return
@@ -36,6 +37,12 @@
 	
 	// Create the popup interface object
 	popup = new interfacePopup();
+	
+	// Create the specification object
+	specification = new Specification();
+	
+	// Create the interface object
+	interfaceContext = new Interface(specification);
 };
 
 function interfacePopup() {
@@ -46,6 +53,7 @@
 	this.popupOptions = null;
 	this.currentIndex = null;
 	this.responses = null;
+	
 	this.createPopup = function(){
 		// Create popup window interface
 		var insertPoint = document.getElementById("topLevelBody");
@@ -69,12 +77,16 @@
 		this.popupButton.className = 'popupButton';
 		this.popupButton.innerHTML = 'Next';
 		this.popupButton.onclick = function(){popup.buttonClicked();};
+		this.popup.style.zIndex = -1;
+		this.popup.style.visibility = 'hidden';
+		blank.style.zIndex = -2;
+		blank.style.visibility = 'hidden';
 		insertPoint.appendChild(this.popup);
 		insertPoint.appendChild(blank);
 	};
 	
 	this.showPopup = function(){
-		if (this.popup == null || this.popup == undefined) {
+		if (this.popup == null) {
 			this.createPopup();
 		}
 		this.popup.style.zIndex = 3;
@@ -96,19 +108,67 @@
 		// This will take the node from the popupOptions and display it
 		var node = this.popupOptions[this.currentIndex];
 		this.popupContent.innerHTML = null;
-		if (node.nodeName == 'statement') {
+		if (node.type == 'statement') {
 			var span = document.createElement('span');
-			span.textContent = node.textContent;
+			span.textContent = node.statement;
 			this.popupContent.appendChild(span);
-		} else if (node.nodeName == 'question') {
+		} else if (node.type == 'question') {
 			var span = document.createElement('span');
-			span.textContent = node.textContent;
+			span.textContent = node.question;
 			var textArea = document.createElement('textarea');
 			var br = document.createElement('br');
 			this.popupContent.appendChild(span);
 			this.popupContent.appendChild(br);
 			this.popupContent.appendChild(textArea);
 			this.popupContent.childNodes[2].focus();
+		} else if (node.type == 'checkbox') {
+			var span = document.createElement('span');
+			span.textContent = node.statement;
+			this.popupContent.appendChild(span);
+			var optHold = document.createElement('div');
+			optHold.id = 'option-holder';
+			optHold.align = 'left';
+			for (var i=0; i<node.options.length; i++) {
+				var option = node.options[i];
+				var input = document.createElement('input');
+				input.id = option.id;
+				input.type = 'checkbox';
+				var span = document.createElement('span');
+				span.textContent = option.text;
+				var hold = document.createElement('div');
+				hold.setAttribute('name','option');
+				hold.style.float = 'left';
+				hold.style.padding = '4px';
+				hold.appendChild(input);
+				hold.appendChild(span);
+				optHold.appendChild(hold);
+			}
+			this.popupContent.appendChild(optHold);
+		} else if (node.type == 'radio') {
+			var span = document.createElement('span');
+			span.textContent = node.statement;
+			this.popupContent.appendChild(span);
+			var optHold = document.createElement('div');
+			optHold.id = 'option-holder';
+			optHold.align = 'none';
+			optHold.style.float = 'left';
+			optHold.style.width = "100%";
+			for (var i=0; i<node.options.length; i++) {
+				var option = node.options[i];
+				var input = document.createElement('input');
+				input.id = option.name;
+				input.type = 'radio';
+				input.name = node.id;
+				var span = document.createElement('span');
+				span.textContent = option.text;
+				var hold = document.createElement('div');
+				hold.setAttribute('name','option');
+				hold.style.padding = '4px';
+				hold.appendChild(input);
+				hold.appendChild(span);
+				optHold.appendChild(hold);
+			}
+			this.popupContent.appendChild(optHold);
 		}
 		this.popupContent.appendChild(this.popupButton);
 	};
@@ -116,11 +176,11 @@
 	this.initState = function(node) {
 		//Call this with your preTest and postTest nodes when needed to
 		// initialise the popup procedure.
-		this.popupOptions = $(node).children();
+		this.popupOptions = node.options;
 		if (this.popupOptions.length > 0) {
-			if (node.nodeName == 'preTest' || node.nodeName == 'PreTest') {
+			if (node.type == 'pretest') {
 				this.responses = document.createElement('PreTest');
-			} else if (node.nodeName == 'postTest' || node.nodeName == 'PostTest') {
+			} else if (node.type == 'posttest') {
 				this.responses = document.createElement('PostTest');
 			} else {
 				console.log ('WARNING - popup node neither pre or post!');
@@ -129,34 +189,61 @@
 			this.currentIndex = 0;
 			this.showPopup();
 			this.postNode();
+		} else {
+			advanceState();
 		}
 	};
 	
 	this.buttonClicked = function() {
 		// Each time the popup button is clicked!
 		var node = this.popupOptions[this.currentIndex];
-		if (node.nodeName == 'question') {
+		if (node.type == 'question') {
 			// Must extract the question data
-			var mandatory = node.attributes['mandatory'];
-			if (mandatory == undefined) {
-				mandatory = false;
-			} else {
-				if (mandatory.value == 'true'){mandatory = true;}
-				else {mandatory = false;}
-			}
 			var textArea = $(popup.popupContent).find('textarea')[0];
-			if (mandatory == true && textArea.value.length == 0) {
+			if (node.mandatory == true && textArea.value.length == 0) {
 				alert('This question is mandatory');
 				return;
 			} else {
 				// Save the text content
 				var hold = document.createElement('comment');
-				hold.id = node.attributes['id'].value;
+				hold.id = node.id;
 				hold.innerHTML = textArea.value;
 				console.log("Question: "+ node.textContent);
 				console.log("Question Response: "+ textArea.value);
 				this.responses.appendChild(hold);
 			}
+		} else if (node.type == 'checkbox') {
+			// Must extract checkbox data
+			var optHold = document.getElementById('option-holder');
+			var hold = document.createElement('checkbox');
+			console.log("Checkbox: "+ node.statement);
+			hold.id = node.id;
+			for (var i=0; i<optHold.childElementCount; i++) {
+				var input = optHold.childNodes[i].getElementsByTagName('input')[0];
+				var statement = optHold.childNodes[i].getElementsByTagName('span')[0];
+				var response = document.createElement('option');
+				response.setAttribute('id',input.id);
+				response.setAttribute('checked',input.checked);
+				hold.appendChild(response);
+				console.log(input.id +': '+ input.checked);
+			}
+			this.responses.appendChild(hold);
+		} else if (node.type == "radio") {
+			var optHold = document.getElementById('option-holder');
+			var hold = document.createElement('radio');
+			var responseID = null;
+			var i=0;
+			while(responseID == null) {
+				var input = optHold.childNodes[i].getElementsByTagName('input')[0];
+				if (input.checked == true) {
+					responseID = i;
+				}
+				i++;
+			}
+			hold.id = node.id;
+			hold.setAttribute('name',node.options[responseID].name);
+			hold.textContent = node.options[responseID].text;
+			this.responses.appendChild(hold);
 		}
 		this.currentIndex++;
 		if (this.currentIndex < this.popupOptions.length) {
@@ -198,9 +285,9 @@
 			this.stateIndex = -1;
 			var that = this;
 			for (var id=0; id<this.stateMap.length; id++){
-				var name = this.stateMap[id].nodeName;
+				var name = this.stateMap[id].type;
 				var obj = document.createElement(name);
-				if (name == "audioHolder") {
+				if (name == 'audioHolder') {
 					obj.id = this.stateMap[id].id;
 				}
 				this.stateResults.push(obj);
@@ -217,7 +304,7 @@
 			console.log('Starting test...');
 		}
 		if (this.currentIndex == null){
-			if (this.currentStateMap.nodeName == "audioHolder") {
+			if (this.currentStateMap.type == "audioHolder") {
 				// Save current page
 				this.testPageCompleted(this.stateResults[this.stateIndex],this.currentStateMap,this.currentTestId);
 				this.currentTestId++;
@@ -225,15 +312,15 @@
 			this.stateIndex++;
 			if (this.stateIndex >= this.stateMap.length) {
 				console.log('Test Completed');
-				createProjectSave(projectReturn);
+				createProjectSave(specification.projectReturn);
 			} else {
 				this.currentStateMap = this.stateMap[this.stateIndex];
-				if (this.currentStateMap.nodeName == "audioHolder") {
+				if (this.currentStateMap.type == "audioHolder") {
 					console.log('Loading test page');
 					loadTest(this.currentStateMap);
 					this.initialiseInnerState(this.currentStateMap);
-				} else if (this.currentStateMap.nodeName == "PreTest" || this.currentStateMap.nodeName == "PostTest") {
-					if (this.currentStateMap.childElementCount >= 1) {
+				} else if (this.currentStateMap.type == "pretest" || this.currentStateMap.type == "posttest") {
+					if (this.currentStateMap.options.length >= 1) {
 						popup.initState(this.currentStateMap);
 					} else {
 						this.advanceState();
@@ -251,22 +338,22 @@
 		// Function called each time a test page has been completed
 		// Can be used to over-rule default behaviour
 		
-		pageXMLSave(store, testXML, testId);
-	}
+		pageXMLSave(store, testXML);
+	};
 	
-	this.initialiseInnerState = function(testXML) {
+	this.initialiseInnerState = function(node) {
 		// Parses the received testXML for pre and post test options
 		this.currentStateMap = [];
-		var preTest = $(testXML).find('PreTest')[0];
-		var postTest = $(testXML).find('PostTest')[0];
+		var preTest = node.preTest;
+		var postTest = node.postTest;
 		if (preTest == undefined) {preTest = document.createElement("preTest");}
 		if (postTest == undefined){postTest= document.createElement("postTest");}
 		this.currentStateMap.push(preTest);
-		this.currentStateMap.push(testXML);
+		this.currentStateMap.push(node);
 		this.currentStateMap.push(postTest);
 		this.currentIndex = -1;
 		this.advanceInnerState();
-	}
+	};
 	
 	this.advanceInnerState = function() {
 		this.currentIndex++;
@@ -275,17 +362,17 @@
 			this.currentStateMap = this.stateMap[this.stateIndex];
 			this.advanceState();
 		} else {
-			if (this.currentStateMap[this.currentIndex].nodeName == "audioHolder") {
+			if (this.currentStateMap[this.currentIndex].type == "audioHolder") {
 				console.log("Loading test page"+this.currentTestId);
-			} else if (this.currentStateMap[this.currentIndex].nodeName == "PreTest") {
+			} else if (this.currentStateMap[this.currentIndex].type == "pretest") {
 				popup.initState(this.currentStateMap[this.currentIndex]);
-			} else if (this.currentStateMap[this.currentIndex].nodeName == "PostTest") {
+			} else if (this.currentStateMap[this.currentIndex].type == "posttest") {
 				popup.initState(this.currentStateMap[this.currentIndex]);
 			} else {
 				this.advanceInnerState();
 			}
 		}
-	}
+	};
 	
 	this.previousState = function(){};
 }
@@ -322,16 +409,67 @@
 
 function loadProjectSpecCallback(response) {
 	// Function called after asynchronous download of XML project specification
-	var decode = $.parseXML(response);
-	projectXML = $(decode);
+	//var decode = $.parseXML(response);
+	//projectXML = $(decode);
 	
-	// Now extract the setup tag
-	var xmlSetup = projectXML.find('setup');
+	var parse = new DOMParser();
+	projectXML = parse.parseFromString(response,'text/xml');
+	
+	// Build the specification
+	specification.decode();
+	
+	testState.stateMap.push(specification.preTest);
+	 
+	// New check if we need to randomise the test order
+	if (specification.randomiseOrder)
+	{
+ 		specification.audioHolders = randomiseOrder(specification.audioHolders);
+	}
+	
+	$(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 interfaceType = xmlSetup[0].attributes['interface'];
 	var interfaceJS = document.createElement('script');
 	interfaceJS.setAttribute("type","text/javascript");
-	if (interfaceType.value == 'APE') {
+	if (specification.interfaceType == 'APE') {
 		interfaceJS.setAttribute("src","ape.js");
 		
 		// APE comes with a css file
@@ -365,11 +503,9 @@
 		a.download = "save.xml";
 		a.textContent = "Save File";
 		
-		var submitDiv = document.getElementById('download-point');
-		submitDiv.appendChild(a);
 		popup.showPopup();
 		popup.popupContent.innerHTML = null;
-		popup.popupContent.appendChild(submitDiv)
+		popup.popupContent.appendChild(a);
 	} else {
 		var xmlhttp = new XMLHttpRequest;
 		xmlhttp.open("POST",destURL,true);
@@ -386,7 +522,6 @@
 		};
 		xmlhttp.send(file);
 	}
-	return submitDiv;
 }
 
 // Only other global function which must be defined in the interface class. Determines how to create the XML document.
@@ -461,7 +596,7 @@
 	};
 	
 	
-	this.newTrack = function(url) {
+	this.newTrack = function(element) {
 		// Pull data from given URL into new audio buffer
 		// URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
 		
@@ -470,7 +605,9 @@
 		this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
 
 		// AudioObject will get track itself.
-		this.audioObjects[audioObjectId].constructTrack(url);
+		this.audioObjects[audioObjectId].specification = element;
+		this.audioObjects[audioObjectId].constructTrack(element.parent.hostURL + element.url);
+		return this.audioObjects[audioObjectId];
 	};
 	
 	this.newTestPage = function() {
@@ -507,11 +644,16 @@
 function audioObject(id) {
 	// The main buffer object with common control nodes to the AudioEngine
 	
+	this.specification;
 	this.id = id;
 	this.state = 0; // 0 - no data, 1 - ready
 	this.url = null; // Hold the URL given for the output back to the results.
 	this.metric = new metricTracker(this);
 	
+	// Bindings for GUI
+	this.interfaceDOM = null;
+	this.commentDOM = null;
+	
 	// Create a buffer and external gain control to allow internal patching of effects and volume leveling.
 	this.bufferNode = undefined;
 	this.outputGain = audioContext.createGain();
@@ -529,14 +671,14 @@
 	this.loopStart = function() {
 		this.outputGain.gain.value = 1.0;
 		this.metric.startListening(audioEngineContext.timer.getTestTime());
-	}
+	};
 	
 	this.loopStop = function() {
 		if (this.outputGain.gain.value != 0.0) {
 			this.outputGain.gain.value = 0.0;
 			this.metric.stopListening(audioEngineContext.timer.getTestTime());
 		}
-	}
+	};
 	
 	this.play = function(startTime) {
 		this.bufferNode = audioContext.createBufferSource();
@@ -609,6 +751,15 @@
 		request.send();
 	};
 	
+	this.exportXMLDOM = function() {
+		var root = document.createElement('audioElement');
+		root.id = this.specification.id;
+		root.setAttribute('url',this.url);
+		root.appendChild(this.interfaceDOM.exportXMLDOM());
+		root.appendChild(this.commentDOM.exportXMLDOM());
+		root.appendChild(this.metric.exportXMLDOM());
+		return root;
+	};
 }
 
 function timer()
@@ -736,6 +887,72 @@
 			console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
 		}
 	};
+	
+	this.exportXMLDOM = function() {
+		var root = document.createElement('metric');
+		if (audioEngineContext.metric.enableElementTimer) {
+			var mElementTimer = document.createElement('metricresult');
+			mElementTimer.setAttribute('name','enableElementTimer');
+			mElementTimer.textContent = this.listenedTimer;
+			root.appendChild(mElementTimer);
+		}
+		if (audioEngineContext.metric.enableElementTracker) {
+			var elementTrackerFull = document.createElement('metricResult');
+			elementTrackerFull.setAttribute('name','elementTrackerFull');
+			for (var k=0; k<this.movementTracker.length; k++)
+			{
+				var timePos = document.createElement('timePos');
+				timePos.id = k;
+				var time = document.createElement('time');
+				time.textContent = this.movementTracker[k][0];
+				var position = document.createElement('position');
+				position.textContent = this.movementTracker[k][1];
+				timePos.appendChild(time);
+				timePos.appendChild(position);
+				elementTrackerFull.appendChild(timePos);
+			}
+			root.appendChild(elementTrackerFull);
+		}
+		if (audioEngineContext.metric.enableElementListenTracker) {
+			var elementListenTracker = document.createElement('metricResult');
+			elementListenTracker.setAttribute('name','elementListenTracker');
+			for (var k=0; k<this.listenTracker.length; k++) {
+				elementListenTracker.appendChild(this.listenTracker[k]);
+			}
+			root.appendChild(elementListenTracker);
+		}
+		if (audioEngineContext.metric.enableElementInitialPosition) {
+			var elementInitial = document.createElement('metricResult');
+			elementInitial.setAttribute('name','elementInitialPosition');
+			elementInitial.textContent = this.initialPosition;
+			root.appendChild(elementInitial);
+		}
+		if (audioEngineContext.metric.enableFlagListenedTo) {
+			var flagListenedTo = document.createElement('metricResult');
+			flagListenedTo.setAttribute('name','elementFlagListenedTo');
+			flagListenedTo.textContent = this.wasListenedTo;
+			root.appendChild(flagListenedTo);
+		}
+		if (audioEngineContext.metric.enableFlagMoved) {
+			var flagMoved = document.createElement('metricResult');
+			flagMoved.setAttribute('name','elementFlagMoved');
+			flagMoved.textContent = this.wasMoved;
+			root.appendChild(flagMoved);
+		}
+		if (audioEngineContext.metric.enableFlagComments) {
+			var flagComments = document.createElement('metricResult');
+			flagComments.setAttribute('name','elementFlagComments');
+			if (this.parent.commentDOM == null)
+				{flag.textContent = 'false';}
+			else if (this.parent.commentDOM.textContent.length == 0) 
+				{flag.textContent = 'false';}
+			else 
+				{flag.textContet = 'true';}
+			root.appendChild(flagComments);
+		}
+		
+		return root;
+	};
 }
 
 function randomiseOrder(input)
@@ -803,17 +1020,24 @@
 		var hold = document.createElement("div");
 		hold.id = "testWaitIndicator";
 		hold.className = "indicator-box";
+		hold.style.zIndex = 3;
 		var span = document.createElement("span");
 		span.textContent = "Please wait! Elements still loading";
 		hold.appendChild(span);
+		var blank = document.createElement('div');
+		blank.className = 'testHalt';
+		blank.id = "testHaltBlank";
 		var body = document.getElementsByTagName('body')[0];
 		body.appendChild(hold);
+		body.appendChild(blank);
 		testWaitTimerIntervalHolder = setInterval(function(){
 			var ready = audioEngineContext.checkAllReady();
 			if (ready) {
 				var elem = document.getElementById('testWaitIndicator');
+				var blank = document.getElementById('testHaltBlank');
 				var body = document.getElementsByTagName('body')[0];
 				body.removeChild(elem);
+				body.removeChild(blank);
 				clearInterval(testWaitTimerIntervalHolder);
 			}
 		},500,false);
@@ -821,3 +1045,253 @@
 }
 
 var testWaitTimerIntervalHolder = null;
+
+function Specification() {
+	// Handles the decoding of the project specification XML into a simple JavaScript Object.
+	
+	this.interfaceType;
+	this.projectReturn;
+	this.randomiseOrder;
+	this.collectMetrics;
+	this.preTest;
+	this.postTest;
+	this.metrics =[];
+	
+	this.audioHolders = [];
+	
+	this.decode = function() {
+		// projectXML - DOM Parsed document
+		var setupNode = projectXML.getElementsByTagName('setup')[0];
+		this.interfaceType = setupNode.getAttribute('interface');
+		this.projectReturn = setupNode.getAttribute('projectReturn');
+		if (setupNode.getAttribute('randomiseOrder') == "true") {
+			this.randomiseOrder = true;
+		} else {this.randomiseOrder = false;}
+		if (setupNode.getAttribute('collectMetrics') == "true") {
+			this.collectMetrics = true;
+		} else {this.collectMetrics = false;}
+		var metricCollection = setupNode.getElementsByTagName('Metric');
+		
+		this.preTest = new this.prepostNode('pretest',setupNode.getElementsByTagName('PreTest'));
+		this.postTest = new this.prepostNode('posttest',setupNode.getElementsByTagName('PostTest'));
+		
+		if (metricCollection.length > 0) {
+			metricCollection = metricCollection[0].getElementsByTagName('metricEnable');
+			for (var i=0; i<metricCollection.length; i++) {
+				this.metrics.push(new this.metricNode(metricCollection[i].textContent));
+			}
+		}
+		
+		var audioHolders = projectXML.getElementsByTagName('audioHolder');
+		for (var i=0; i<audioHolders.length; i++) {
+			this.audioHolders.push(new this.audioHolderNode(this,audioHolders[i]));
+		}
+		
+	};
+	
+	this.prepostNode = function(type,Collection) {
+		this.type = type;
+		this.options = [];
+		
+		this.OptionNode = function(child) {
+			
+			this.childOption = function(element) {
+				this.type = 'option';
+				this.id = element.id;
+				this.name = element.getAttribute('name');
+				this.text = element.textContent;
+			}
+			
+			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;
+			} 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;
+					}
+				}
+			}
+		};
+		
+		// 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.metricNode = function(name) {
+		this.enabled = name;
+	};
+	
+	this.audioHolderNode = function(parent,xml) {
+		this.type = 'audioHolder';
+		this.interfaceNode = function(DOM) {
+			var title = DOM.getElementsByTagName('title');
+			if (title.length == 0) {this.title = null;}
+			else {this.title = title[0].textContent;}
+			
+			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.audioElementNode = function(parent,xml) {
+			this.url = xml.getAttribute('url');
+			this.id = xml.id;
+			this.parent = parent;
+		};
+		
+		this.commentQuestionNode = function(xml) {
+			this.id = xml.id;
+			if (xml.getAttribute('mandatory') == 'true') {this.mandatory = true;}
+			else {this.mandatory = false;}
+			this.question = xml.textContent;
+		};
+		
+		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');
+		for (var i=0; i<audioElementsDOM.length; i++) {
+			this.audioElements.push(new this.audioElementNode(this,audioElementsDOM[i]));
+		}
+		
+		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");
+	
+	// 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.
+	this.interfaceObjects = [];
+	this.interfaceObject = function(){};
+	
+	this.commentBoxes = [];
+	this.commentBox = function(audioObject) {
+		var element = audioObject.specification;
+		this.audioObject = audioObject;
+		this.id = audioObject.id;
+		var audioHolderObject = audioObject.specification.parent;
+		// Create document objects to hold the comment boxes
+		this.trackComment = document.createElement('div');
+		this.trackComment.className = 'comment-div';
+		this.trackComment.id = 'comment-div-'+audioObject.id;
+		// Create a string next to each comment asking for a comment
+		this.trackString = document.createElement('span');
+		this.trackString.innerHTML = audioHolderObject.commentBoxPrefix+' '+audioObject.id;
+		// Create the HTML5 comment box 'textarea'
+		this.trackCommentBox = document.createElement('textarea');
+		this.trackCommentBox.rows = '4';
+		this.trackCommentBox.cols = '100';
+		this.trackCommentBox.name = 'trackComment'+audioObject.id;
+		this.trackCommentBox.className = 'trackComment';
+		var br = document.createElement('br');
+		// Add to the holder.
+		this.trackComment.appendChild(this.trackString);
+		this.trackComment.appendChild(br);
+		this.trackComment.appendChild(this.trackCommentBox);
+		
+		this.exportXMLDOM = function() {
+			var root = document.createElement('comment');
+			if (this.audioObject.specification.parent.elementComments) {
+				var question = document.createElement('question');
+				question.textContent = this.trackString.textContent;
+				var response = document.createElement('response');
+				response.textContent = this.trackCommentBox.value;
+				root.appendChild(question);
+				root.appendChild(response);
+			}
+			return root;
+		};
+	};
+	
+	this.createCommentBox = function(audioObject) {
+		var node = new this.commentBox(audioObject);
+		this.commentBoxes.push(node);
+		audioObject.commentDOM = node;
+		return node;
+	};
+	
+	this.sortCommentBoxes = function() {
+		var holder = [];
+		while (this.commentBoxes.length > 0) {
+			var node = this.commentBoxes.pop(0);
+			holder[node.id] = node;
+		}
+		this.commentBoxes = holder;
+	};
+	
+	this.showCommentBoxes = function(inject, sort) {
+		if (sort) {interfaceContext.sortCommentBoxes();}
+		for (var i=0; i<interfaceContext.commentBoxes.length; i++) {
+			inject.appendChild(this.commentBoxes[i].trackComment);
+		}
+	};
+}
+
--- a/example_eval/project.xml	Thu Jun 04 10:06:42 2015 +0100
+++ b/example_eval/project.xml	Fri Jun 05 12:54:52 2015 +0100
@@ -3,6 +3,21 @@
 	<setup interface="APE" projectReturn="/save" randomiseOrder='true' collectMetrics='true'>
 		<PreTest>
 			<question id="Location" mandatory="true">Please enter your location.</question>
+			<checkbox id="experience">
+				<statement>Check options which are relevant to you</statement>
+				<option id="digital">Digital Consoles</option>
+				<option id="analog">Analog Consoles</option>
+				<option id="live">Live Mixing</option>
+				<option id="studio">Studio Mixing</option>
+				<option id="player">Play an instrument</option>
+			</checkbox>
+			<radio id="rating">
+				<statement>Please rate this interface</statement>
+				<option name="bad">Bad</option>
+				<option name="OK">OK</option>
+				<option name="Good">Good</option>
+				<option name="Great">Great</option>
+			</radio>
 			<statement>Please listen to all mixes</statement>
 		</PreTest>
 		<PostTest>
@@ -32,19 +47,15 @@
 		<audioElements url="1.wav" id="1"/>
 		<audioElements url="2.wav" id="2"/>
 		<audioElements url="3.wav" id="3"/>
-		<audioElements url="4.wav" id="4"/>
+		<!--<audioElements url="4.wav" id="4"/>
 		<audioElements url="5.wav" id="5"/>
-		<!--<audioElements url="6.wav" id="6"/>
+		<audioElements url="6.wav" id="6"/>
 		<audioElements url="7.wav" id="7"/>
 		<audioElements url="8.wav" id="8"/>
 		<audioElements url="9.wav" id="9"/>
 		<audioElements url="10.wav" id="10"/>-->
 		<CommentQuestion id='mixingExperience'>What is your mixing experience</CommentQuestion>
-		<!--
-		<PreTest>
-			<statement>Start the Test 3</statement>
-		</PreTest>
-		-->
+		<PreTest/>
 		<PostTest>
 			<question id="genre" mandatory="true">Please enter the genre</question>
 		</PostTest>