Mercurial > hg > webaudioevaluationtool
diff core.js @ 749:07c996307cbd
Bug #1510 Fixed.
author | Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk> |
---|---|
date | Mon, 21 Dec 2015 11:53:05 +0000 |
parents | |
children | 3b8069ea47d2 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/core.js Mon Dec 21 11:53:05 2015 +0000 @@ -0,0 +1,2915 @@ +/** + * core.js + * + * Main script to run, calls all other core functions and manages loading/store to backend. + * Also contains all global variables. + */ + +/* 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 currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order +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; +// Add a prototype to the bufferNode to hold the desired LINEAR gain +AudioBuffer.prototype.gain = undefined; +// Add a prototype to the bufferNode to hold the computed LUFS loudness +AudioBuffer.prototype.lufs = undefined; + +window.onload = function() { + // Function called once the browser has loaded all files. + // This should perform any initial commands such as structure / loading documents + + // Create a web audio API context + // Fixed for cross-browser support + var AudioContext = window.AudioContext || window.webkitAudioContext; + audioContext = new AudioContext; + + // Create test state + testState = new stateMachine(); + + // Create the popup interface object + popup = new interfacePopup(); + + // Create the specification object + specification = new Specification(); + + // Create the interface object + interfaceContext = new Interface(specification); + // Define window callbacks for interface + window.onresize = function(event){interfaceContext.resizeWindow(event);}; +}; + +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; + this.popupContent = null; + this.popupTitle = null; + this.popupResponse = null; + this.buttonProceed = null; + this.buttonPrevious = null; + 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 + var insertPoint = document.getElementById("topLevelBody"); + var blank = document.createElement('div'); + blank.className = 'testHalt'; + + this.popup = document.createElement('div'); + this.popup.id = 'popupHolder'; + this.popup.className = 'popupHolder'; + this.popup.style.position = 'absolute'; + this.popup.style.left = (window.innerWidth/2)-250 + 'px'; + this.popup.style.top = (window.innerHeight/2)-125 + 'px'; + + this.popupContent = document.createElement('div'); + this.popupContent.id = 'popupContent'; + this.popupContent.style.marginTop = '20px'; + this.popupContent.style.marginBottom = '5px'; + this.popup.appendChild(this.popupContent); + + var titleHolder = document.createElement('div'); + titleHolder.id = 'popupTitleHolder'; + titleHolder.align = 'center'; + titleHolder.style.width = 'inherit'; + titleHolder.style.minHeight = '25px'; + titleHolder.style.maxHeight = '250px'; + titleHolder.style.overflow = 'auto'; + titleHolder.style.marginBottom = '5px'; + + this.popupTitle = document.createElement('span'); + this.popupTitle.id = 'popupTitle'; + titleHolder.appendChild(this.popupTitle); + this.popupContent.appendChild(titleHolder); + + this.popupResponse = document.createElement('div'); + this.popupResponse.id = 'popupResponse'; + this.popupResponse.align = 'center'; + this.popupResponse.style.width = 'inherit'; + this.popupResponse.style.minHeight = '50px'; + this.popupResponse.style.maxHeight = '320px'; + this.popupResponse.style.overflow = 'auto'; + this.popupContent.appendChild(this.popupResponse); + + this.buttonProceed = document.createElement('button'); + this.buttonProceed.className = 'popupButton'; + this.buttonProceed.position = 'relative'; + this.buttonProceed.style.left = '390px'; + this.buttonProceed.innerHTML = 'Next'; + this.buttonProceed.onclick = function(){popup.proceedClicked();}; + + this.buttonPrevious = document.createElement('button'); + this.buttonPrevious.className = 'popupButton'; + this.buttonPrevious.position = 'relative'; + this.buttonPrevious.style.left = '10px'; + this.buttonPrevious.innerHTML = 'Back'; + this.buttonPrevious.onclick = function(){popup.previousClick();}; + + this.popupContent.appendChild(this.buttonPrevious); + this.popupContent.appendChild(this.buttonProceed); + + 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.createPopup(); + } + this.popup.style.zIndex = 3; + this.popup.style.visibility = 'visible'; + var blank = document.getElementsByClassName('testHalt')[0]; + blank.style.zIndex = 2; + blank.style.visibility = 'visible'; + }; + + this.hidePopup = function(){ + this.popup.style.zIndex = -1; + this.popup.style.visibility = 'hidden'; + var blank = document.getElementsByClassName('testHalt')[0]; + blank.style.zIndex = -2; + blank.style.visibility = 'hidden'; + this.buttonPrevious.style.visibility = 'inherit'; + }; + + this.postNode = function() { + // This will take the node from the popupOptions and display it + var node = this.popupOptions[this.currentIndex]; + this.popupResponse.innerHTML = null; + if (node.type == 'statement') { + this.popupTitle.textContent = null; + var statement = document.createElement('span'); + statement.textContent = node.statement; + this.popupResponse.appendChild(statement); + } else if (node.type == 'question') { + this.popupTitle.textContent = node.question; + var textArea = document.createElement('textarea'); + switch (node.boxsize) { + case 'small': + textArea.cols = "20"; + textArea.rows = "1"; + break; + case 'normal': + textArea.cols = "30"; + textArea.rows = "2"; + break; + case 'large': + textArea.cols = "40"; + textArea.rows = "5"; + break; + case 'huge': + textArea.cols = "50"; + textArea.rows = "10"; + break; + } + this.popupResponse.appendChild(textArea); + textArea.focus(); + } else if (node.type == 'checkbox') { + this.popupTitle.textContent = node.statement; + var optHold = this.popupResponse; + 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.padding = '4px'; + hold.appendChild(input); + hold.appendChild(span); + optHold.appendChild(hold); + } + } else if (node.type == 'radio') { + this.popupTitle.textContent = node.statement; + var optHold = this.popupResponse; + 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); + } + } else if (node.type == 'number') { + this.popupTitle.textContent = node.statement; + var input = document.createElement('input'); + input.type = 'textarea'; + if (node.min != null) {input.min = node.min;} + if (node.max != null) {input.max = node.max;} + if (node.step != null) {input.step = node.step;} + this.popupResponse.appendChild(input); + } + var content_height = Number(this.popup.offsetHeight.toFixed()); + content_height -= Number(this.popupContent.offsetHeight.toFixed()); + content_height -=Number(this.buttonProceed.offsetHeight.toFixed()); + content_height = content_height + "px"; + this.buttonProceed.style.top = content_height; + this.buttonPrevious.style.top = content_height; + if(this.currentIndex+1 == this.popupOptions.length) { + if (this.responses.nodeName == "PRETEST") { + this.buttonProceed.textContent = 'Start'; + } else { + this.buttonProceed.textContent = 'Submit'; + } + } else { + this.buttonProceed.textContent = 'Next'; + } + if(this.currentIndex > 0) + this.buttonPrevious.style.visibility = 'visible'; + else + this.buttonPrevious.style.visibility = 'hidden'; + }; + + this.initState = function(node) { + //Call this with your preTest and postTest nodes when needed to + // initialise the popup procedure. + this.popupOptions = node.options; + if (this.popupOptions.length > 0) { + if (node.type == 'pretest') { + this.responses = document.createElement('PreTest'); + } else if (node.type == 'posttest') { + this.responses = document.createElement('PostTest'); + } else { + console.log ('WARNING - popup node neither pre or post!'); + this.responses = document.createElement('responses'); + } + this.currentIndex = 0; + this.showPopup(); + this.postNode(); + } else { + advanceState(); + } + }; + + this.proceedClicked = function() { + // Each time the popup button is clicked! + var node = this.popupOptions[this.currentIndex]; + if (node.type == 'question') { + // Must extract the question data + var textArea = $(popup.popupContent).find('textarea')[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.id; + hold.innerHTML = textArea.value; + console.log("Question: "+ node.question); + console.log("Question Response: "+ textArea.value); + this.responses.appendChild(hold); + } + } else if (node.type == 'checkbox') { + // Must extract checkbox data + var optHold = this.popupResponse; + 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('name',input.id); + response.textContent = input.checked; + hold.appendChild(response); + console.log(input.id +': '+ input.checked); + } + this.responses.appendChild(hold); + } 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++; + } + hold.id = node.id; + hold.setAttribute('name',node.options[responseID].name); + hold.textContent = node.options[responseID].text; + this.responses.appendChild(hold); + } else if (node.type == "number") { + var input = this.popupContent.getElementsByTagName('input')[0]; + if (node.mandatory == true && input.value.length == 0) { + alert('This question is mandatory. Please enter a number'); + return; + } + var enteredNumber = Number(input.value); + if (isNaN(enteredNumber)) { + alert('Please enter a valid number'); + return; + } + if (enteredNumber < node.min && node.min != null) { + alert('Number is below the minimum value of '+node.min); + return; + } + if (enteredNumber > node.max && node.max != null) { + alert('Number is above the maximum value of '+node.max); + return; + } + var hold = document.createElement('number'); + hold.id = node.id; + hold.textContent = input.value; + this.responses.appendChild(hold); + } + this.currentIndex++; + if (this.currentIndex < this.popupOptions.length) { + this.postNode(); + } else { + // Reached the end of the popupOptions + this.hidePopup(); + if (this.responses.nodeName == testState.stateResults[testState.stateIndex].nodeName) { + testState.stateResults[testState.stateIndex] = this.responses; + } else { + testState.stateResults[testState.stateIndex].appendChild(this.responses); + } + advanceState(); + } + }; + + this.previousClick = function() { + // Triggered when the 'Back' button is clicked in the survey + if (this.currentIndex > 0) { + this.currentIndex--; + var node = this.popupOptions[this.currentIndex]; + if (node.type != 'statement') { + var prevResp = this.responses.childNodes[this.responses.childElementCount-1]; + this.responses.removeChild(prevResp); + } + this.postNode(); + if (node.type == 'question') { + this.popupContent.getElementsByTagName('textarea')[0].value = prevResp.textContent; + } else if (node.type == 'checkbox') { + var options = this.popupContent.getElementsByTagName('input'); + var savedOptions = prevResp.getElementsByTagName('option'); + for (var i=0; i<options.length; i++) { + var id = options[i].id; + for (var j=0; j<savedOptions.length; j++) { + if (savedOptions[j].getAttribute('name') == id) { + if (savedOptions[j].textContent == 'true') {options[i].checked = true;} + else {options[i].checked = false;} + break; + } + } + } + } else if (node.type == 'number') { + this.popupContent.getElementsByTagName('input')[0].value = prevResp.textContent; + } else if (node.type == 'radio') { + var options = this.popupContent.getElementsByTagName('input'); + var name = prevResp.getAttribute('name'); + for (var i=0; i<options.length; i++) { + if (options[i].id == name) { + options[i].checked = true; + break; + } + } + } + } + }; + + 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() +{ + // Just for complete clarity + testState.advanceState(); +} + +function stateMachine() +{ + // Object prototype for tracking and managing the test state + this.stateMap = []; + this.stateIndex = null; + this.currentStateMap = []; + this.currentIndex = null; + this.currentTestId = 0; + this.stateResults = []; + this.timerCallBackHolders = null; + this.initialise = function(){ + if (this.stateMap.length > 0) { + if(this.stateIndex != null) { + console.log('NOTE - State already initialise'); + } + this.stateIndex = -1; + var that = this; + var aH_pId = 0; + for (var id=0; id<this.stateMap.length; id++){ + var name = this.stateMap[id].type; + var obj = document.createElement(name); + if (name == 'audioHolder') { + obj.id = this.stateMap[id].id; + obj.setAttribute('presentedid',aH_pId); + aH_pId+=1; + } + this.stateResults.push(obj); + } + } else { + console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP'); + } + }; + this.advanceState = function(){ + if (this.stateIndex == null) { + this.initialise(); + } + if (this.stateIndex == -1) { + console.log('Starting test...'); + } + if (this.currentIndex == null){ + if (this.currentStateMap.type == "audioHolder") { + // Save current page + this.testPageCompleted(this.stateResults[this.stateIndex],this.currentStateMap,this.currentTestId); + this.currentTestId++; + } + this.stateIndex++; + if (this.stateIndex >= this.stateMap.length) { + console.log('Test Completed'); + createProjectSave(specification.projectReturn); + } else { + this.currentStateMap = this.stateMap[this.stateIndex]; + if (this.currentStateMap.type == "audioHolder") { + console.log('Loading test page'); + interfaceContext.newPage(this.currentStateMap); + this.initialiseInnerState(this.currentStateMap); + } else if (this.currentStateMap.type == "pretest" || this.currentStateMap.type == "posttest") { + if (this.currentStateMap.options.length >= 1) { + popup.initState(this.currentStateMap); + } else { + this.advanceState(); + } + } else { + this.advanceState(); + } + } + } else { + this.advanceInnerState(); + } + }; + + this.testPageCompleted = function(store, testXML, testId) { + // Function called each time a test page has been completed + 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); + }; + + this.initialiseInnerState = function(node) { + // Parses the received testXML for pre and post test options + this.currentStateMap = []; + 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(node); + this.currentStateMap.push(postTest); + this.currentIndex = -1; + this.advanceInnerState(); + }; + + this.advanceInnerState = function() { + this.currentIndex++; + if (this.currentIndex >= this.currentStateMap.length) { + this.currentIndex = null; + this.currentStateMap = this.stateMap[this.stateIndex]; + this.advanceState(); + } else { + if (this.currentStateMap[this.currentIndex].type == "audioHolder") { + console.log("Loading test page"+this.currentTestId); + } else if (this.currentStateMap[this.currentIndex].type == "pretest") { + popup.initState(this.currentStateMap[this.currentIndex]); + } else if (this.currentStateMap[this.currentIndex].type == "posttest") { + popup.initState(this.currentStateMap[this.currentIndex]); + } else { + this.advanceInnerState(); + } + } + }; + + this.previousState = function(){}; +} + +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 + // Foo gain is used for analysis to ensure paths get processed, but are not heard + // because web audio will optimise and any route which does not go to the destination gets ignored. + this.outputGain = audioContext.createGain(); + this.fooGain = audioContext.createGain(); + this.fooGain.gain = 0; + + // Use this to detect playback state: 0 - stopped, 1 - playing + this.status = 0; + + // Connect both gains to output + this.outputGain.connect(audioContext.destination); + this.fooGain.connect(audioContext.destination); + + // Create the timer Object + this.timer = new timer(); + // Create session metrics + 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) { + // Check if all audioObjects are ready + if(this.checkAllReady()) + { + this.status = 1; + this.setSynchronousLoop(); + } + } + else + { + this.status = 1; + } + if (this.status== 1) { + this.timer.startTest(); + if (id == undefined) { + id = -1; + console.log('FATAL - Passed id was undefined - AudioEngineContext.play(id)'); + return; + } else { + interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]); + } + if (this.loopPlayback) { + for (var i=0; i<this.audioObjects.length; i++) + { + this.audioObjects[i].play(this.timer.getTestTime()+1); + if (id == i) { + this.audioObjects[i].loopStart(); + } else { + this.audioObjects[i].loopStop(); + } + } + } else { + for (var i=0; i<this.audioObjects.length; i++) + { + if (i != id) { + this.audioObjects[i].outputGain.gain.value = 0.0; + this.audioObjects[i].stop(); + } else if (i == id) { + this.audioObjects[id].outputGain.gain.value = this.audioObjects[id].specification.gain*this.audioObjects[id].buffer.buffer.gain; + this.audioObjects[id].play(audioContext.currentTime+0.01); + } + } + } + interfaceContext.playhead.start(); + } + }; + + this.stop = function() { + // Send stop and reset command to all playback buffers and set audioEngine state to stopped (1) + if (this.status == 1) { + for (var i=0; i<this.audioObjects.length; i++) + { + this.audioObjects[i].stop(); + } + interfaceContext.playhead.stop(); + this.status = 0; + } + }; + + 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' + + // Create the audioObject with ID of the new track length; + audioObjectId = this.audioObjects.length; + this.audioObjects[audioObjectId] = new audioObject(audioObjectId); + + // 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].url = URL; + buffer.users.push(this.audioObjects[audioObjectId]); + if (buffer.buffer != null) + { + this.audioObjects[audioObjectId].bufferLoaded(buffer); + } + return this.audioObjects[audioObjectId]; + }; + + this.newTestPage = function() { + this.state = 0; + this.audioObjectsReady = false; + this.metric.reset(); + for (var i=0; i < this.buffers.length; i++) + { + this.buffers[i].users = []; + } + this.audioObjects = []; + }; + + this.checkAllPlayed = function() { + arr = []; + for (var id=0; id<this.audioObjects.length; id++) { + if (this.audioObjects[id].metric.wasListenedTo == false) { + arr.push(this.audioObjects[id].id); + } + } + return arr; + }; + + this.checkAllReady = function() { + var ready = true; + for (var i=0; i<this.audioObjects.length; i++) { + if (this.audioObjects[i].state == 0) { + // Track not ready + console.log('WAIT -- audioObject '+i+' not ready yet!'); + ready = false; + }; + } + return ready; + }; + + this.setSynchronousLoop = function() { + // Pads the signals so they are all exactly the same length + var length = 0; + var maxId; + for (var i=0; i<this.audioObjects.length; i++) + { + if (length < this.audioObjects[i].buffer.buffer.length) + { + length = this.audioObjects[i].buffer.buffer.length; + maxId = i; + } + } + // Extract the audio and zero-pad + for (var i=0; i<this.audioObjects.length; i++) + { + var orig = this.audioObjects[i].buffer.buffer; + var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate); + for (var c=0; c<orig.numberOfChannels; c++) + { + var inData = hold.getChannelData(c); + var outData = orig.getChannelData(c); + for (var n=0; n<orig.length; n++) + {inData[n] = outData[n];} + } + hold.gain = orig.gain; + hold.lufs = orig.lufs; + this.audioObjects[i].buffer.buffer = hold; + } + }; + +} + +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(); + + // Default output gain to be zero + this.outputGain.gain.value = 0.0; + + // Connect buffer to the audio graph + this.outputGain.connect(audioEngineContext.outputGain); + + // 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 = this.specification.gain*this.buffer.buffer.gain; + 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) { + 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.buffer; + this.bufferNode.loop = audioEngineContext.loopPlayback; + this.bufferNode.onended = function(event) { + // Safari does not like using 'this' to reference the calling object! + //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition()); + event.currentTarget.owner.stop(); + }; + if (this.bufferNode.loop == false) { + this.metric.startListening(audioEngineContext.timer.getTestTime()); + } + this.bufferNode.start(startTime); + } + }; + + this.stop = function() { + if (this.bufferNode != undefined) + { + this.metric.stopListening(audioEngineContext.timer.getTestTime(),this.getCurrentPosition()); + this.bufferNode.stop(0); + this.bufferNode = undefined; + } + }; + + this.getCurrentPosition = function() { + var time = audioEngineContext.timer.getTestTime(); + if (this.bufferNode != undefined) { + if (this.bufferNode.loop == true) { + if (audioEngineContext.status == 1) { + return (time-this.metric.listenStart)%this.buffer.buffer.duration; + } else { + return 0; + } + } else { + if (this.metric.listenHold) { + return time - this.metric.listenStart; + } else { + return 0; + } + } + } else { + return 0; + } + }; + + this.exportXMLDOM = function() { + var root = document.createElement('audioElement'); + root.id = this.specification.id; + root.setAttribute('url',this.specification.url); + var file = document.createElement('file'); + 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') { + 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); + } else if(this.specification.type == 'reference') { + root.setAttribute('reference',true); + } + } + root.appendChild(this.metric.exportXMLDOM()); + return root; + }; +} + +function timer() +{ + /* Timer object used in audioEngine to keep track of session timings + * Uses the timer of the web audio API, so sample resolution + */ + this.testStarted = false; + this.testStartTime = 0; + this.testDuration = 0; + this.minimumTestTime = 0; // No minimum test time + this.startTest = function() + { + if (this.testStarted == false) + { + this.testStartTime = audioContext.currentTime; + this.testStarted = true; + this.updateTestTime(); + audioEngineContext.metric.initialiseTest(); + } + }; + this.stopTest = function() + { + if (this.testStarted) + { + this.testDuration = this.getTestTime(); + this.testStarted = false; + } else { + console.log('ERR: Test tried to end before beginning'); + } + }; + this.updateTestTime = function() + { + if (this.testStarted) + { + this.testDuration = audioContext.currentTime - this.testStartTime; + } + }; + this.getTestTime = function() + { + this.updateTestTime(); + return this.testDuration; + }; +} + +function sessionMetrics(engine,specification) +{ + /* Used by audioEngine to link to audioObjects to minimise the timer call timers; + */ + this.engine = engine; + this.lastClicked = -1; + this.data = -1; + this.reset = function() { + 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(){}; +} + +function metricTracker(caller) +{ + /* Custom object to track and collect metric data + * Used only inside the audioObjects object. + */ + + this.listenedTimer = 0; + this.listenStart = 0; + this.listenHold = false; + this.initialPosition = -1; + this.movementTracker = []; + this.listenTracker =[]; + this.wasListenedTo = false; + this.wasMoved = false; + this.hasComments = false; + this.parent = caller; + + this.initialised = function(position) + { + if (this.initialPosition == -1) { + this.initialPosition = position; + } + }; + + this.moved = function(time,position) + { + this.wasMoved = true; + this.movementTracker[this.movementTracker.length] = [time, position]; + }; + + this.startListening = function(time) + { + if (this.listenHold == false) + { + this.wasListenedTo = true; + this.listenStart = time; + this.listenHold = true; + + var evnt = document.createElement('event'); + var testTime = document.createElement('testTime'); + testTime.setAttribute('start',time); + var bufferTime = document.createElement('bufferTime'); + bufferTime.setAttribute('start',this.parent.getCurrentPosition()); + evnt.appendChild(testTime); + evnt.appendChild(bufferTime); + this.listenTracker.push(evnt); + + console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id + } + }; + + this.stopListening = function(time,bufferStopTime) + { + if (this.listenHold == true) + { + var diff = time - this.listenStart; + this.listenedTimer += (diff); + this.listenStart = 0; + this.listenHold = false; + + var evnt = this.listenTracker[this.listenTracker.length-1]; + var testTime = evnt.getElementsByTagName('testTime')[0]; + var bufferTime = evnt.getElementsByTagName('bufferTime')[0]; + testTime.setAttribute('stop',time); + if (bufferStopTime == undefined) { + bufferTime.setAttribute('stop',this.parent.getCurrentPosition()); + } else { + bufferTime.setAttribute('stop',bufferStopTime); + } + 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) +{ + // 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; +} + +function returnDateNode() +{ + // Create an XML Node for the Date and Time a test was conducted + // Structure is + // <datetime> + // <date year="##" month="##" day="##">DD/MM/YY</date> + // <time hour="##" minute="##" sec="##">HH:MM:SS</time> + // </datetime> + var dateTime = new Date(); + var year = document.createAttribute('year'); + var month = document.createAttribute('month'); + var day = document.createAttribute('day'); + var hour = document.createAttribute('hour'); + var minute = document.createAttribute('minute'); + var secs = document.createAttribute('secs'); + + year.nodeValue = dateTime.getFullYear(); + month.nodeValue = dateTime.getMonth()+1; + day.nodeValue = dateTime.getDate(); + hour.nodeValue = dateTime.getHours(); + minute.nodeValue = dateTime.getMinutes(); + secs.nodeValue = dateTime.getSeconds(); + + var hold = document.createElement("datetime"); + var date = document.createElement("date"); + date.textContent = year.nodeValue+'/'+month.nodeValue+'/'+day.nodeValue; + var time = document.createElement("time"); + time.textContent = hour.nodeValue+':'+minute.nodeValue+':'+secs.nodeValue; + + date.setAttributeNode(year); + date.setAttributeNode(month); + date.setAttributeNode(day); + time.setAttributeNode(hour); + time.setAttributeNode(minute); + time.setAttributeNode(secs); + + hold.appendChild(date); + hold.appendChild(time); + return hold; + +} + +function Specification() { + // Handles the decoding of the project specification XML into a simple JavaScript Object. + + this.interfaceType = 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.audioHolders = []; + this.metrics = []; + this.loudness = null; + + this.decode = function(projectXML) { + // projectXML - DOM Parsed document + this.projectXML = projectXML.childNodes[0]; + var setupNode = projectXML.getElementsByTagName('setup')[0]; + this.interfaceType = setupNode.getAttribute('interface'); + this.projectReturn = setupNode.getAttribute('projectReturn'); + this.testPages = setupNode.getAttribute('testPages'); + if (setupNode.getAttribute('randomiseOrder') == "true") { + this.randomiseOrder = true; + } else {this.randomiseOrder = false;} + if (setupNode.getAttribute('collectMetrics') == "true") { + this.collectMetrics = true; + } else {this.collectMetrics = false;} + if (isNaN(Number(this.testPages)) || this.testPages == undefined) + { + this.testPages = null; + } else { + 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'); + + 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'); + for (var i=0; i<metricCollection.length; i++) { + this.metrics.push(new this.metricNode(metricCollection[i].textContent)); + } + } + + var commonInterfaceNode = setupNode.getElementsByTagName('interface'); + if (commonInterfaceNode.length > 0) { + commonInterfaceNode = commonInterfaceNode[0]; + } else { + commonInterfaceNode = undefined; + } + + this.commonInterface = new function() { + this.OptionNode = function(child) { + this.type = child.nodeName; + if (this.type == 'option') + { + this.name = child.getAttribute('name'); + } + else if (this.type == 'check') { + this.check = child.getAttribute('name'); + if (this.check == 'scalerange') { + this.min = child.getAttribute('min'); + this.max = child.getAttribute('max'); + if (this.min == null) {this.min = 1;} + else if (Number(this.min) > 1 && this.min != null) { + this.min = Number(this.min)/100; + } else { + this.min = Number(this.min); + } + if (this.max == null) {this.max = 0;} + else if (Number(this.max) > 1 && this.max != null) { + this.max = Number(this.max)/100; + } else { + this.max = Number(this.max); + } + } + } else if (this.type == 'anchor' || this.type == 'reference') { + this.value = Number(child.textContent); + this.enforce = child.getAttribute('enforce'); + if (this.enforce == 'true') {this.enforce = true;} + else {this.enforce = false;} + } + }; + this.options = []; + if (commonInterfaceNode != undefined) { + var child = commonInterfaceNode.firstElementChild; + while (child != undefined) { + this.options.push(new this.OptionNode(child)); + child = child.nextElementSibling; + } + } + }; + + var audioHolders = projectXML.getElementsByTagName('audioHolder'); + for (var i=0; i<audioHolders.length; 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 && typeof randomiseOrder === "function") + { + this.audioHolders = randomiseOrder(this.audioHolders); + for (var i=0; i<this.audioHolders.length; i++) + { + this.audioHolders[i].presentedId = i; + } + } + + if (this.testPages != null || this.testPages != undefined) + { + if (this.testPages > audioHolders.length) + { + console.log('Warning: You have specified '+audioHolders.length+' tests but requested '+this.testPages+' be completed!'); + this.testPages = audioHolders.length; + } + var aH = this.audioHolders; + this.audioHolders = []; + for (var i=0; i<this.testPages; i++) + { + this.audioHolders.push(aH[i]); + } + } + }; + + 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() { + + this.childOption = function() { + this.type = 'option'; + this.id = null; + this.name = undefined; + this.text = null; + }; + + 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'); + } + }; + + 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; + } + 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); + } + } + }; + }; + this.preTest = new this.prepostNode("pretest"); + this.postTest = new this.prepostNode("posttest"); + + this.metricNode = function(name) { + this.enabled = name; + }; + + this.audioHolderNode = function(parent) { + this.type = 'audioHolder'; + 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.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)); + } + + 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++) + { + 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) + { + 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") + { + 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) + { + 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 a number!"); + console.log("ERROR - Marker not enforced!"); + } + } + } + }; + 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.id = null; + this.type = undefined; + this.question = undefined; + this.options = []; + this.statement = undefined; + + this.childOption = function() { + this.type = 'option'; + this.name = null; + this.text = null; + }; + 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); + } + 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; + } + 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; + } + 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; + } + }; + }; + }; +} + +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. + this.interfaceObjects = []; + this.interfaceObject = function(){}; + + 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++) + {this.commentQuestions[i].resize();} + try + { + resizeWindow(event); + } + catch(err) + { + console.log("Warning - Interface does not have Resize option"); + console.log(err); + } + }; + + this.returnNavigator = function() + { + var node = document.createElement("navigator"); + var platform = document.createElement("platform"); + platform.textContent = navigator.platform; + var vendor = document.createElement("vendor"); + vendor.textContent = navigator.vendor; + var userAgent = document.createElement("uagent"); + userAgent.textContent = navigator.userAgent; + node.appendChild(platform); + node.appendChild(vendor); + node.appendChild(userAgent); + return node; + }; + + this.commentBoxes = []; + this.elementCommentBox = 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; + console.log("Comment frag-"+this.id+": "+response.textContent); + root.appendChild(question); + root.appendChild(response); + } + return root; + }; + this.resize = function() + { + var boxwidth = (window.innerWidth-100)/2; + if (boxwidth >= 600) + { + boxwidth = 600; + } + else if (boxwidth < 400) + { + boxwidth = 400; + } + this.trackComment.style.width = boxwidth+"px"; + this.trackCommentBox.style.width = boxwidth-6+"px"; + }; + this.resize(); + }; + + this.commentQuestions = []; + + this.commentBox = function(commentQuestion) { + this.specification = commentQuestion; + // Create document objects to hold the comment boxes + this.holder = document.createElement('div'); + this.holder.className = 'comment-div'; + // Create a string next to each comment asking for a comment + this.string = document.createElement('span'); + this.string.innerHTML = commentQuestion.question; + // Create the HTML5 comment box 'textarea' + this.textArea = document.createElement('textarea'); + this.textArea.rows = '4'; + this.textArea.cols = '100'; + this.textArea.className = 'trackComment'; + var br = document.createElement('br'); + // Add to the holder. + this.holder.appendChild(this.string); + this.holder.appendChild(br); + this.holder.appendChild(this.textArea); + + this.exportXMLDOM = function() { + var root = document.createElement('comment'); + root.id = this.specification.id; + root.setAttribute('type',this.specification.type); + root.textContent = this.textArea.value; + console.log("Question: "+this.string.textContent); + console.log("Response: "+root.textContent); + return root; + }; + this.resize = function() + { + var boxwidth = (window.innerWidth-100)/2; + if (boxwidth >= 600) + { + boxwidth = 600; + } + else if (boxwidth < 400) + { + boxwidth = 400; + } + this.holder.style.width = boxwidth+"px"; + this.textArea.style.width = boxwidth-6+"px"; + }; + this.resize(); + }; + + this.radioBox = function(commentQuestion) { + this.specification = commentQuestion; + // Create document objects to hold the comment boxes + this.holder = document.createElement('div'); + this.holder.className = 'comment-div'; + // Create a string next to each comment asking for a comment + this.string = document.createElement('span'); + this.string.innerHTML = commentQuestion.statement; + var br = document.createElement('br'); + // Add to the holder. + this.holder.appendChild(this.string); + this.holder.appendChild(br); + this.options = []; + this.inputs = document.createElement('div'); + this.span = document.createElement('div'); + this.inputs.align = 'center'; + this.inputs.style.marginLeft = '12px'; + this.span.style.marginLeft = '12px'; + this.span.align = 'center'; + this.span.style.marginTop = '15px'; + + var optCount = commentQuestion.options.length; + for (var i=0; i<optCount; i++) + { + var div = document.createElement('div'); + div.style.width = '80px'; + div.style.float = 'left'; + var input = document.createElement('input'); + input.type = 'radio'; + input.name = commentQuestion.id; + input.setAttribute('setvalue',commentQuestion.options[i].name); + input.className = 'comment-radio'; + div.appendChild(input); + this.inputs.appendChild(div); + + + div = document.createElement('div'); + div.style.width = '80px'; + div.style.float = 'left'; + div.align = 'center'; + var span = document.createElement('span'); + span.textContent = commentQuestion.options[i].text; + span.className = 'comment-radio-span'; + div.appendChild(span); + this.span.appendChild(div); + this.options.push(input); + } + this.holder.appendChild(this.span); + this.holder.appendChild(this.inputs); + + this.exportXMLDOM = function() { + var root = document.createElement('comment'); + root.id = this.specification.id; + root.setAttribute('type',this.specification.type); + var question = document.createElement('question'); + question.textContent = this.string.textContent; + var response = document.createElement('response'); + var i=0; + while(this.options[i].checked == false) { + i++; + if (i >= this.options.length) { + break; + } + } + if (i >= this.options.length) { + response.textContent = 'null'; + } else { + response.textContent = this.options[i].getAttribute('setvalue'); + response.setAttribute('number',i); + } + console.log('Comment: '+question.textContent); + console.log('Response: '+response.textContent); + root.appendChild(question); + root.appendChild(response); + return root; + }; + this.resize = function() + { + var boxwidth = (window.innerWidth-100)/2; + if (boxwidth >= 600) + { + boxwidth = 600; + } + else if (boxwidth < 400) + { + boxwidth = 400; + } + this.holder.style.width = boxwidth+"px"; + var text = this.holder.children[2]; + var options = this.holder.children[3]; + var optCount = options.children.length; + var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px'; + var options = options.firstChild; + var text = text.firstChild; + options.style.marginRight = spanMargin; + options.style.marginLeft = spanMargin; + text.style.marginRight = spanMargin; + text.style.marginLeft = spanMargin; + while(options.nextSibling != undefined) + { + options = options.nextSibling; + text = text.nextSibling; + options.style.marginRight = spanMargin; + options.style.marginLeft = spanMargin; + text.style.marginRight = spanMargin; + text.style.marginLeft = spanMargin; + } + }; + this.resize(); + }; + + this.checkboxBox = function(commentQuestion) { + this.specification = commentQuestion; + // Create document objects to hold the comment boxes + this.holder = document.createElement('div'); + this.holder.className = 'comment-div'; + // Create a string next to each comment asking for a comment + this.string = document.createElement('span'); + this.string.innerHTML = commentQuestion.statement; + var br = document.createElement('br'); + // Add to the holder. + this.holder.appendChild(this.string); + this.holder.appendChild(br); + this.options = []; + this.inputs = document.createElement('div'); + this.span = document.createElement('div'); + this.inputs.align = 'center'; + this.inputs.style.marginLeft = '12px'; + this.span.style.marginLeft = '12px'; + this.span.align = 'center'; + this.span.style.marginTop = '15px'; + + var optCount = commentQuestion.options.length; + for (var i=0; i<optCount; i++) + { + var div = document.createElement('div'); + div.style.width = '80px'; + div.style.float = 'left'; + var input = document.createElement('input'); + input.type = 'checkbox'; + input.name = commentQuestion.id; + input.setAttribute('setvalue',commentQuestion.options[i].name); + input.className = 'comment-radio'; + div.appendChild(input); + this.inputs.appendChild(div); + + + div = document.createElement('div'); + div.style.width = '80px'; + div.style.float = 'left'; + div.align = 'center'; + var span = document.createElement('span'); + span.textContent = commentQuestion.options[i].text; + span.className = 'comment-radio-span'; + div.appendChild(span); + this.span.appendChild(div); + this.options.push(input); + } + this.holder.appendChild(this.span); + this.holder.appendChild(this.inputs); + + this.exportXMLDOM = function() { + var root = document.createElement('comment'); + root.id = this.specification.id; + root.setAttribute('type',this.specification.type); + var question = document.createElement('question'); + question.textContent = this.string.textContent; + root.appendChild(question); + console.log('Comment: '+question.textContent); + for (var i=0; i<this.options.length; i++) { + var response = document.createElement('response'); + response.textContent = this.options[i].checked; + response.setAttribute('name',this.options[i].getAttribute('setvalue')); + root.appendChild(response); + console.log('Response '+response.getAttribute('name') +': '+response.textContent); + } + return root; + }; + this.resize = function() + { + var boxwidth = (window.innerWidth-100)/2; + if (boxwidth >= 600) + { + boxwidth = 600; + } + else if (boxwidth < 400) + { + boxwidth = 400; + } + this.holder.style.width = boxwidth+"px"; + var text = this.holder.children[2]; + var options = this.holder.children[3]; + var optCount = options.children.length; + var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px'; + var options = options.firstChild; + var text = text.firstChild; + options.style.marginRight = spanMargin; + options.style.marginLeft = spanMargin; + text.style.marginRight = spanMargin; + text.style.marginLeft = spanMargin; + while(options.nextSibling != undefined) + { + options = options.nextSibling; + text = text.nextSibling; + options.style.marginRight = spanMargin; + options.style.marginLeft = spanMargin; + text.style.marginRight = spanMargin; + text.style.marginLeft = spanMargin; + } + }; + this.resize(); + }; + + this.createCommentBox = function(audioObject) { + var node = new this.elementCommentBox(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); + } + }; + + this.deleteCommentBoxes = function() { + this.commentBoxes = []; + }; + + this.createCommentQuestion = function(element) { + var node; + if (element.type == 'text') { + node = new this.commentBox(element); + } else if (element.type == 'radio') { + node = new this.radioBox(element); + } else if (element.type == 'checkbox') { + node = new this.checkboxBox(element); + } + this.commentQuestions.push(node); + return node; + }; + + this.deleteCommentQuestions = function() + { + this.commentQuestions = []; + }; + + this.playhead = new function() + { + this.object = document.createElement('div'); + this.object.className = 'playhead'; + this.object.align = 'left'; + var curTime = document.createElement('div'); + curTime.style.width = '50px'; + this.curTimeSpan = document.createElement('span'); + this.curTimeSpan.textContent = '00:00'; + curTime.appendChild(this.curTimeSpan); + this.object.appendChild(curTime); + this.scrubberTrack = document.createElement('div'); + this.scrubberTrack.className = 'playhead-scrub-track'; + + this.scrubberHead = document.createElement('div'); + this.scrubberHead.id = 'playhead-scrubber'; + this.scrubberTrack.appendChild(this.scrubberHead); + this.object.appendChild(this.scrubberTrack); + + this.timePerPixel = 0; + this.maxTime = 0; + + this.playbackObject; + + this.setTimePerPixel = function(audioObject) { + //maxTime must be in seconds + this.playbackObject = audioObject; + 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) { + this.curTimeSpan.textContent = '0.00'; + } else { + this.curTimeSpan.textContent = '00:00'; + } + }; + + this.update = function() { + // Update the playhead position, startPlay must be called + if (this.timePerPixel > 0) { + var time = this.playbackObject.getCurrentPosition(); + if (time > 0) { + var width = 490; + var pix = Math.floor(time/this.timePerPixel); + this.scrubberHead.style.left = pix+'px'; + if (this.maxTime > 60.0) { + var secs = time%60; + var mins = Math.floor((time-secs)/60); + secs = secs.toString(); + secs = secs.substr(0,2); + mins = mins.toString(); + this.curTimeSpan.textContent = mins+':'+secs; + } else { + time = time.toString(); + this.curTimeSpan.textContent = time.substr(0,4); + } + } else { + this.scrubberHead.style.left = '0px'; + if (this.maxTime < 60) { + this.curTimeSpan.textContent = '0.00'; + } else { + this.curTimeSpan.textContent = '00:00'; + } + } + } + }; + + this.interval = undefined; + + this.start = function() { + if (this.playbackObject != undefined && this.interval == undefined) { + if (this.maxTime < 60) { + this.interval = setInterval(function(){interfaceContext.playhead.update();},10); + } else { + this.interval = setInterval(function(){interfaceContext.playhead.update();},100); + } + } + }; + this.stop = function() { + clearInterval(this.interval); + this.interval = undefined; + if (this.maxTime < 60) { + this.curTimeSpan.textContent = '0.00'; + } else { + this.curTimeSpan.textContent = '00:00'; + } + }; + }; + + // Global Checkers + // These functions will help enforce the checkers + this.checkHiddenAnchor = function() + { + var audioHolder = testState.currentStateMap[testState.currentIndex]; + if (audioHolder.anchorId != null) + { + var audioObject = audioEngineContext.audioObjects[audioHolder.anchorId]; + if (audioObject.interfaceDOM.getValue() > audioObject.specification.marker && audioObject.interfaceDOM.enforce == true) + { + // Anchor is not set below + console.log('Anchor node not below marker value'); + alert('Please keep listening'); + return false; + } + } + return true; + }; + + this.checkHiddenReference = function() + { + var audioHolder = testState.currentStateMap[testState.currentIndex]; + if (audioHolder.referenceId != null) + { + var audioObject = audioEngineContext.audioObjects[audioHolder.referenceId]; + if (audioObject.interfaceDOM.getValue() < audioObject.specification.marker && audioObject.interfaceDOM.enforce == true) + { + // Anchor is not set below + console.log('Reference node not above marker value'); + alert('Please keep listening'); + return false; + } + } + return true; + }; + + this.checkFragmentsFullyPlayed = function () + { + // Checks the entire file has been played back + // NOTE ! This will return true IF playback is Looped!!! + if (audioEngineContext.loopPlayback) + { + console.log("WARNING - Looped source: Cannot check fragments are fully played"); + return true; + } + var check_pass = true; + var error_obj = []; + for (var i = 0; i<audioEngineContext.audioObjects.length; i++) + { + var object = audioEngineContext.audioObjects[i]; + var time = object.buffer.buffer.duration; + var metric = object.metric; + var passed = false; + for (var j=0; j<metric.listenTracker.length; j++) + { + var bt = metric.listenTracker[j].getElementsByTagName('buffertime'); + var start_time = Number(bt[0].getAttribute('start')); + var stop_time = Number(bt[0].getAttribute('stop')); + var delta = stop_time - start_time; + if (delta >= time) + { + passed = true; + break; + } + } + if (passed == false) + { + check_pass = false; + console.log("Continue listening to track-"+i); + error_obj.push(i); + } + } + if (check_pass == false) + { + var str_start = "You have not completely listened to fragments "; + for (var i=0; i<error_obj.length; i++) + { + str_start += error_obj[i]; + if (i != error_obj.length-1) + { + str_start += ', '; + } + } + str_start += ". Please keep listening"; + console.log("[ALERT]: "+str_start); + 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) + { + 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