Mercurial > hg > webaudioevaluationtool
view ape.js @ 964:9c09cb530ec1
Major Update. All new state machine to track the session state and hold session data. Will enable new interfaces to be built on top and have the same common structures.
author | Nicholas Jillings <nicholas.jillings@eecs.qmul.ac.uk> |
---|---|
date | Wed, 27 May 2015 16:45:48 +0100 |
parents | ba734075da2d |
children | 2dc61bd6494e |
line wrap: on
line source
/** * ape.js * Create the APE interface */ // preTest - In preTest state // testRun-ID - In test running, test Id number at the end 'testRun-2' // testRunPost-ID - Post test of test ID // testRunPre-ID - Pre-test of test ID // postTest - End of test, final submission! // Once this is loaded and parsed, begin execution loadInterface(projectXML); function loadInterface(xmlDoc) { // Get the dimensions of the screen available to the page var width = window.innerWidth; var height = window.innerHeight; // The injection point into the HTML page var insertPoint = document.getElementById("topLevelBody"); var testContent = document.createElement('div'); testContent.id = 'testContent'; // Decode parts of the xmlDoc that are needed // xmlDoc MUST already be parsed by jQuery! var xmlSetup = xmlDoc.find('setup'); // Should put in an error function here incase of malprocessed or malformed XML // Create pre and post test questions var preTest = xmlSetup.find('PreTest'); var postTest = xmlSetup.find('PostTest'); preTest = preTest[0]; postTest = postTest[0]; if (preTest == undefined) {preTest = document.createElement("preTest");} if (postTest == undefined){postTest= document.createElement("postTest");} testState.stateMap.push(preTest); // Extract the different test XML DOM trees var audioHolders = xmlDoc.find('audioHolder'); var testXMLSetups = []; audioHolders.each(function(index,element) { var repeatN = element.attributes['repeatCount'].value; for (var r=0; r<=repeatN; r++) { testXMLSetups.push(element); } }); // New check if we need to randomise the test order var randomise = xmlSetup[0].attributes['randomiseOrder']; if (randomise != undefined) { if (randomise.value === 'true'){ randomise = true; } else { randomise = false; } } else { randomise = false; } if (randomise) { testXMLSetups = randomiseOrder(testXMLSetups); } $(testXMLSetups).each(function(index,elem){ testState.stateMap.push(elem); }) testState.stateMap.push(postTest); // Obtain the metrics enabled var metricNode = xmlSetup.find('Metric'); var metricNode = metricNode.find('metricEnable'); metricNode.each(function(index,node){ var enabled = node.textContent; switch(enabled) { case 'testTimer': sessionMetrics.prototype.enableTestTimer = true; break; case 'elementTimer': sessionMetrics.prototype.enableElementTimer = true; break; case 'elementTracker': sessionMetrics.prototype.enableElementTracker = true; break; case 'elementInitalPosition': sessionMetrics.prototype.enableElementInitialPosition = true; break; case 'elementFlagListenedTo': sessionMetrics.prototype.enableFlagListenedTo = true; break; case 'elementFlagMoved': sessionMetrics.prototype.enableFlagMoved = true; break; case 'elementFlagComments': sessionMetrics.prototype.enableFlagComments = true; break; } }); // Create APE specific metric functions audioEngineContext.metric.initialiseTest = function() { var sliders = document.getElementsByClassName('track-slider'); for (var i=0; i<sliders.length; i++) { audioEngineContext.audioObjects[i].metric.initialised(convSliderPosToRate(i)); } }; audioEngineContext.metric.sliderMoveStart = function(id) { if (this.data == -1) { this.data = id; } else { console.log('ERROR: Metric tracker detecting two moves!'); this.data = -1; } }; audioEngineContext.metric.sliderMoved = function() { var time = audioEngineContext.timer.getTestTime(); var id = this.data; this.data = -1; var position = convSliderPosToRate(id); console.log('slider ' + id + ': '+ position + ' (' + time + ')'); // DEBUG/SAFETY: show position and slider id if (audioEngineContext.timer.testStarted) { audioEngineContext.audioObjects[id].metric.moved(time,position); } }; audioEngineContext.metric.sliderPlayed = function(id) { var time = audioEngineContext.timer.getTestTime(); if (audioEngineContext.timer.testStarted) { if (this.lastClicked >= 0) { audioEngineContext.audioObjects[this.lastClicked].metric.listening(time); } this.lastClicked = id; audioEngineContext.audioObjects[id].metric.listening(time); } console.log('slider ' + id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id }; // Create the top div for the Title element var titleAttr = xmlSetup[0].attributes['title']; var title = document.createElement('div'); title.className = "title"; title.align = "center"; var titleSpan = document.createElement('span'); // Set title to that defined in XML, else set to default if (titleAttr != undefined) { titleSpan.innerHTML = titleAttr.value; } else { titleSpan.innerHTML = 'Listening test'; } // Insert the titleSpan element into the title div element. title.appendChild(titleSpan); var pagetitle = document.createElement('div'); pagetitle.className = "pageTitle"; pagetitle.align = "center"; var titleSpan = document.createElement('span'); titleSpan.id = "pageTitle"; pagetitle.appendChild(titleSpan); // Store the return URL path in global projectReturn projectReturn = xmlSetup[0].attributes['projectReturn'].value; // Create Interface buttons! var interfaceButtons = document.createElement('div'); interfaceButtons.id = 'interface-buttons'; // MANUAL DOWNLOAD POINT // If project return is null, this MUST be specified as the location to create the download link var downloadPoint = document.createElement('div'); downloadPoint.id = 'download-point'; // Create playback start/stop points var playback = document.createElement("button"); playback.innerHTML = 'Stop'; playback.id = 'playback-button'; // onclick function. Check if it is playing or not, call the correct function in the // audioEngine, change the button text to reflect the next state. playback.onclick = function() { if (audioEngineContext.status == 1) { audioEngineContext.stop(); this.innerHTML = 'Stop'; var time = audioEngineContext.timer.getTestTime(); console.log('Stopped at ' + time); // DEBUG/SAFETY } }; // Create Submit (save) button var submit = document.createElement("button"); submit.innerHTML = 'Submit'; submit.onclick = buttonSubmitClick; submit.id = 'submit-button'; // Append the interface buttons into the interfaceButtons object. interfaceButtons.appendChild(playback); interfaceButtons.appendChild(submit); interfaceButtons.appendChild(downloadPoint); // Now create the slider and HTML5 canvas boxes // Create the div box to center align var sliderBox = document.createElement('div'); sliderBox.className = 'sliderCanvasDiv'; sliderBox.id = 'sliderCanvasHolder'; // Create the slider box to hold the slider elements var canvas = document.createElement('div'); canvas.id = 'slider'; canvas.align = "left"; canvas.addEventListener('dragover',function(event){ event.preventDefault(); return false; },false); var sliderMargin = document.createAttribute('marginsize'); sliderMargin.nodeValue = 42; // Set default margins to 42px either side // Must have a known EXACT width, as this is used later to determine the ratings var w = (Number(sliderMargin.nodeValue)+8)*2; canvas.style.width = width - w +"px"; canvas.style.marginLeft = sliderMargin.nodeValue +'px'; canvas.setAttributeNode(sliderMargin); sliderBox.appendChild(canvas); // Create the div to hold any scale objects var scale = document.createElement('div'); scale.className = 'sliderScale'; scale.id = 'sliderScaleHolder'; scale.align = 'left'; sliderBox.appendChild(scale); // Global parent for the comment boxes on the page var feedbackHolder = document.createElement('div'); feedbackHolder.id = 'feedbackHolder'; testContent.style.zIndex = 1; insertPoint.innerHTML = null; // Clear the current schema currentState = 'preTest'; // Inject into HTML testContent.appendChild(title); // Insert the title testContent.appendChild(pagetitle); testContent.appendChild(interfaceButtons); testContent.appendChild(sliderBox); testContent.appendChild(feedbackHolder); insertPoint.appendChild(testContent); // Load the full interface testState.initialise(); testState.advanceState(); } function loadTest(textXML) { // Reset audioEngineContext.Metric globals for new test audioEngineContext.newTestPage(); var id = textXML.id; var feedbackHolder = document.getElementById('feedbackHolder'); var canvas = document.getElementById('slider'); feedbackHolder.innerHTML = null; canvas.innerHTML = null; // Setup question title var interfaceObj = $(textXML).find('interface'); var titleNode = interfaceObj.find('title'); if (titleNode[0] != undefined) { document.getElementById('pageTitle').textContent = titleNode[0].textContent; } var positionScale = canvas.style.width.substr(0,canvas.style.width.length-2); var offset = Number(document.getElementById('slider').attributes['marginsize'].value); var scale = document.getElementById('sliderScaleHolder'); scale.innerHTML = null; interfaceObj.find('scale').each(function(index,scaleObj){ var value = document.createAttribute('value'); var position = Number(scaleObj.attributes['position'].value)*0.01; value.nodeValue = position; var pixelPosition = (position*positionScale)+offset; var scaleDOM = document.createElement('span'); scaleDOM.textContent = scaleObj.textContent; scale.appendChild(scaleDOM); scaleDOM.style.left = Math.floor((pixelPosition-($(scaleDOM).width()/2)))+'px'; scaleDOM.setAttributeNode(value); }); // Extract the hostURL attribute. If not set, create an empty string. var hostURL = textXML.attributes['hostURL']; if (hostURL == undefined) { hostURL = ""; } else { hostURL = hostURL.value; } // Extract the sampleRate. If set, convert the string to a Number. var hostFs = textXML.attributes['sampleRate']; if (hostFs != undefined) { hostFs = Number(hostFs.value); } /// CHECK FOR SAMPLE RATE COMPATIBILITY if (hostFs != undefined) { if (Number(hostFs) != audioContext.sampleRate) { var errStr = 'Sample rates do not match! Requested '+Number(hostFs)+', got '+audioContext.sampleRate+'. Please set the sample rate to match before completing this test.'; alert(errStr); return; } } var commentShow = textXML.attributes['elementComments']; if (commentShow != undefined) { if (commentShow.value == 'false') {commentShow = false;} else {commentShow = true;} } else {commentShow = true;} var loopPlayback = textXML.attributes['loop']; if (loopPlayback != undefined) { loopPlayback = loopPlayback.value; if (loopPlayback == 'true') { loopPlayback = true; } else { loopPlayback = false; } } else { loopPlayback = false; } audioEngineContext.loopPlayback = loopPlayback; loopPlayback = false; // Create AudioEngine bindings for playback if (loopPlayback) { audioEngineContext.selectedTrack = function(id) { for (var i=0; i<this.audioObjects.length; i++) { if (id == i) { this.audioObjects[i].outputGain.gain.value = 1.0; } else { this.audioObjects[i].outputGain.gain.value = 0.0; } } }; } else { audioEngineContext.selectedTrack = function(id) { for (var i=0; i<this.audioObjects.length; i++) { this.audioObjects[i].outputGain.gain.value = 0.0; this.audioObjects[i].stop(); } if (this.status == 1) { this.audioObjects[id].outputGain.gain.value = 1.0; this.audioObjects[id].play(audioContext.currentTime+0.01); } }; } currentTestHolder = document.createElement('audioHolder'); currentTestHolder.id = textXML.id; currentTestHolder.repeatCount = textXML.attributes['repeatCount'].value; var randomise = textXML.attributes['randomiseOrder']; if (randomise != undefined) {randomise = randomise.value;} else {randomise = false;} var audioElements = $(textXML).find('audioElements'); currentTrackOrder = []; audioElements.each(function(index,element){ // Find any blind-repeats // Not implemented yet, but just in case currentTrackOrder[index] = element; }); if (randomise) { currentTrackOrder = randomiseOrder(currentTrackOrder); } // Delete any previous audioObjects associated with the audioEngine audioEngineContext.audioObjects = []; // Find all the audioElements from the audioHolder $(currentTrackOrder).each(function(index,element){ // Find URL of track // In this jQuery loop, variable 'this' holds the current audioElement. // Now load each audio sample. First create the new track by passing the full URL var trackURL = hostURL + this.attributes['url'].value; audioEngineContext.newTrack(trackURL); if (commentShow) { // Create document objects to hold the comment boxes var trackComment = document.createElement('div'); trackComment.className = 'comment-div'; // Create a string next to each comment asking for a comment var trackString = document.createElement('span'); trackString.innerHTML = 'Comment on track '+index; // Create the HTML5 comment box 'textarea' var trackCommentBox = document.createElement('textarea'); trackCommentBox.rows = '4'; trackCommentBox.cols = '100'; trackCommentBox.name = 'trackComment'+index; trackCommentBox.className = 'trackComment'; var br = document.createElement('br'); // Add to the holder. trackComment.appendChild(trackString); trackComment.appendChild(br); trackComment.appendChild(trackCommentBox); feedbackHolder.appendChild(trackComment); } // Create a slider per track var trackSliderObj = document.createElement('div'); trackSliderObj.className = 'track-slider'; trackSliderObj.id = 'track-slider-'+index; // Distribute it randomnly var w = window.innerWidth - 100; w = Math.random()*w; trackSliderObj.style.left = Math.floor(w)+50+'px'; trackSliderObj.innerHTML = '<span>'+index+'</span>'; trackSliderObj.draggable = true; trackSliderObj.ondragend = dragEnd; trackSliderObj.ondragstart = function() { var id = Number(this.id.substr(13,2)); // Maximum theoretical tracks is 99! audioEngineContext.metric.sliderMoveStart(id); }; // Onclick, switch playback to that track trackSliderObj.onclick = function() { // Start the test on first click, that way timings are more accurate. audioEngineContext.play(); // Get the track ID from the object ID var id = Number(this.id.substr(13,2)); // Maximum theoretical tracks is 99! //audioEngineContext.metric.sliderPlayed(id); audioEngineContext.selectedTrack(id); // Currently playing track red, rest green document.getElementById('track-slider-'+index).style.backgroundColor = "#FF0000"; for (var i = 0; i<$(currentTrackOrder).length; i++) { if (i!=index) // Make all other sliders green { document.getElementById('track-slider-'+i).style.backgroundColor = "rgb(100,200,100)"; } } }; canvas.appendChild(trackSliderObj); }); // Append any commentQuestion boxes var commentQuestions = $(textXML).find('CommentQuestion'); $(commentQuestions).each(function(index,element) { // Create document objects to hold the comment boxes var trackComment = document.createElement('div'); trackComment.className = 'comment-div commentQuestion'; trackComment.id = element.attributes['id'].value; // Create a string next to each comment asking for a comment var trackString = document.createElement('span'); trackString.innerHTML = element.textContent; // Create the HTML5 comment box 'textarea' var trackCommentBox = document.createElement('textarea'); trackCommentBox.rows = '4'; trackCommentBox.cols = '100'; trackCommentBox.name = 'commentQuestion'+index; trackCommentBox.className = 'trackComment'; var br = document.createElement('br'); // Add to the holder. trackComment.appendChild(trackString); trackComment.appendChild(br); trackComment.appendChild(trackCommentBox); feedbackHolder.appendChild(trackComment); }); } function dragEnd(ev) { // Function call when a div has been dropped var slider = document.getElementById('slider'); var marginSize = Number(slider.attributes['marginsize'].value); var w = slider.style.width; w = Number(w.substr(0,w.length-2)); var x = ev.x - ev.view.screenX; if (x >= marginSize && x < w+marginSize) { this.style.left = (x)+'px'; } else { if (x<marginSize) { this.style.left = marginSize+'px'; } else { this.style.left = (w+marginSize) + 'px'; } } audioEngineContext.metric.sliderMoved(); } function buttonSubmitClick() // TODO: Only when all songs have been played! { hasBeenPlayed = audioEngineContext.checkAllPlayed(); if (hasBeenPlayed.length == 0) { if (audioEngineContext.status == 1) { var playback = document.getElementById('playback-button'); playback.click(); // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options } else { if (audioEngineContext.timer.testStarted == false) { alert('You have not started the test! Please press start to begin the test!'); return; } } testState.advanceState(); } else // if a fragment has not been played yet { str = ""; if (hasBeenPlayed.length > 1) { for (var i=0; i<hasBeenPlayed.length; i++) { str = str + hasBeenPlayed[i]; if (i < hasBeenPlayed.length-2){ str += ", "; } else if (i == hasBeenPlayed.length-2) { str += " or "; } } alert('You have not played fragments ' + str + ' yet. Please listen, rate and comment all samples before submitting.'); } else { alert('You have not played fragment ' + hasBeenPlayed[0] + ' yet. Please listen, rate and comment all samples before submitting.'); } return; } } function convSliderPosToRate(id) { var w = document.getElementById('slider').style.width; var marginsize = Number(document.getElementById('slider').attributes['marginsize'].value); var maxPix = w.substr(0,w.length-2); var slider = document.getElementsByClassName('track-slider')[id]; var pix = slider.style.left; pix = pix.substr(0,pix.length-2); var rate = (pix-marginsize)/maxPix; return rate; } function resizeWindow(event){ // Function called when the window has been resized. // MANDATORY FUNCTION // Store the slider marker values var holdValues = []; $(".track-slider").each(function(index,sliderObj){ holdValues.push(convSliderPosToRate(index)); }); var width = event.target.innerWidth; var canvas = document.getElementById('sliderCanvasHolder'); var sliderDiv = canvas.children[0]; var sliderScaleDiv = canvas.children[1]; var marginsize = Number(sliderDiv.attributes['marginsize'].value); var w = (marginsize+8)*2; sliderDiv.style.width = width - w + 'px'; var width = width - w; // Move sliders into new position $(".track-slider").each(function(index,sliderObj){ var pos = holdValues[index]; var pix = pos * width; sliderObj.style.left = pix+marginsize+'px'; }); // Move scale labels $(sliderScaleDiv.children).each(function(index,scaleObj){ var position = Number(scaleObj.attributes['value'].value); var pixelPosition = (position*width)+marginsize; scaleObj.style.left = Math.floor((pixelPosition-($(scaleObj).width()/2)))+'px'; }); } function pageXMLSave(store, testXML, testId) { // Saves a specific test page var xmlDoc = store; // Check if any session wide metrics are enabled var commentShow = testXML.attributes['elementComments']; if (commentShow != undefined) { if (commentShow.value == 'false') {commentShow = false;} else {commentShow = true;} } else {commentShow = true;} var metric = document.createElement('metric'); if (audioEngineContext.metric.enableTestTimer) { var testTime = document.createElement('metricResult'); testTime.id = 'testTime'; testTime.textContent = audioEngineContext.timer.testDuration; metric.appendChild(testTime); } xmlDoc.appendChild(metric); var trackSliderObjects = document.getElementsByClassName('track-slider'); var commentObjects = document.getElementsByClassName('comment-div'); for (var i=0; i<trackSliderObjects.length; i++) { var audioElement = document.createElement('audioElement'); audioElement.id = currentTrackOrder[i].attributes['id'].value; audioElement.url = currentTrackOrder[i].attributes['url'].value; var value = document.createElement('value'); value.innerHTML = convSliderPosToRate(i); if (commentShow) { var comment = document.createElement("comment"); var question = document.createElement("question"); var response = document.createElement("response"); question.textContent = commentObjects[i].children[0].textContent; response.textContent = commentObjects[i].children[2].value; console.log('Comment ' + i + ': ' + commentObjects[i].children[2].value); // DEBUG/SAFETY comment.appendChild(question); comment.appendChild(response); audioElement.appendChild(comment); } audioElement.appendChild(value); // Check for any per element metrics var metric = document.createElement('metric'); var elementMetric = audioEngineContext.audioObjects[i].metric; if (audioEngineContext.metric.enableElementTimer) { var elementTimer = document.createElement('metricResult'); elementTimer.id = 'elementTimer'; elementTimer.textContent = elementMetric.listenedTimer; metric.appendChild(elementTimer); } if (audioEngineContext.metric.enableElementTracker) { var elementTrackerFull = document.createElement('metricResult'); elementTrackerFull.id = 'elementTrackerFull'; var data = elementMetric.movementTracker; for (var k=0; k<data.length; k++) { var timePos = document.createElement('timePos'); timePos.id = k; var time = document.createElement('time'); time.textContent = data[k][0]; var position = document.createElement('position'); position.textContent = data[k][1]; timePos.appendChild(time); timePos.appendChild(position); elementTrackerFull.appendChild(timePos); } metric.appendChild(elementTrackerFull); } if (audioEngineContext.metric.enableElementInitialPosition) { var elementInitial = document.createElement('metricResult'); elementInitial.id = 'elementInitialPosition'; elementInitial.textContent = elementMetric.initialPosition; metric.appendChild(elementInitial); } if (audioEngineContext.metric.enableFlagListenedTo) { var flagListenedTo = document.createElement('metricResult'); flagListenedTo.id = 'elementFlagListenedTo'; flagListenedTo.textContent = elementMetric.wasListenedTo; metric.appendChild(flagListenedTo); } if (audioEngineContext.metric.enableFlagMoved) { var flagMoved = document.createElement('metricResult'); flagMoved.id = 'elementFlagMoved'; flagMoved.textContent = elementMetric.wasMoved; metric.appendChild(flagMoved); } if (audioEngineContext.metric.enableFlagComments) { var flagComments = document.createElement('metricResult'); flagComments.id = 'elementFlagComments'; if (response.textContent.length == 0) {flag.textContent = 'false';} else {flag.textContet = 'true';} metric.appendChild(flagComments); } audioElement.appendChild(metric); xmlDoc.appendChild(audioElement); } var commentQuestion = document.getElementsByClassName('commentQuestion'); for (var i=0; i<commentQuestion.length; i++) { var cqHolder = document.createElement('CommentQuestion'); var comment = document.createElement('comment'); var question = document.createElement('question'); cqHolder.id = commentQuestion[i].id; comment.textContent = commentQuestion[i].children[2].value; question.textContent = commentQuestion[i].children[0].textContent; console.log('Question ' + i + ': ' + commentQuestion[i].children[2].value); // DEBUG/SAFETY cqHolder.appendChild(question); cqHolder.appendChild(comment); xmlDoc.appendChild(cqHolder); } store = xmlDoc; }