BrechtDeMan@938: /** BrechtDeMan@938: * core.js BrechtDeMan@938: * BrechtDeMan@938: * Main script to run, calls all other core functions and manages loading/store to backend. BrechtDeMan@938: * Also contains all global variables. BrechtDeMan@938: */ BrechtDeMan@938: BrechtDeMan@938: /* create the web audio API context and store in audioContext*/ BrechtDeMan@938: var audioContext; // Hold the browser web audio API BrechtDeMan@938: var projectXML; // Hold the parsed setup XML nicholas@952: var popup; // Hold the interfacePopup object nicholas@952: var currentState; // Keep track of the current state (pre/post test, which test, final test? first test?) BrechtDeMan@938: var testXMLSetups = []; // Hold the parsed test instances BrechtDeMan@938: var testResultsHolders =[]; // Hold the results from each test for publishing to XML BrechtDeMan@938: var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order BrechtDeMan@938: var currentTestHolder; // Hold any intermediate results during test - metrics BrechtDeMan@938: var audioEngineContext; // The custome AudioEngine object BrechtDeMan@938: var projectReturn; // Hold the URL for the return BrechtDeMan@938: var preTestQuestions = document.createElement('PreTest'); // Store any pre-test question response BrechtDeMan@938: var postTestQuestions = document.createElement('PostTest'); // Store any post-test question response BrechtDeMan@938: BrechtDeMan@938: // Add a prototype to the bufferSourceNode to reference to the audioObject holding it BrechtDeMan@938: AudioBufferSourceNode.prototype.owner = undefined; BrechtDeMan@938: BrechtDeMan@938: window.onload = function() { BrechtDeMan@938: // Function called once the browser has loaded all files. BrechtDeMan@938: // This should perform any initial commands such as structure / loading documents BrechtDeMan@938: BrechtDeMan@938: // Create a web audio API context BrechtDeMan@938: // Fixed for cross-browser support BrechtDeMan@938: var AudioContext = window.AudioContext || window.webkitAudioContext; BrechtDeMan@938: audioContext = new AudioContext; BrechtDeMan@938: BrechtDeMan@938: // Create the audio engine object BrechtDeMan@938: audioEngineContext = new AudioEngine(); nicholas@952: nicholas@952: // Create the popup interface object nicholas@952: popup = new interfacePopup(); BrechtDeMan@938: }; BrechtDeMan@938: nicholas@952: function interfacePopup() { nicholas@952: // Creates an object to manage the popup nicholas@952: this.popup = null; nicholas@952: this.popupContent = null; nicholas@952: this.popupButton = null; nicholas@952: this.popupOptions = null; nicholas@952: this.currentIndex = null; nicholas@952: this.responses = null; nicholas@952: this.createPopup = function(){ nicholas@952: // Create popup window interface nicholas@952: var insertPoint = document.getElementById("topLevelBody"); nicholas@952: var blank = document.createElement('div'); nicholas@952: blank.className = 'testHalt'; nicholas@952: nicholas@952: this.popup = document.createElement('div'); nicholas@952: this.popup.id = 'popupHolder'; nicholas@952: this.popup.className = 'popupHolder'; nicholas@952: this.popup.style.position = 'absolute'; nicholas@952: this.popup.style.left = (window.innerWidth/2)-250 + 'px'; nicholas@952: this.popup.style.top = (window.innerHeight/2)-125 + 'px'; nicholas@952: nicholas@952: this.popupContent = document.createElement('div'); nicholas@952: this.popupContent.id = 'popupContent'; nicholas@952: this.popupContent.style.marginTop = '25px'; nicholas@952: this.popupContent.align = 'center'; nicholas@952: this.popup.appendChild(this.popupContent); nicholas@952: nicholas@952: this.popupButton = document.createElement('button'); nicholas@952: this.popupButton.className = 'popupButton'; nicholas@952: this.popupButton.innerHTML = 'Next'; nicholas@952: this.popupButton.onclick = function(){popup.buttonClicked();}; nicholas@952: insertPoint.appendChild(this.popup); nicholas@952: insertPoint.appendChild(blank); nicholas@952: }; nicholas@951: nicholas@952: this.showPopup = function(){ nicholas@952: if (this.popup == null || this.popup == undefined) { nicholas@952: this.createPopup(); nicholas@952: } nicholas@952: this.popup.style.zIndex = 3; nicholas@952: this.popup.style.visibility = 'visible'; nicholas@952: var blank = document.getElementsByClassName('testHalt')[0]; nicholas@952: blank.style.zIndex = 2; nicholas@952: blank.style.visibility = 'visible'; nicholas@952: }; nicholas@952: nicholas@952: this.hidePopup = function(){ nicholas@952: this.popup.style.zIndex = -1; nicholas@952: this.popup.style.visibility = 'hidden'; nicholas@952: var blank = document.getElementsByClassName('testHalt')[0]; nicholas@952: blank.style.zIndex = -2; nicholas@952: blank.style.visibility = 'hidden'; nicholas@952: }; nicholas@952: nicholas@952: this.postNode = function() { nicholas@952: // This will take the node from the popupOptions and display it nicholas@952: var node = this.popupOptions[this.currentIndex]; nicholas@952: this.popupContent.innerHTML = null; nicholas@952: if (node.nodeName == 'statement') { nicholas@952: var span = document.createElement('span'); nicholas@952: span.textContent = node.textContent; nicholas@952: this.popupContent.appendChild(span); nicholas@952: } else if (node.nodeName == 'question') { nicholas@952: var span = document.createElement('span'); nicholas@952: span.textContent = node.textContent; nicholas@952: var textArea = document.createElement('textarea'); nicholas@952: var br = document.createElement('br'); nicholas@952: this.popupContent.appendChild(span); nicholas@952: this.popupContent.appendChild(br); nicholas@952: this.popupContent.appendChild(textArea); nicholas@952: } nicholas@952: this.popupContent.appendChild(this.popupButton); nicholas@952: } nicholas@952: nicholas@952: this.initState = function(node) { nicholas@952: //Call this with your preTest and postTest nodes when needed to nicholas@952: // initialise the popup procedure. nicholas@952: this.popupOptions = $(node).children(); nicholas@952: if (this.popupOptions.length > 0) { nicholas@952: if (node.nodeName == 'preTest' || node.nodeName == 'PreTest') { nicholas@952: this.responses = document.createElement('PreTest'); nicholas@952: } else if (node.nodeName == 'postTest' || node.nodeName == 'PostTest') { nicholas@952: this.responses = document.createElement('PostTest'); nicholas@952: } else { nicholas@952: console.log ('WARNING - popup node neither pre or post!'); nicholas@952: this.responses = document.createElement('responses'); nicholas@952: } nicholas@952: this.currentIndex = 0; nicholas@952: this.showPopup(); nicholas@952: this.postNode(); nicholas@952: } nicholas@952: } nicholas@952: nicholas@952: this.buttonClicked = function() { nicholas@952: // Each time the popup button is clicked! nicholas@952: var node = this.popupOptions[this.currentIndex]; nicholas@952: if (node.nodeName == 'question') { nicholas@952: // Must extract the question data nicholas@952: var mandatory = node.attributes['mandatory']; nicholas@952: if (mandatory == undefined) { nicholas@952: mandatory = false; nicholas@952: } else { nicholas@952: if (mandatory.value == 'true'){mandatory = true;} nicholas@952: else {mandatory = false;} nicholas@952: } nicholas@952: var textArea = $(popup.popupContent).find('textarea')[0]; nicholas@952: if (mandatory == true && textArea.value.length == 0) { nicholas@952: alert('This question is mandatory'); nicholas@952: return; nicholas@952: } else { nicholas@952: // Save the text content nicholas@952: var hold = document.createElement('comment'); nicholas@952: hold.id = node.attributes['id'].value; nicholas@952: hold.innerHTML = textArea.value; nicholas@953: console.log("Question: "+ node.textContent); nicholas@953: console.log("Question Response: "+ textArea.value); nicholas@952: this.responses.appendChild(hold); nicholas@952: } nicholas@952: } nicholas@952: this.currentIndex++; nicholas@952: if (this.currentIndex < this.popupOptions.length) { nicholas@952: this.postNode(); nicholas@952: } else { nicholas@952: // Reached the end of the popupOptions nicholas@952: this.hidePopup(); nicholas@952: advanceState(); nicholas@952: } nicholas@952: } nicholas@951: } nicholas@951: nicholas@952: function advanceState() nicholas@951: { nicholas@952: console.log(currentState); nicholas@952: if (currentState == 'preTest') nicholas@952: { nicholas@952: // End of pre-test, begin the test nicholas@952: preTestQuestions = popup.responses; nicholas@952: loadTest(0); nicholas@952: } else if (currentState == 'postTest') { nicholas@952: postTestQuestions = popup.responses; nicholas@952: console.log('ALL COLLECTED!'); nicholas@952: }else if (currentState.substr(0,10) == 'testRunPre') nicholas@952: { nicholas@952: // Start the test nicholas@952: var testId = currentState.substr(11,currentState.length-10); nicholas@952: currentState = 'testRun-'+testId; nicholas@952: currentTestHolder.appendChild(popup.responses); nicholas@952: //audioEngineContext.timer.startTest(); nicholas@952: //audioEngineContext.play(); nicholas@952: } else if (currentState.substr(0,11) == 'testRunPost') nicholas@952: { nicholas@952: var testId = currentState.substr(12,currentState.length-11); nicholas@952: currentTestHolder.appendChild(popup.responses); nicholas@952: testEnded(testId); nicholas@952: } else if (currentState.substr(0,7) == 'testRun') nicholas@952: { nicholas@952: var testId = currentState.substr(8,currentState.length-7); nicholas@952: // Check if we have any post tests to perform nicholas@952: var postXML = $(testXMLSetups[testId]).find('PostTest')[0]; nicholas@952: if (postXML == undefined || postXML.childElementCount == 0) { nicholas@952: testEnded(testId); nicholas@952: } nicholas@952: else if (postXML.childElementCount > 0) nicholas@952: { nicholas@952: currentState = 'testRunPost-'+testId; nicholas@952: popup.initState(postXML); nicholas@952: } nicholas@952: else { nicholas@952: nicholas@952: nicholas@952: // No post tests, check if we have another test to perform instead nicholas@952: nicholas@952: } nicholas@951: } nicholas@952: console.log(currentState); nicholas@951: } nicholas@951: nicholas@952: function testEnded(testId) nicholas@951: { nicholas@952: pageXMLSave(testId); nicholas@952: if (testXMLSetups.length-1 > testId) nicholas@952: { nicholas@952: // Yes we have another test to perform nicholas@952: testId = (Number(testId)+1); nicholas@952: currentState = 'testRun-'+testId; nicholas@952: loadTest(testId); nicholas@952: } else { nicholas@952: console.log('Testing Completed!'); nicholas@952: currentState = 'postTest'; nicholas@952: // Check for any post tests nicholas@952: var xmlSetup = projectXML.find('setup'); nicholas@952: var postTest = xmlSetup.find('PostTest')[0]; nicholas@952: popup.initState(postTest); nicholas@952: } nicholas@951: } nicholas@951: BrechtDeMan@938: function loadProjectSpec(url) { BrechtDeMan@938: // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data BrechtDeMan@938: // If url is null, request client to upload project XML document BrechtDeMan@938: var r = new XMLHttpRequest(); BrechtDeMan@938: r.open('GET',url,true); BrechtDeMan@938: r.onload = function() { BrechtDeMan@938: loadProjectSpecCallback(r.response); BrechtDeMan@938: }; BrechtDeMan@938: r.send(); BrechtDeMan@938: }; BrechtDeMan@938: BrechtDeMan@938: function loadProjectSpecCallback(response) { BrechtDeMan@938: // Function called after asynchronous download of XML project specification BrechtDeMan@938: var decode = $.parseXML(response); BrechtDeMan@938: projectXML = $(decode); BrechtDeMan@938: BrechtDeMan@938: // Now extract the setup tag BrechtDeMan@938: var xmlSetup = projectXML.find('setup'); BrechtDeMan@938: // Detect the interface to use and load the relevant javascripts. BrechtDeMan@938: var interfaceType = xmlSetup[0].attributes['interface']; BrechtDeMan@938: var interfaceJS = document.createElement('script'); BrechtDeMan@938: interfaceJS.setAttribute("type","text/javascript"); BrechtDeMan@938: if (interfaceType.value == 'APE') { BrechtDeMan@938: interfaceJS.setAttribute("src","ape.js"); BrechtDeMan@938: BrechtDeMan@938: // APE comes with a css file BrechtDeMan@938: var css = document.createElement('link'); BrechtDeMan@938: css.rel = 'stylesheet'; BrechtDeMan@938: css.type = 'text/css'; BrechtDeMan@938: css.href = 'ape.css'; BrechtDeMan@938: BrechtDeMan@938: document.getElementsByTagName("head")[0].appendChild(css); BrechtDeMan@938: } BrechtDeMan@938: document.getElementsByTagName("head")[0].appendChild(interfaceJS); BrechtDeMan@938: } BrechtDeMan@938: BrechtDeMan@938: function createProjectSave(destURL) { BrechtDeMan@938: // Save the data from interface into XML and send to destURL BrechtDeMan@938: // If destURL is null then download XML in client BrechtDeMan@938: // Now time to render file locally BrechtDeMan@938: var xmlDoc = interfaceXMLSave(); BrechtDeMan@938: if (destURL == "null" || destURL == undefined) { BrechtDeMan@938: var parent = document.createElement("div"); BrechtDeMan@938: parent.appendChild(xmlDoc); BrechtDeMan@938: var file = [parent.innerHTML]; BrechtDeMan@938: var bb = new Blob(file,{type : 'application/xml'}); BrechtDeMan@938: var dnlk = window.URL.createObjectURL(bb); BrechtDeMan@938: var a = document.createElement("a"); BrechtDeMan@938: a.hidden = ''; BrechtDeMan@938: a.href = dnlk; BrechtDeMan@938: a.download = "save.xml"; BrechtDeMan@938: a.textContent = "Save File"; BrechtDeMan@938: BrechtDeMan@938: var submitDiv = document.getElementById('download-point'); BrechtDeMan@938: submitDiv.appendChild(a); BrechtDeMan@938: } BrechtDeMan@938: return submitDiv; BrechtDeMan@938: } BrechtDeMan@938: BrechtDeMan@938: function AudioEngine() { BrechtDeMan@938: BrechtDeMan@938: // Create two output paths, the main outputGain and fooGain. BrechtDeMan@938: // Output gain is default to 1 and any items for playback route here BrechtDeMan@938: // Foo gain is used for analysis to ensure paths get processed, but are not heard BrechtDeMan@938: // because web audio will optimise and any route which does not go to the destination gets ignored. BrechtDeMan@938: this.outputGain = audioContext.createGain(); BrechtDeMan@938: this.fooGain = audioContext.createGain(); BrechtDeMan@938: this.fooGain.gain = 0; BrechtDeMan@938: BrechtDeMan@938: // Use this to detect playback state: 0 - stopped, 1 - playing BrechtDeMan@938: this.status = 0; n@950: this.audioObjectsReady = false; BrechtDeMan@938: BrechtDeMan@938: // Connect both gains to output BrechtDeMan@938: this.outputGain.connect(audioContext.destination); BrechtDeMan@938: this.fooGain.connect(audioContext.destination); BrechtDeMan@938: BrechtDeMan@938: // Create the timer Object BrechtDeMan@938: this.timer = new timer(); BrechtDeMan@938: // Create session metrics BrechtDeMan@938: this.metric = new sessionMetrics(this); BrechtDeMan@938: BrechtDeMan@938: this.loopPlayback = false; BrechtDeMan@938: BrechtDeMan@938: // Create store for new audioObjects BrechtDeMan@938: this.audioObjects = []; BrechtDeMan@938: n@950: this.play = function() { n@950: // Start the timer and set the audioEngine state to playing (1) n@950: if (this.status == 0) { n@950: // Check if all audioObjects are ready n@950: if (this.audioObjectsReady == false) { n@950: this.audioObjectsReady = this.checkAllReady(); n@950: } n@950: if (this.audioObjectsReady == true) { n@950: this.timer.startTest(); n@950: this.status = 1; n@950: } n@950: } n@950: }; BrechtDeMan@938: n@950: this.stop = function() { n@950: // Send stop and reset command to all playback buffers and set audioEngine state to stopped (1) n@950: if (this.status == 1) { n@950: for (var i=0; i