Mercurial > hg > webaudioevaluationtool
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