Mercurial > hg > webaudioevaluationtool
comparison core.js @ 444:9c9fd68693b1
Merge. Pull of revision info from dev_main.
author | Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk> |
---|---|
date | Wed, 23 Dec 2015 14:36:00 +0000 |
parents | da368f23bcd3 |
children | 63c4163fc705 |
comparison
equal
deleted
inserted
replaced
442:1081368deed7 | 444:9c9fd68693b1 |
---|---|
14 var testState; | 14 var testState; |
15 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order | 15 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order |
16 var audioEngineContext; // The custome AudioEngine object | 16 var audioEngineContext; // The custome AudioEngine object |
17 var projectReturn; // Hold the URL for the return | 17 var projectReturn; // Hold the URL for the return |
18 | 18 |
19 | |
20 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it | |
21 AudioBufferSourceNode.prototype.owner = undefined; | |
22 | |
23 window.onload = function() { | 19 window.onload = function() { |
24 // Function called once the browser has loaded all files. | 20 // Function called once the browser has loaded all files. |
25 // This should perform any initial commands such as structure / loading documents | 21 // This should perform any initial commands such as structure / loading documents |
26 | 22 |
27 // Create a web audio API context | 23 // Create a web audio API context |
30 audioContext = new AudioContext; | 26 audioContext = new AudioContext; |
31 | 27 |
32 // Create test state | 28 // Create test state |
33 testState = new stateMachine(); | 29 testState = new stateMachine(); |
34 | 30 |
35 // Create the audio engine object | |
36 audioEngineContext = new AudioEngine(); | |
37 | |
38 // Create the popup interface object | 31 // Create the popup interface object |
39 popup = new interfacePopup(); | 32 popup = new interfacePopup(); |
40 | 33 |
41 // Create the specification object | 34 // Create the specification object |
42 specification = new Specification(); | 35 specification = new Specification(); |
43 | 36 |
44 // Create the interface object | 37 // Create the interface object |
45 interfaceContext = new Interface(specification); | 38 interfaceContext = new Interface(specification); |
39 // Define window callbacks for interface | |
40 window.onresize = function(event){interfaceContext.resizeWindow(event);}; | |
41 | |
42 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it | |
43 AudioBufferSourceNode.prototype.owner = undefined; | |
44 // Add a prototype to the bufferNode to hold the desired LINEAR gain | |
45 AudioBuffer.prototype.gain = 1.0; | |
46 // Add a prototype to the bufferNode to hold the computed LUFS loudness | |
47 AudioBuffer.prototype.lufs = -23; | |
46 }; | 48 }; |
49 | |
50 function loadProjectSpec(url) { | |
51 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data | |
52 // If url is null, request client to upload project XML document | |
53 var r = new XMLHttpRequest(); | |
54 r.open('GET',url,true); | |
55 r.onload = function() { | |
56 loadProjectSpecCallback(r.response); | |
57 }; | |
58 r.send(); | |
59 }; | |
60 | |
61 function loadProjectSpecCallback(response) { | |
62 // Function called after asynchronous download of XML project specification | |
63 //var decode = $.parseXML(response); | |
64 //projectXML = $(decode); | |
65 | |
66 var parse = new DOMParser(); | |
67 projectXML = parse.parseFromString(response,'text/xml'); | |
68 var errorNode = projectXML.getElementsByTagName('parsererror'); | |
69 if (errorNode.length >= 1) | |
70 { | |
71 var msg = document.createElement("h3"); | |
72 msg.textContent = "FATAL ERROR"; | |
73 var span = document.createElement("span"); | |
74 span.textContent = "The XML parser returned the following errors when decoding your XML file"; | |
75 document.getElementsByTagName('body')[0].innerHTML = null; | |
76 document.getElementsByTagName('body')[0].appendChild(msg); | |
77 document.getElementsByTagName('body')[0].appendChild(span); | |
78 document.getElementsByTagName('body')[0].appendChild(errorNode[0]); | |
79 return; | |
80 } | |
81 | |
82 // Build the specification | |
83 specification.decode(projectXML); | |
84 | |
85 // Detect the interface to use and load the relevant javascripts. | |
86 var interfaceJS = document.createElement('script'); | |
87 interfaceJS.setAttribute("type","text/javascript"); | |
88 if (specification.interfaceType == 'APE') { | |
89 interfaceJS.setAttribute("src","ape.js"); | |
90 | |
91 // APE comes with a css file | |
92 var css = document.createElement('link'); | |
93 css.rel = 'stylesheet'; | |
94 css.type = 'text/css'; | |
95 css.href = 'ape.css'; | |
96 | |
97 document.getElementsByTagName("head")[0].appendChild(css); | |
98 } else if (specification.interfaceType == "MUSHRA") | |
99 { | |
100 interfaceJS.setAttribute("src","mushra.js"); | |
101 | |
102 // MUSHRA comes with a css file | |
103 var css = document.createElement('link'); | |
104 css.rel = 'stylesheet'; | |
105 css.type = 'text/css'; | |
106 css.href = 'mushra.css'; | |
107 | |
108 document.getElementsByTagName("head")[0].appendChild(css); | |
109 } | |
110 document.getElementsByTagName("head")[0].appendChild(interfaceJS); | |
111 | |
112 // Create the audio engine object | |
113 audioEngineContext = new AudioEngine(specification); | |
114 | |
115 testState.stateMap.push(specification.preTest); | |
116 | |
117 $(specification.audioHolders).each(function(index,elem){ | |
118 testState.stateMap.push(elem); | |
119 $(elem.audioElements).each(function(i,audioElem){ | |
120 var URL = audioElem.parent.hostURL + audioElem.url; | |
121 var buffer = null; | |
122 for (var i=0; i<audioEngineContext.buffers.length; i++) | |
123 { | |
124 if (URL == audioEngineContext.buffers[i].url) | |
125 { | |
126 buffer = audioEngineContext.buffers[i]; | |
127 break; | |
128 } | |
129 } | |
130 if (buffer == null) | |
131 { | |
132 buffer = new audioEngineContext.bufferObj(); | |
133 buffer.getMedia(URL); | |
134 audioEngineContext.buffers.push(buffer); | |
135 } | |
136 }); | |
137 }); | |
138 | |
139 testState.stateMap.push(specification.postTest); | |
140 } | |
141 | |
142 function createProjectSave(destURL) { | |
143 // Save the data from interface into XML and send to destURL | |
144 // If destURL is null then download XML in client | |
145 // Now time to render file locally | |
146 var xmlDoc = interfaceXMLSave(); | |
147 var parent = document.createElement("div"); | |
148 parent.appendChild(xmlDoc); | |
149 var file = [parent.innerHTML]; | |
150 if (destURL == "null" || destURL == undefined) { | |
151 var bb = new Blob(file,{type : 'application/xml'}); | |
152 var dnlk = window.URL.createObjectURL(bb); | |
153 var a = document.createElement("a"); | |
154 a.hidden = ''; | |
155 a.href = dnlk; | |
156 a.download = "save.xml"; | |
157 a.textContent = "Save File"; | |
158 | |
159 popup.showPopup(); | |
160 popup.popupContent.innerHTML = null; | |
161 popup.popupContent.appendChild(a); | |
162 } else { | |
163 var xmlhttp = new XMLHttpRequest; | |
164 xmlhttp.open("POST",destURL,true); | |
165 xmlhttp.setRequestHeader('Content-Type', 'text/xml'); | |
166 xmlhttp.onerror = function(){ | |
167 console.log('Error saving file to server! Presenting download locally'); | |
168 createProjectSave(null); | |
169 }; | |
170 xmlhttp.onreadystatechange = function() { | |
171 console.log(xmlhttp.status); | |
172 if (xmlhttp.status != 200 && xmlhttp.readyState == 4) { | |
173 createProjectSave(null); | |
174 } else { | |
175 if (xmlhttp.responseXML == null) | |
176 { | |
177 return createProjectSave(null); | |
178 } | |
179 var response = xmlhttp.responseXML.childNodes[0]; | |
180 if (response.getAttribute('state') == "OK") | |
181 { | |
182 var file = response.getElementsByTagName('file')[0]; | |
183 console.log('Save OK: Filename '+file.textContent+','+file.getAttribute('bytes')+'B'); | |
184 popup.showPopup(); | |
185 popup.popupContent.innerHTML = null; | |
186 popup.popupContent.textContent = "Thank you!"; | |
187 } else { | |
188 var message = response.getElementsByTagName('message')[0]; | |
189 errorSessionDump(message.textContent); | |
190 } | |
191 } | |
192 }; | |
193 xmlhttp.send(file); | |
194 } | |
195 } | |
196 | |
197 function errorSessionDump(msg){ | |
198 // Create the partial interface XML save | |
199 // Include error node with message on why the dump occured | |
200 popup.showPopup(); | |
201 popup.popupContent.innerHTML = null; | |
202 var err = document.createElement('error'); | |
203 var parent = document.createElement("div"); | |
204 if (typeof msg === "object") | |
205 { | |
206 err.appendChild(msg); | |
207 popup.popupContent.appendChild(msg); | |
208 | |
209 } else { | |
210 err.textContent = msg; | |
211 popup.popupContent.innerHTML = "ERROR : "+msg; | |
212 } | |
213 var xmlDoc = interfaceXMLSave(); | |
214 xmlDoc.appendChild(err); | |
215 parent.appendChild(xmlDoc); | |
216 var file = [parent.innerHTML]; | |
217 var bb = new Blob(file,{type : 'application/xml'}); | |
218 var dnlk = window.URL.createObjectURL(bb); | |
219 var a = document.createElement("a"); | |
220 a.hidden = ''; | |
221 a.href = dnlk; | |
222 a.download = "save.xml"; | |
223 a.textContent = "Save File"; | |
224 | |
225 | |
226 | |
227 popup.popupContent.appendChild(a); | |
228 } | |
229 | |
230 // Only other global function which must be defined in the interface class. Determines how to create the XML document. | |
231 function interfaceXMLSave(){ | |
232 // Create the XML string to be exported with results | |
233 var xmlDoc = document.createElement("BrowserEvaluationResult"); | |
234 var projectDocument = specification.projectXML; | |
235 projectDocument.setAttribute('file-name',url); | |
236 xmlDoc.appendChild(projectDocument); | |
237 xmlDoc.appendChild(returnDateNode()); | |
238 xmlDoc.appendChild(interfaceContext.returnNavigator()); | |
239 for (var i=0; i<testState.stateResults.length; i++) | |
240 { | |
241 xmlDoc.appendChild(testState.stateResults[i]); | |
242 } | |
243 | |
244 return xmlDoc; | |
245 } | |
246 | |
247 function linearToDecibel(gain) | |
248 { | |
249 return 20.0*Math.log10(gain); | |
250 } | |
251 | |
252 function decibelToLinear(gain) | |
253 { | |
254 return Math.pow(10,gain/20.0); | |
255 } | |
47 | 256 |
48 function interfacePopup() { | 257 function interfacePopup() { |
49 // Creates an object to manage the popup | 258 // Creates an object to manage the popup |
50 this.popup = null; | 259 this.popup = null; |
51 this.popupContent = null; | 260 this.popupContent = null; |
54 this.buttonProceed = null; | 263 this.buttonProceed = null; |
55 this.buttonPrevious = null; | 264 this.buttonPrevious = null; |
56 this.popupOptions = null; | 265 this.popupOptions = null; |
57 this.currentIndex = null; | 266 this.currentIndex = null; |
58 this.responses = null; | 267 this.responses = null; |
268 $(window).keypress(function(e){ | |
269 if (e.keyCode == 13 && popup.popup.style.visibility == 'visible') | |
270 { | |
271 console.log(e); | |
272 popup.buttonProceed.onclick(); | |
273 e.preventDefault(); | |
274 } | |
275 }); | |
59 | 276 |
60 this.createPopup = function(){ | 277 this.createPopup = function(){ |
61 // Create popup window interface | 278 // Create popup window interface |
62 var insertPoint = document.getElementById("topLevelBody"); | 279 var insertPoint = document.getElementById("topLevelBody"); |
63 var blank = document.createElement('div'); | 280 var blank = document.createElement('div'); |
131 this.popup.style.zIndex = 3; | 348 this.popup.style.zIndex = 3; |
132 this.popup.style.visibility = 'visible'; | 349 this.popup.style.visibility = 'visible'; |
133 var blank = document.getElementsByClassName('testHalt')[0]; | 350 var blank = document.getElementsByClassName('testHalt')[0]; |
134 blank.style.zIndex = 2; | 351 blank.style.zIndex = 2; |
135 blank.style.visibility = 'visible'; | 352 blank.style.visibility = 'visible'; |
136 $(window).keypress(function(e){ | |
137 if (e.keyCode == 13 && popup.popup.style.visibility == 'visible') | |
138 { | |
139 popup.buttonProceed.onclick(); | |
140 } | |
141 }); | |
142 }; | 353 }; |
143 | 354 |
144 this.hidePopup = function(){ | 355 this.hidePopup = function(){ |
145 this.popup.style.zIndex = -1; | 356 this.popup.style.zIndex = -1; |
146 this.popup.style.visibility = 'hidden'; | 357 this.popup.style.visibility = 'hidden'; |
147 var blank = document.getElementsByClassName('testHalt')[0]; | 358 var blank = document.getElementsByClassName('testHalt')[0]; |
148 blank.style.zIndex = -2; | 359 blank.style.zIndex = -2; |
149 blank.style.visibility = 'hidden'; | 360 blank.style.visibility = 'hidden'; |
150 this.buttonPrevious.style.visibility = 'inherit'; | 361 this.buttonPrevious.style.visibility = 'inherit'; |
151 $(window).keypress(function(e){}); | |
152 }; | 362 }; |
153 | 363 |
154 this.postNode = function() { | 364 this.postNode = function() { |
155 // This will take the node from the popupOptions and display it | 365 // This will take the node from the popupOptions and display it |
156 var node = this.popupOptions[this.currentIndex]; | 366 var node = this.popupOptions[this.currentIndex]; |
304 } | 514 } |
305 this.responses.appendChild(hold); | 515 this.responses.appendChild(hold); |
306 } else if (node.type == "radio") { | 516 } else if (node.type == "radio") { |
307 var optHold = this.popupResponse; | 517 var optHold = this.popupResponse; |
308 var hold = document.createElement('radio'); | 518 var hold = document.createElement('radio'); |
519 console.log("Checkbox: "+ node.statement); | |
309 var responseID = null; | 520 var responseID = null; |
310 var i=0; | 521 var i=0; |
311 while(responseID == null) { | 522 while(responseID == null) { |
312 var input = optHold.childNodes[i].getElementsByTagName('input')[0]; | 523 var input = optHold.childNodes[i].getElementsByTagName('input')[0]; |
313 if (input.checked == true) { | 524 if (input.checked == true) { |
314 responseID = i; | 525 responseID = i; |
526 console.log("Selected: "+ node.options[i].name); | |
315 } | 527 } |
316 i++; | 528 i++; |
317 } | 529 } |
318 hold.id = node.id; | 530 hold.id = node.id; |
319 hold.setAttribute('name',node.options[responseID].name); | 531 hold.setAttribute('name',node.options[responseID].name); |
394 break; | 606 break; |
395 } | 607 } |
396 } | 608 } |
397 } | 609 } |
398 } | 610 } |
611 }; | |
612 | |
613 this.resize = function(event) | |
614 { | |
615 // Called on window resize; | |
616 this.popup.style.left = (window.innerWidth/2)-250 + 'px'; | |
617 this.popup.style.top = (window.innerHeight/2)-125 + 'px'; | |
618 var blank = document.getElementsByClassName('testHalt')[0]; | |
619 blank.style.width = window.innerWidth; | |
620 blank.style.height = window.innerHeight; | |
399 }; | 621 }; |
400 } | 622 } |
401 | 623 |
402 function advanceState() | 624 function advanceState() |
403 { | 625 { |
456 createProjectSave(specification.projectReturn); | 678 createProjectSave(specification.projectReturn); |
457 } else { | 679 } else { |
458 this.currentStateMap = this.stateMap[this.stateIndex]; | 680 this.currentStateMap = this.stateMap[this.stateIndex]; |
459 if (this.currentStateMap.type == "audioHolder") { | 681 if (this.currentStateMap.type == "audioHolder") { |
460 console.log('Loading test page'); | 682 console.log('Loading test page'); |
461 loadTest(this.currentStateMap); | 683 interfaceContext.newPage(this.currentStateMap); |
462 this.initialiseInnerState(this.currentStateMap); | 684 this.initialiseInnerState(this.currentStateMap); |
463 } else if (this.currentStateMap.type == "pretest" || this.currentStateMap.type == "posttest") { | 685 } else if (this.currentStateMap.type == "pretest" || this.currentStateMap.type == "posttest") { |
464 if (this.currentStateMap.options.length >= 1) { | 686 if (this.currentStateMap.options.length >= 1) { |
465 popup.initState(this.currentStateMap); | 687 popup.initState(this.currentStateMap); |
466 } else { | 688 } else { |
475 } | 697 } |
476 }; | 698 }; |
477 | 699 |
478 this.testPageCompleted = function(store, testXML, testId) { | 700 this.testPageCompleted = function(store, testXML, testId) { |
479 // Function called each time a test page has been completed | 701 // Function called each time a test page has been completed |
480 // Can be used to over-rule default behaviour | 702 var metric = document.createElement('metric'); |
481 | 703 if (audioEngineContext.metric.enableTestTimer) |
704 { | |
705 var testTime = document.createElement('metricResult'); | |
706 testTime.id = 'testTime'; | |
707 testTime.textContent = audioEngineContext.timer.testDuration; | |
708 metric.appendChild(testTime); | |
709 } | |
710 store.appendChild(metric); | |
711 var audioObjects = audioEngineContext.audioObjects; | |
712 for (var i=0; i<audioObjects.length; i++) | |
713 { | |
714 var audioElement = audioEngineContext.audioObjects[i].exportXMLDOM(); | |
715 audioElement.setAttribute('presentedId',i); | |
716 store.appendChild(audioElement); | |
717 } | |
718 $(interfaceContext.commentQuestions).each(function(index,element){ | |
719 var node = element.exportXMLDOM(); | |
720 store.appendChild(node); | |
721 }); | |
482 pageXMLSave(store, testXML); | 722 pageXMLSave(store, testXML); |
483 }; | 723 }; |
484 | 724 |
485 this.initialiseInnerState = function(node) { | 725 this.initialiseInnerState = function(node) { |
486 // Parses the received testXML for pre and post test options | 726 // Parses the received testXML for pre and post test options |
516 }; | 756 }; |
517 | 757 |
518 this.previousState = function(){}; | 758 this.previousState = function(){}; |
519 } | 759 } |
520 | 760 |
521 function testEnded(testId) | 761 function AudioEngine(specification) { |
522 { | |
523 pageXMLSave(testId); | |
524 if (testXMLSetups.length-1 > testId) | |
525 { | |
526 // Yes we have another test to perform | |
527 testId = (Number(testId)+1); | |
528 currentState = 'testRun-'+testId; | |
529 loadTest(testId); | |
530 } else { | |
531 console.log('Testing Completed!'); | |
532 currentState = 'postTest'; | |
533 // Check for any post tests | |
534 var xmlSetup = projectXML.find('setup'); | |
535 var postTest = xmlSetup.find('PostTest')[0]; | |
536 popup.initState(postTest); | |
537 } | |
538 } | |
539 | |
540 function loadProjectSpec(url) { | |
541 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data | |
542 // If url is null, request client to upload project XML document | |
543 var r = new XMLHttpRequest(); | |
544 r.open('GET',url,true); | |
545 r.onload = function() { | |
546 loadProjectSpecCallback(r.response); | |
547 }; | |
548 r.send(); | |
549 }; | |
550 | |
551 function loadProjectSpecCallback(response) { | |
552 // Function called after asynchronous download of XML project specification | |
553 //var decode = $.parseXML(response); | |
554 //projectXML = $(decode); | |
555 | |
556 var parse = new DOMParser(); | |
557 projectXML = parse.parseFromString(response,'text/xml'); | |
558 | |
559 // Build the specification | |
560 specification.decode(); | |
561 | |
562 testState.stateMap.push(specification.preTest); | |
563 | |
564 $(specification.audioHolders).each(function(index,elem){ | |
565 testState.stateMap.push(elem); | |
566 }); | |
567 | |
568 testState.stateMap.push(specification.postTest); | |
569 | |
570 // Obtain the metrics enabled | |
571 $(specification.metrics).each(function(index,node){ | |
572 var enabled = node.textContent; | |
573 switch(node.enabled) | |
574 { | |
575 case 'testTimer': | |
576 sessionMetrics.prototype.enableTestTimer = true; | |
577 break; | |
578 case 'elementTimer': | |
579 sessionMetrics.prototype.enableElementTimer = true; | |
580 break; | |
581 case 'elementTracker': | |
582 sessionMetrics.prototype.enableElementTracker = true; | |
583 break; | |
584 case 'elementListenTracker': | |
585 sessionMetrics.prototype.enableElementListenTracker = true; | |
586 break; | |
587 case 'elementInitialPosition': | |
588 sessionMetrics.prototype.enableElementInitialPosition = true; | |
589 break; | |
590 case 'elementFlagListenedTo': | |
591 sessionMetrics.prototype.enableFlagListenedTo = true; | |
592 break; | |
593 case 'elementFlagMoved': | |
594 sessionMetrics.prototype.enableFlagMoved = true; | |
595 break; | |
596 case 'elementFlagComments': | |
597 sessionMetrics.prototype.enableFlagComments = true; | |
598 break; | |
599 } | |
600 }); | |
601 | |
602 | |
603 | |
604 // Detect the interface to use and load the relevant javascripts. | |
605 var interfaceJS = document.createElement('script'); | |
606 interfaceJS.setAttribute("type","text/javascript"); | |
607 if (specification.interfaceType == 'APE') { | |
608 interfaceJS.setAttribute("src","ape.js"); | |
609 | |
610 // APE comes with a css file | |
611 var css = document.createElement('link'); | |
612 css.rel = 'stylesheet'; | |
613 css.type = 'text/css'; | |
614 css.href = 'ape.css'; | |
615 | |
616 document.getElementsByTagName("head")[0].appendChild(css); | |
617 } else if (specification.interfaceType == "MUSHRA") | |
618 { | |
619 interfaceJS.setAttribute("src","mushra.js"); | |
620 | |
621 // MUSHRA comes with a css file | |
622 var css = document.createElement('link'); | |
623 css.rel = 'stylesheet'; | |
624 css.type = 'text/css'; | |
625 css.href = 'mushra.css'; | |
626 | |
627 document.getElementsByTagName("head")[0].appendChild(css); | |
628 } | |
629 document.getElementsByTagName("head")[0].appendChild(interfaceJS); | |
630 | |
631 // Define window callbacks for interface | |
632 window.onresize = function(event){interfaceContext.resizeWindow(event);}; | |
633 } | |
634 | |
635 function createProjectSave(destURL) { | |
636 // Save the data from interface into XML and send to destURL | |
637 // If destURL is null then download XML in client | |
638 // Now time to render file locally | |
639 var xmlDoc = interfaceXMLSave(); | |
640 var parent = document.createElement("div"); | |
641 parent.appendChild(xmlDoc); | |
642 var file = [parent.innerHTML]; | |
643 if (destURL == "null" || destURL == undefined) { | |
644 var bb = new Blob(file,{type : 'application/xml'}); | |
645 var dnlk = window.URL.createObjectURL(bb); | |
646 var a = document.createElement("a"); | |
647 a.hidden = ''; | |
648 a.href = dnlk; | |
649 a.download = "save.xml"; | |
650 a.textContent = "Save File"; | |
651 | |
652 popup.showPopup(); | |
653 popup.popupContent.innerHTML = null; | |
654 popup.popupContent.appendChild(a); | |
655 } else { | |
656 var xmlhttp = new XMLHttpRequest; | |
657 xmlhttp.open("POST",destURL,true); | |
658 xmlhttp.setRequestHeader('Content-Type', 'text/xml'); | |
659 xmlhttp.onerror = function(){ | |
660 console.log('Error saving file to server! Presenting download locally'); | |
661 createProjectSave(null); | |
662 }; | |
663 xmlhttp.onreadystatechange = function() { | |
664 console.log(xmlhttp.status); | |
665 if (xmlhttp.status != 200 && xmlhttp.readyState == 4) { | |
666 createProjectSave(null); | |
667 } else { | |
668 if (xmlhttp.responseXML == null) | |
669 { | |
670 return createProjectSave(null); | |
671 } | |
672 var response = xmlhttp.responseXML.childNodes[0]; | |
673 if (response.getAttribute('state') == "OK") | |
674 { | |
675 var file = response.getElementsByTagName('file')[0]; | |
676 console.log('Save OK: Filename '+file.textContent+','+file.getAttribute('bytes')+'B'); | |
677 popup.showPopup(); | |
678 popup.popupContent.innerHTML = null; | |
679 popup.popupContent.textContent = "Thank you!"; | |
680 } else { | |
681 var message = response.getElementsByTagName('message')[0]; | |
682 errorSessionDump(message.textContent); | |
683 } | |
684 } | |
685 }; | |
686 xmlhttp.send(file); | |
687 } | |
688 } | |
689 | |
690 function errorSessionDump(msg){ | |
691 // Create the partial interface XML save | |
692 // Include error node with message on why the dump occured | |
693 var xmlDoc = interfaceXMLSave(); | |
694 var err = document.createElement('error'); | |
695 err.textContent = msg; | |
696 xmlDoc.appendChild(err); | |
697 var parent = document.createElement("div"); | |
698 parent.appendChild(xmlDoc); | |
699 var file = [parent.innerHTML]; | |
700 var bb = new Blob(file,{type : 'application/xml'}); | |
701 var dnlk = window.URL.createObjectURL(bb); | |
702 var a = document.createElement("a"); | |
703 a.hidden = ''; | |
704 a.href = dnlk; | |
705 a.download = "save.xml"; | |
706 a.textContent = "Save File"; | |
707 | |
708 popup.showPopup(); | |
709 popup.popupContent.innerHTML = "ERROR : "+msg; | |
710 popup.popupContent.appendChild(a); | |
711 } | |
712 | |
713 // Only other global function which must be defined in the interface class. Determines how to create the XML document. | |
714 function interfaceXMLSave(){ | |
715 // Create the XML string to be exported with results | |
716 var xmlDoc = document.createElement("BrowserEvaluationResult"); | |
717 var projectDocument = specification.projectXML; | |
718 projectDocument.setAttribute('file-name',url); | |
719 xmlDoc.appendChild(projectDocument); | |
720 xmlDoc.appendChild(returnDateNode()); | |
721 xmlDoc.appendChild(interfaceContext.returnNavigator()); | |
722 for (var i=0; i<testState.stateResults.length; i++) | |
723 { | |
724 xmlDoc.appendChild(testState.stateResults[i]); | |
725 } | |
726 | |
727 return xmlDoc; | |
728 } | |
729 | |
730 function AudioEngine() { | |
731 | 762 |
732 // Create two output paths, the main outputGain and fooGain. | 763 // Create two output paths, the main outputGain and fooGain. |
733 // Output gain is default to 1 and any items for playback route here | 764 // Output gain is default to 1 and any items for playback route here |
734 // Foo gain is used for analysis to ensure paths get processed, but are not heard | 765 // Foo gain is used for analysis to ensure paths get processed, but are not heard |
735 // because web audio will optimise and any route which does not go to the destination gets ignored. | 766 // because web audio will optimise and any route which does not go to the destination gets ignored. |
745 this.fooGain.connect(audioContext.destination); | 776 this.fooGain.connect(audioContext.destination); |
746 | 777 |
747 // Create the timer Object | 778 // Create the timer Object |
748 this.timer = new timer(); | 779 this.timer = new timer(); |
749 // Create session metrics | 780 // Create session metrics |
750 this.metric = new sessionMetrics(this); | 781 this.metric = new sessionMetrics(this,specification); |
751 | 782 |
752 this.loopPlayback = false; | 783 this.loopPlayback = false; |
753 | 784 |
754 // Create store for new audioObjects | 785 // Create store for new audioObjects |
755 this.audioObjects = []; | 786 this.audioObjects = []; |
787 | |
788 this.buffers = []; | |
789 this.bufferObj = function() | |
790 { | |
791 this.url = null; | |
792 this.buffer = null; | |
793 this.xmlRequest = new XMLHttpRequest(); | |
794 this.xmlRequest.parent = this; | |
795 this.users = []; | |
796 this.getMedia = function(url) { | |
797 this.url = url; | |
798 this.xmlRequest.open('GET',this.url,true); | |
799 this.xmlRequest.responseType = 'arraybuffer'; | |
800 | |
801 var bufferObj = this; | |
802 | |
803 // Create callback to decode the data asynchronously | |
804 this.xmlRequest.onloadend = function() { | |
805 audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) { | |
806 bufferObj.buffer = decodedData; | |
807 for (var i=0; i<bufferObj.users.length; i++) | |
808 { | |
809 bufferObj.users[i].state = 1; | |
810 if (bufferObj.users[i].interfaceDOM != null) | |
811 { | |
812 bufferObj.users[i].bufferLoaded(bufferObj); | |
813 } | |
814 } | |
815 //calculateLoudness(bufferObj.buffer,"I"); | |
816 }, function(){ | |
817 // Should only be called if there was an error, but sometimes gets called continuously | |
818 // Check here if the error is genuine | |
819 if (bufferObj.buffer == undefined) { | |
820 // Genuine error | |
821 console.log('FATAL - Error loading buffer on '+audioObj.id); | |
822 if (request.status == 404) | |
823 { | |
824 console.log('FATAL - Fragment '+audioObj.id+' 404 error'); | |
825 console.log('URL: '+audioObj.url); | |
826 errorSessionDump('Fragment '+audioObj.id+' 404 error'); | |
827 } | |
828 } | |
829 }); | |
830 }; | |
831 this.progress = 0; | |
832 this.progressCallback = function(event){ | |
833 if (event.lengthComputable) | |
834 { | |
835 this.parent.progress = event.loaded / event.total; | |
836 for (var i=0; i<this.parent.users.length; i++) | |
837 { | |
838 if(this.parent.users[i].interfaceDOM != null) | |
839 { | |
840 if (typeof this.parent.users[i].interfaceDOM.updateLoading === "function") | |
841 { | |
842 this.parent.users[i].interfaceDOM.updateLoading(this.parent.progress*100); | |
843 } | |
844 } | |
845 } | |
846 } | |
847 }; | |
848 this.xmlRequest.addEventListener("progress", this.progressCallback); | |
849 this.xmlRequest.send(); | |
850 }; | |
851 }; | |
756 | 852 |
757 this.play = function(id) { | 853 this.play = function(id) { |
758 // Start the timer and set the audioEngine state to playing (1) | 854 // Start the timer and set the audioEngine state to playing (1) |
759 if (this.status == 0 && this.loopPlayback) { | 855 if (this.status == 0 && this.loopPlayback) { |
760 // Check if all audioObjects are ready | 856 // Check if all audioObjects are ready |
792 { | 888 { |
793 if (i != id) { | 889 if (i != id) { |
794 this.audioObjects[i].outputGain.gain.value = 0.0; | 890 this.audioObjects[i].outputGain.gain.value = 0.0; |
795 this.audioObjects[i].stop(); | 891 this.audioObjects[i].stop(); |
796 } else if (i == id) { | 892 } else if (i == id) { |
797 this.audioObjects[id].outputGain.gain.value = 1.0; | 893 this.audioObjects[id].outputGain.gain.value = this.audioObjects[id].specification.gain*this.audioObjects[id].buffer.buffer.gain; |
798 this.audioObjects[id].play(audioContext.currentTime+0.01); | 894 this.audioObjects[id].play(audioContext.currentTime+0.01); |
799 } | 895 } |
800 } | 896 } |
801 } | 897 } |
802 interfaceContext.playhead.start(); | 898 interfaceContext.playhead.start(); |
821 | 917 |
822 // Create the audioObject with ID of the new track length; | 918 // Create the audioObject with ID of the new track length; |
823 audioObjectId = this.audioObjects.length; | 919 audioObjectId = this.audioObjects.length; |
824 this.audioObjects[audioObjectId] = new audioObject(audioObjectId); | 920 this.audioObjects[audioObjectId] = new audioObject(audioObjectId); |
825 | 921 |
826 // AudioObject will get track itself. | 922 // Check if audioObject buffer is currently stored by full URL |
923 var URL = element.parent.hostURL + element.url; | |
924 var buffer = null; | |
925 for (var i=0; i<this.buffers.length; i++) | |
926 { | |
927 if (URL == this.buffers[i].url) | |
928 { | |
929 buffer = this.buffers[i]; | |
930 break; | |
931 } | |
932 } | |
933 if (buffer == null) | |
934 { | |
935 console.log("[WARN]: Buffer was not loaded in pre-test! "+URL); | |
936 buffer = new this.bufferObj(); | |
937 buffer.getMedia(URL); | |
938 this.buffers.push(buffer); | |
939 } | |
827 this.audioObjects[audioObjectId].specification = element; | 940 this.audioObjects[audioObjectId].specification = element; |
828 this.audioObjects[audioObjectId].constructTrack(element.parent.hostURL + element.url); | 941 this.audioObjects[audioObjectId].url = URL; |
942 buffer.users.push(this.audioObjects[audioObjectId]); | |
943 if (buffer.buffer != null) | |
944 { | |
945 this.audioObjects[audioObjectId].bufferLoaded(buffer); | |
946 } | |
829 return this.audioObjects[audioObjectId]; | 947 return this.audioObjects[audioObjectId]; |
830 }; | 948 }; |
831 | 949 |
832 this.newTestPage = function() { | 950 this.newTestPage = function() { |
833 this.state = 0; | 951 this.state = 0; |
834 this.audioObjectsReady = false; | 952 this.audioObjectsReady = false; |
835 this.metric.reset(); | 953 this.metric.reset(); |
954 for (var i=0; i < this.buffers.length; i++) | |
955 { | |
956 this.buffers[i].users = []; | |
957 } | |
836 this.audioObjects = []; | 958 this.audioObjects = []; |
837 }; | 959 }; |
838 | 960 |
839 this.checkAllPlayed = function() { | 961 this.checkAllPlayed = function() { |
840 arr = []; | 962 arr = []; |
859 }; | 981 }; |
860 | 982 |
861 this.setSynchronousLoop = function() { | 983 this.setSynchronousLoop = function() { |
862 // Pads the signals so they are all exactly the same length | 984 // Pads the signals so they are all exactly the same length |
863 var length = 0; | 985 var length = 0; |
864 var lens = []; | |
865 var maxId; | 986 var maxId; |
866 for (var i=0; i<this.audioObjects.length; i++) | 987 for (var i=0; i<this.audioObjects.length; i++) |
867 { | 988 { |
868 lens.push(this.audioObjects[i].buffer.length); | 989 if (length < this.audioObjects[i].buffer.buffer.length) |
869 if (length < this.audioObjects[i].buffer.length) | 990 { |
870 { | 991 length = this.audioObjects[i].buffer.buffer.length; |
871 length = this.audioObjects[i].buffer.length; | |
872 maxId = i; | 992 maxId = i; |
873 } | 993 } |
874 } | 994 } |
875 // Perform difference | |
876 for (var i=0; i<lens.length; i++) | |
877 { | |
878 lens[i] = length - lens[i]; | |
879 } | |
880 // Extract the audio and zero-pad | 995 // Extract the audio and zero-pad |
881 for (var i=0; i<lens.length; i++) | 996 for (var i=0; i<this.audioObjects.length; i++) |
882 { | 997 { |
883 var orig = this.audioObjects[i].buffer; | 998 var orig = this.audioObjects[i].buffer.buffer; |
884 var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate); | 999 var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate); |
885 for (var c=0; c<orig.numberOfChannels; c++) | 1000 for (var c=0; c<orig.numberOfChannels; c++) |
886 { | 1001 { |
887 var inData = hold.getChannelData(c); | 1002 var inData = hold.getChannelData(c); |
888 var outData = orig.getChannelData(c); | 1003 var outData = orig.getChannelData(c); |
889 for (var n=0; n<orig.length; n++) | 1004 for (var n=0; n<orig.length; n++) |
890 {inData[n] = outData[n];} | 1005 {inData[n] = outData[n];} |
891 } | 1006 } |
892 this.audioObjects[i].buffer = hold; | 1007 hold.gain = orig.gain; |
893 delete orig; | 1008 hold.lufs = orig.lufs; |
1009 this.audioObjects[i].buffer.buffer = hold; | |
894 } | 1010 } |
895 }; | 1011 }; |
896 | 1012 |
897 } | 1013 } |
898 | 1014 |
920 this.outputGain.connect(audioEngineContext.outputGain); | 1036 this.outputGain.connect(audioEngineContext.outputGain); |
921 | 1037 |
922 // the audiobuffer is not designed for multi-start playback | 1038 // the audiobuffer is not designed for multi-start playback |
923 // When stopeed, the buffer node is deleted and recreated with the stored buffer. | 1039 // When stopeed, the buffer node is deleted and recreated with the stored buffer. |
924 this.buffer; | 1040 this.buffer; |
1041 | |
1042 this.bufferLoaded = function(callee) | |
1043 { | |
1044 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the | |
1045 // audioObject and trigger the interfaceDOM.enable() function for user feedback | |
1046 if (audioEngineContext.loopPlayback){ | |
1047 // First copy the buffer into this.buffer | |
1048 this.buffer = new audioEngineContext.bufferObj(); | |
1049 this.buffer.url = callee.url; | |
1050 this.buffer.buffer = audioContext.createBuffer(callee.buffer.numberOfChannels, callee.buffer.length, callee.buffer.sampleRate); | |
1051 for (var c=0; c<callee.buffer.numberOfChannels; c++) | |
1052 { | |
1053 var src = callee.buffer.getChannelData(c); | |
1054 var dst = this.buffer.buffer.getChannelData(c); | |
1055 for (var n=0; n<src.length; n++) | |
1056 { | |
1057 dst[n] = src[n]; | |
1058 } | |
1059 } | |
1060 } else { | |
1061 this.buffer = callee; | |
1062 } | |
1063 this.state = 1; | |
1064 this.buffer.buffer.gain = callee.buffer.gain; | |
1065 this.buffer.buffer.lufs = callee.buffer.lufs; | |
1066 /* | |
1067 var targetLUFS = this.specification.parent.loudness; | |
1068 if (typeof targetLUFS === "number") | |
1069 { | |
1070 this.buffer.buffer.gain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs); | |
1071 } else { | |
1072 this.buffer.buffer.gain = 1.0; | |
1073 } | |
1074 */ | |
1075 if (this.interfaceDOM != null) { | |
1076 this.interfaceDOM.enable(); | |
1077 } | |
1078 }; | |
925 | 1079 |
926 this.loopStart = function() { | 1080 this.loopStart = function() { |
927 this.outputGain.gain.value = 1.0; | 1081 this.outputGain.gain.value = this.specification.gain*this.buffer.buffer.gain; |
928 this.metric.startListening(audioEngineContext.timer.getTestTime()); | 1082 this.metric.startListening(audioEngineContext.timer.getTestTime()); |
929 }; | 1083 }; |
930 | 1084 |
931 this.loopStop = function() { | 1085 this.loopStop = function() { |
932 if (this.outputGain.gain.value != 0.0) { | 1086 if (this.outputGain.gain.value != 0.0) { |
934 this.metric.stopListening(audioEngineContext.timer.getTestTime()); | 1088 this.metric.stopListening(audioEngineContext.timer.getTestTime()); |
935 } | 1089 } |
936 }; | 1090 }; |
937 | 1091 |
938 this.play = function(startTime) { | 1092 this.play = function(startTime) { |
939 if (this.bufferNode == undefined) { | 1093 if (this.bufferNode == undefined && this.buffer.buffer != undefined) { |
940 this.bufferNode = audioContext.createBufferSource(); | 1094 this.bufferNode = audioContext.createBufferSource(); |
941 this.bufferNode.owner = this; | 1095 this.bufferNode.owner = this; |
942 this.bufferNode.connect(this.outputGain); | 1096 this.bufferNode.connect(this.outputGain); |
943 this.bufferNode.buffer = this.buffer; | 1097 this.bufferNode.buffer = this.buffer.buffer; |
944 this.bufferNode.loop = audioEngineContext.loopPlayback; | 1098 this.bufferNode.loop = audioEngineContext.loopPlayback; |
945 this.bufferNode.onended = function(event) { | 1099 this.bufferNode.onended = function(event) { |
946 // Safari does not like using 'this' to reference the calling object! | 1100 // Safari does not like using 'this' to reference the calling object! |
947 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition()); | 1101 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition()); |
948 event.currentTarget.owner.stop(); | 1102 event.currentTarget.owner.stop(); |
966 this.getCurrentPosition = function() { | 1120 this.getCurrentPosition = function() { |
967 var time = audioEngineContext.timer.getTestTime(); | 1121 var time = audioEngineContext.timer.getTestTime(); |
968 if (this.bufferNode != undefined) { | 1122 if (this.bufferNode != undefined) { |
969 if (this.bufferNode.loop == true) { | 1123 if (this.bufferNode.loop == true) { |
970 if (audioEngineContext.status == 1) { | 1124 if (audioEngineContext.status == 1) { |
971 return (time-this.metric.listenStart)%this.buffer.duration; | 1125 return (time-this.metric.listenStart)%this.buffer.buffer.duration; |
972 } else { | 1126 } else { |
973 return 0; | 1127 return 0; |
974 } | 1128 } |
975 } else { | 1129 } else { |
976 if (this.metric.listenHold) { | 1130 if (this.metric.listenHold) { |
981 } | 1135 } |
982 } else { | 1136 } else { |
983 return 0; | 1137 return 0; |
984 } | 1138 } |
985 }; | 1139 }; |
986 | |
987 this.constructTrack = function(url) { | |
988 var request = new XMLHttpRequest(); | |
989 this.url = url; | |
990 request.open('GET',url,true); | |
991 request.responseType = 'arraybuffer'; | |
992 | |
993 var audioObj = this; | |
994 | |
995 // Create callback to decode the data asynchronously | |
996 request.onloadend = function() { | |
997 audioContext.decodeAudioData(request.response, function(decodedData) { | |
998 audioObj.buffer = decodedData; | |
999 audioObj.state = 1; | |
1000 if (audioObj.specification.type != 'outsidereference') | |
1001 {audioObj.interfaceDOM.enable();} | |
1002 }, function(){ | |
1003 // Should only be called if there was an error, but sometimes gets called continuously | |
1004 // Check here if the error is genuine | |
1005 if (audioObj.state == 0 || audioObj.buffer == undefined) { | |
1006 // Genuine error | |
1007 console.log('FATAL - Error loading buffer on '+audioObj.id); | |
1008 if (request.status == 404) | |
1009 { | |
1010 console.log('FATAL - Fragment '+audioObj.id+' 404 error'); | |
1011 console.log('URL: '+audioObj.url); | |
1012 errorSessionDump('Fragment '+audioObj.id+' 404 error'); | |
1013 } | |
1014 } | |
1015 }); | |
1016 }; | |
1017 request.send(); | |
1018 }; | |
1019 | 1140 |
1020 this.exportXMLDOM = function() { | 1141 this.exportXMLDOM = function() { |
1021 var root = document.createElement('audioElement'); | 1142 var root = document.createElement('audioElement'); |
1022 root.id = this.specification.id; | 1143 root.id = this.specification.id; |
1023 root.setAttribute('url',this.url); | 1144 root.setAttribute('url',this.specification.url); |
1024 var file = document.createElement('file'); | 1145 var file = document.createElement('file'); |
1025 file.setAttribute('sampleRate',this.buffer.sampleRate); | 1146 file.setAttribute('sampleRate',this.buffer.buffer.sampleRate); |
1026 file.setAttribute('channels',this.buffer.numberOfChannels); | 1147 file.setAttribute('channels',this.buffer.buffer.numberOfChannels); |
1027 file.setAttribute('sampleCount',this.buffer.length); | 1148 file.setAttribute('sampleCount',this.buffer.buffer.length); |
1028 file.setAttribute('duration',this.buffer.duration); | 1149 file.setAttribute('duration',this.buffer.buffer.duration); |
1029 root.appendChild(file); | 1150 root.appendChild(file); |
1030 if (this.specification.type != 'outsidereference') { | 1151 if (this.specification.type != 'outsidereference') { |
1031 root.appendChild(this.interfaceDOM.exportXMLDOM(this)); | 1152 var interfaceXML = this.interfaceDOM.exportXMLDOM(this); |
1153 if (interfaceXML.length == undefined) { | |
1154 root.appendChild(interfaceXML); | |
1155 } else { | |
1156 for (var i=0; i<interfaceXML.length; i++) | |
1157 { | |
1158 root.appendChild(interfaceXML[i]); | |
1159 } | |
1160 } | |
1032 root.appendChild(this.commentDOM.exportXMLDOM(this)); | 1161 root.appendChild(this.commentDOM.exportXMLDOM(this)); |
1033 if(this.specification.type == 'anchor') { | 1162 if(this.specification.type == 'anchor') { |
1034 root.setAttribute('anchor',true); | 1163 root.setAttribute('anchor',true); |
1035 } else if(this.specification.type == 'reference') { | 1164 } else if(this.specification.type == 'reference') { |
1036 root.setAttribute('reference',true); | 1165 root.setAttribute('reference',true); |
1082 this.updateTestTime(); | 1211 this.updateTestTime(); |
1083 return this.testDuration; | 1212 return this.testDuration; |
1084 }; | 1213 }; |
1085 } | 1214 } |
1086 | 1215 |
1087 function sessionMetrics(engine) | 1216 function sessionMetrics(engine,specification) |
1088 { | 1217 { |
1089 /* Used by audioEngine to link to audioObjects to minimise the timer call timers; | 1218 /* Used by audioEngine to link to audioObjects to minimise the timer call timers; |
1090 */ | 1219 */ |
1091 this.engine = engine; | 1220 this.engine = engine; |
1092 this.lastClicked = -1; | 1221 this.lastClicked = -1; |
1093 this.data = -1; | 1222 this.data = -1; |
1094 this.reset = function() { | 1223 this.reset = function() { |
1095 this.lastClicked = -1; | 1224 this.lastClicked = -1; |
1096 this.data = -1; | 1225 this.data = -1; |
1097 }; | 1226 }; |
1227 | |
1228 this.enableElementInitialPosition = false; | |
1229 this.enableElementListenTracker = false; | |
1230 this.enableElementTimer = false; | |
1231 this.enableElementTracker = false; | |
1232 this.enableFlagListenedTo = false; | |
1233 this.enableFlagMoved = false; | |
1234 this.enableTestTimer = false; | |
1235 // Obtain the metrics enabled | |
1236 for (var i=0; i<specification.metrics.length; i++) | |
1237 { | |
1238 var node = specification.metrics[i]; | |
1239 switch(node.enabled) | |
1240 { | |
1241 case 'testTimer': | |
1242 this.enableTestTimer = true; | |
1243 break; | |
1244 case 'elementTimer': | |
1245 this.enableElementTimer = true; | |
1246 break; | |
1247 case 'elementTracker': | |
1248 this.enableElementTracker = true; | |
1249 break; | |
1250 case 'elementListenTracker': | |
1251 this.enableElementListenTracker = true; | |
1252 break; | |
1253 case 'elementInitialPosition': | |
1254 this.enableElementInitialPosition = true; | |
1255 break; | |
1256 case 'elementFlagListenedTo': | |
1257 this.enableFlagListenedTo = true; | |
1258 break; | |
1259 case 'elementFlagMoved': | |
1260 this.enableFlagMoved = true; | |
1261 break; | |
1262 case 'elementFlagComments': | |
1263 this.enableFlagComments = true; | |
1264 break; | |
1265 } | |
1266 } | |
1098 this.initialiseTest = function(){}; | 1267 this.initialiseTest = function(){}; |
1099 } | 1268 } |
1100 | 1269 |
1101 function metricTracker(caller) | 1270 function metricTracker(caller) |
1102 { | 1271 { |
1302 time.setAttributeNode(minute); | 1471 time.setAttributeNode(minute); |
1303 time.setAttributeNode(secs); | 1472 time.setAttributeNode(secs); |
1304 | 1473 |
1305 hold.appendChild(date); | 1474 hold.appendChild(date); |
1306 hold.appendChild(time); | 1475 hold.appendChild(time); |
1307 return hold | 1476 return hold; |
1308 | 1477 |
1309 } | 1478 } |
1310 | 1479 |
1311 function Specification() { | 1480 function Specification() { |
1312 // Handles the decoding of the project specification XML into a simple JavaScript Object. | 1481 // Handles the decoding of the project specification XML into a simple JavaScript Object. |
1313 | 1482 |
1314 this.interfaceType = null; | 1483 this.interfaceType = null; |
1315 this.commonInterface = null; | 1484 this.commonInterface = new function() |
1485 { | |
1486 this.options = []; | |
1487 this.optionNode = function(input) | |
1488 { | |
1489 var name = input.getAttribute('name'); | |
1490 this.type = name; | |
1491 if(this.type == "option") | |
1492 { | |
1493 this.name = input.id; | |
1494 } else if (this.type == "check") | |
1495 { | |
1496 this.check = input.id; | |
1497 } | |
1498 }; | |
1499 }; | |
1500 | |
1501 this.randomiseOrder = function(input) | |
1502 { | |
1503 // This takes an array of information and randomises the order | |
1504 var N = input.length; | |
1505 | |
1506 var inputSequence = []; // For safety purposes: keep track of randomisation | |
1507 for (var counter = 0; counter < N; ++counter) | |
1508 inputSequence.push(counter) // Fill array | |
1509 var inputSequenceClone = inputSequence.slice(0); | |
1510 | |
1511 var holdArr = []; | |
1512 var outputSequence = []; | |
1513 for (var n=0; n<N; n++) | |
1514 { | |
1515 // First pick a random number | |
1516 var r = Math.random(); | |
1517 // Multiply and floor by the number of elements left | |
1518 r = Math.floor(r*input.length); | |
1519 // Pick out that element and delete from the array | |
1520 holdArr.push(input.splice(r,1)[0]); | |
1521 // Do the same with sequence | |
1522 outputSequence.push(inputSequence.splice(r,1)[0]); | |
1523 } | |
1524 console.log(inputSequenceClone.toString()); // print original array to console | |
1525 console.log(outputSequence.toString()); // print randomised array to console | |
1526 return holdArr; | |
1527 }; | |
1316 this.projectReturn = null; | 1528 this.projectReturn = null; |
1317 this.randomiseOrder = null; | 1529 this.randomiseOrder = null; |
1318 this.collectMetrics = null; | 1530 this.collectMetrics = null; |
1319 this.testPages = null; | 1531 this.testPages = null; |
1320 this.preTest = null; | |
1321 this.postTest = null; | |
1322 this.metrics =[]; | |
1323 | |
1324 this.audioHolders = []; | 1532 this.audioHolders = []; |
1325 | 1533 this.metrics = []; |
1326 this.decode = function() { | 1534 this.loudness = null; |
1535 | |
1536 this.decode = function(projectXML) { | |
1327 // projectXML - DOM Parsed document | 1537 // projectXML - DOM Parsed document |
1328 this.projectXML = projectXML.childNodes[0]; | 1538 this.projectXML = projectXML.childNodes[0]; |
1329 var setupNode = projectXML.getElementsByTagName('setup')[0]; | 1539 var setupNode = projectXML.getElementsByTagName('setup')[0]; |
1330 this.interfaceType = setupNode.getAttribute('interface'); | 1540 this.interfaceType = setupNode.getAttribute('interface'); |
1331 this.projectReturn = setupNode.getAttribute('projectReturn'); | 1541 this.projectReturn = setupNode.getAttribute('projectReturn'); |
1341 this.testPages = null; | 1551 this.testPages = null; |
1342 } else { | 1552 } else { |
1343 this.testPages = Number(this.testPages); | 1553 this.testPages = Number(this.testPages); |
1344 if (this.testPages == 0) {this.testPages = null;} | 1554 if (this.testPages == 0) {this.testPages = null;} |
1345 } | 1555 } |
1556 if (setupNode.getAttribute('loudness') != null) | |
1557 { | |
1558 var XMLloudness = setupNode.getAttribute('loudness'); | |
1559 if (isNaN(Number(XMLloudness)) == false) | |
1560 { | |
1561 this.loudness = Number(XMLloudness); | |
1562 } | |
1563 } | |
1346 var metricCollection = setupNode.getElementsByTagName('Metric'); | 1564 var metricCollection = setupNode.getElementsByTagName('Metric'); |
1347 | 1565 |
1348 this.preTest = new this.prepostNode('pretest',setupNode.getElementsByTagName('PreTest')); | 1566 var setupPreTestNode = setupNode.getElementsByTagName('PreTest'); |
1349 this.postTest = new this.prepostNode('posttest',setupNode.getElementsByTagName('PostTest')); | 1567 if (setupPreTestNode.length != 0) |
1568 { | |
1569 setupPreTestNode = setupPreTestNode[0]; | |
1570 this.preTest.construct(setupPreTestNode); | |
1571 } | |
1572 | |
1573 var setupPostTestNode = setupNode.getElementsByTagName('PostTest'); | |
1574 if (setupPostTestNode.length != 0) | |
1575 { | |
1576 setupPostTestNode = setupPostTestNode[0]; | |
1577 this.postTest.construct(setupPostTestNode); | |
1578 } | |
1350 | 1579 |
1351 if (metricCollection.length > 0) { | 1580 if (metricCollection.length > 0) { |
1352 metricCollection = metricCollection[0].getElementsByTagName('metricEnable'); | 1581 metricCollection = metricCollection[0].getElementsByTagName('metricEnable'); |
1353 for (var i=0; i<metricCollection.length; i++) { | 1582 for (var i=0; i<metricCollection.length; i++) { |
1354 this.metrics.push(new this.metricNode(metricCollection[i].textContent)); | 1583 this.metrics.push(new this.metricNode(metricCollection[i].textContent)); |
1386 } else { | 1615 } else { |
1387 this.max = Number(this.max); | 1616 this.max = Number(this.max); |
1388 } | 1617 } |
1389 } | 1618 } |
1390 } else if (this.type == 'anchor' || this.type == 'reference') { | 1619 } else if (this.type == 'anchor' || this.type == 'reference') { |
1391 Console.log("WARNING: Anchor and Reference tags in the <interface> node are depricated"); | 1620 this.value = Number(child.textContent); |
1621 this.enforce = child.getAttribute('enforce'); | |
1622 if (this.enforce == 'true') {this.enforce = true;} | |
1623 else {this.enforce = false;} | |
1392 } | 1624 } |
1393 }; | 1625 }; |
1394 this.options = []; | 1626 this.options = []; |
1395 if (commonInterfaceNode != undefined) { | 1627 if (commonInterfaceNode != undefined) { |
1396 var child = commonInterfaceNode.firstElementChild; | 1628 var child = commonInterfaceNode.firstElementChild; |
1401 } | 1633 } |
1402 }; | 1634 }; |
1403 | 1635 |
1404 var audioHolders = projectXML.getElementsByTagName('audioHolder'); | 1636 var audioHolders = projectXML.getElementsByTagName('audioHolder'); |
1405 for (var i=0; i<audioHolders.length; i++) { | 1637 for (var i=0; i<audioHolders.length; i++) { |
1406 this.audioHolders.push(new this.audioHolderNode(this,audioHolders[i])); | 1638 var node = new this.audioHolderNode(this); |
1639 node.decode(this,audioHolders[i]); | |
1640 this.audioHolders.push(node); | |
1407 } | 1641 } |
1408 | 1642 |
1409 // New check if we need to randomise the test order | 1643 // New check if we need to randomise the test order |
1410 if (this.randomiseOrder) | 1644 if (this.randomiseOrder && typeof randomiseOrder === "function") |
1411 { | 1645 { |
1412 this.audioHolders = randomiseOrder(this.audioHolders); | 1646 this.audioHolders = randomiseOrder(this.audioHolders); |
1413 for (var i=0; i<this.audioHolders.length; i++) | 1647 for (var i=0; i<this.audioHolders.length; i++) |
1414 { | 1648 { |
1415 this.audioHolders[i].presentedId = i; | 1649 this.audioHolders[i].presentedId = i; |
1430 this.audioHolders.push(aH[i]); | 1664 this.audioHolders.push(aH[i]); |
1431 } | 1665 } |
1432 } | 1666 } |
1433 }; | 1667 }; |
1434 | 1668 |
1435 this.prepostNode = function(type,Collection) { | 1669 this.encode = function() |
1670 { | |
1671 var root = document.implementation.createDocument(null,"BrowserEvalProjectDocument"); | |
1672 // First get all the <setup> tag compiled | |
1673 var setupNode = root.createElement("setup"); | |
1674 setupNode.setAttribute('interface',this.interfaceType); | |
1675 setupNode.setAttribute('projectReturn',this.projectReturn); | |
1676 setupNode.setAttribute('randomiseOrder',this.randomiseOrder); | |
1677 setupNode.setAttribute('collectMetrics',this.collectMetrics); | |
1678 setupNode.setAttribute('testPages',this.testPages); | |
1679 if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);} | |
1680 | |
1681 var setupPreTest = root.createElement("PreTest"); | |
1682 for (var i=0; i<this.preTest.options.length; i++) | |
1683 { | |
1684 setupPreTest.appendChild(this.preTest.options[i].exportXML(root)); | |
1685 } | |
1686 | |
1687 var setupPostTest = root.createElement("PostTest"); | |
1688 for (var i=0; i<this.postTest.options.length; i++) | |
1689 { | |
1690 setupPostTest.appendChild(this.postTest.options[i].exportXML(root)); | |
1691 } | |
1692 | |
1693 setupNode.appendChild(setupPreTest); | |
1694 setupNode.appendChild(setupPostTest); | |
1695 | |
1696 // <Metric> tag | |
1697 var Metric = root.createElement("Metric"); | |
1698 for (var i=0; i<this.metrics.length; i++) | |
1699 { | |
1700 var metricEnable = root.createElement("metricEnable"); | |
1701 metricEnable.textContent = this.metrics[i].enabled; | |
1702 Metric.appendChild(metricEnable); | |
1703 } | |
1704 setupNode.appendChild(Metric); | |
1705 | |
1706 // <interface> tag | |
1707 var CommonInterface = root.createElement("interface"); | |
1708 for (var i=0; i<this.commonInterface.options.length; i++) | |
1709 { | |
1710 var CIObj = this.commonInterface.options[i]; | |
1711 var CINode = root.createElement(CIObj.type); | |
1712 if (CIObj.type == "check") {CINode.setAttribute("name",CIObj.check);} | |
1713 else {CINode.setAttribute("name",CIObj.name);} | |
1714 CommonInterface.appendChild(CINode); | |
1715 } | |
1716 setupNode.appendChild(CommonInterface); | |
1717 | |
1718 root.getElementsByTagName("BrowserEvalProjectDocument")[0].appendChild(setupNode); | |
1719 // Time for the <audioHolder> tags | |
1720 for (var ahIndex = 0; ahIndex < this.audioHolders.length; ahIndex++) | |
1721 { | |
1722 var node = this.audioHolders[ahIndex].encode(root); | |
1723 root.getElementsByTagName("BrowserEvalProjectDocument")[0].appendChild(node); | |
1724 } | |
1725 return root; | |
1726 }; | |
1727 | |
1728 this.prepostNode = function(type) { | |
1436 this.type = type; | 1729 this.type = type; |
1437 this.options = []; | 1730 this.options = []; |
1438 | 1731 |
1439 this.OptionNode = function(child) { | 1732 this.OptionNode = function() { |
1440 | 1733 |
1441 this.childOption = function(element) { | 1734 this.childOption = function() { |
1442 this.type = 'option'; | 1735 this.type = 'option'; |
1443 this.id = element.id; | 1736 this.id = null; |
1444 this.name = element.getAttribute('name'); | 1737 this.name = undefined; |
1445 this.text = element.textContent; | 1738 this.text = null; |
1446 }; | 1739 }; |
1447 | 1740 |
1448 this.type = child.nodeName; | 1741 this.type = undefined; |
1449 if (child.nodeName == "question") { | 1742 this.id = undefined; |
1450 this.id = child.id; | 1743 this.mandatory = undefined; |
1451 this.mandatory; | 1744 this.question = undefined; |
1452 if (child.getAttribute('mandatory') == "true") {this.mandatory = true;} | 1745 this.statement = undefined; |
1453 else {this.mandatory = false;} | 1746 this.boxsize = undefined; |
1454 this.question = child.textContent; | 1747 this.options = []; |
1455 if (child.getAttribute('boxsize') == null) { | 1748 this.min = undefined; |
1456 this.boxsize = 'normal'; | 1749 this.max = undefined; |
1457 } else { | 1750 this.step = undefined; |
1458 this.boxsize = child.getAttribute('boxsize'); | 1751 |
1459 } | 1752 this.decode = function(child) |
1460 } else if (child.nodeName == "statement") { | 1753 { |
1461 this.statement = child.textContent; | 1754 this.type = child.nodeName; |
1462 } else if (child.nodeName == "checkbox" || child.nodeName == "radio") { | 1755 if (child.nodeName == "question") { |
1463 var element = child.firstElementChild; | 1756 this.id = child.id; |
1464 this.id = child.id; | 1757 this.mandatory; |
1465 if (element == null) { | 1758 if (child.getAttribute('mandatory') == "true") {this.mandatory = true;} |
1466 console.log('Malformed' +child.nodeName+ 'entry'); | 1759 else {this.mandatory = false;} |
1467 this.statement = 'Malformed' +child.nodeName+ 'entry'; | 1760 this.question = child.textContent; |
1468 this.type = 'statement'; | 1761 if (child.getAttribute('boxsize') == null) { |
1469 } else { | 1762 this.boxsize = 'normal'; |
1470 this.options = []; | 1763 } else { |
1471 while (element != null) { | 1764 this.boxsize = child.getAttribute('boxsize'); |
1472 if (element.nodeName == 'statement' && this.statement == undefined){ | 1765 } |
1473 this.statement = element.textContent; | 1766 } else if (child.nodeName == "statement") { |
1474 } else if (element.nodeName == 'option') { | 1767 this.statement = child.textContent; |
1475 this.options.push(new this.childOption(element)); | 1768 } else if (child.nodeName == "checkbox" || child.nodeName == "radio") { |
1769 var element = child.firstElementChild; | |
1770 this.id = child.id; | |
1771 if (element == null) { | |
1772 console.log('Malformed' +child.nodeName+ 'entry'); | |
1773 this.statement = 'Malformed' +child.nodeName+ 'entry'; | |
1774 this.type = 'statement'; | |
1775 } else { | |
1776 this.options = []; | |
1777 while (element != null) { | |
1778 if (element.nodeName == 'statement' && this.statement == undefined){ | |
1779 this.statement = element.textContent; | |
1780 } else if (element.nodeName == 'option') { | |
1781 var node = new this.childOption(); | |
1782 node.id = element.id; | |
1783 node.name = element.getAttribute('name'); | |
1784 node.text = element.textContent; | |
1785 this.options.push(node); | |
1786 } | |
1787 element = element.nextElementSibling; | |
1476 } | 1788 } |
1477 element = element.nextElementSibling; | |
1478 } | 1789 } |
1479 } | 1790 } else if (child.nodeName == "number") { |
1480 } else if (child.nodeName == "number") { | 1791 this.statement = child.textContent; |
1481 this.statement = child.textContent; | 1792 this.id = child.id; |
1482 this.id = child.id; | 1793 this.min = child.getAttribute('min'); |
1483 this.min = child.getAttribute('min'); | 1794 this.max = child.getAttribute('max'); |
1484 this.max = child.getAttribute('max'); | 1795 this.step = child.getAttribute('step'); |
1485 this.step = child.getAttribute('step'); | 1796 } |
1486 } | 1797 }; |
1798 | |
1799 this.exportXML = function(root) | |
1800 { | |
1801 var node = root.createElement(this.type); | |
1802 switch(this.type) | |
1803 { | |
1804 case "statement": | |
1805 node.textContent = this.statement; | |
1806 break; | |
1807 case "question": | |
1808 node.id = this.id; | |
1809 node.setAttribute("mandatory",this.mandatory); | |
1810 node.setAttribute("boxsize",this.boxsize); | |
1811 node.textContent = this.question; | |
1812 break; | |
1813 case "number": | |
1814 node.id = this.id; | |
1815 node.setAttribute("mandatory",this.mandatory); | |
1816 node.setAttribute("min", this.min); | |
1817 node.setAttribute("max", this.max); | |
1818 node.setAttribute("step", this.step); | |
1819 node.textContent = this.statement; | |
1820 break; | |
1821 case "checkbox": | |
1822 node.id = this.id; | |
1823 var statement = root.createElement("statement"); | |
1824 statement.textContent = this.statement; | |
1825 node.appendChild(statement); | |
1826 for (var i=0; i<this.options.length; i++) | |
1827 { | |
1828 var option = this.options[i]; | |
1829 var optionNode = root.createElement("option"); | |
1830 optionNode.id = option.id; | |
1831 optionNode.textContent = option.text; | |
1832 node.appendChild(optionNode); | |
1833 } | |
1834 break; | |
1835 case "radio": | |
1836 node.id = this.id; | |
1837 var statement = root.createElement("statement"); | |
1838 statement.textContent = this.statement; | |
1839 node.appendChild(statement); | |
1840 for (var i=0; i<this.options.length; i++) | |
1841 { | |
1842 var option = this.options[i]; | |
1843 var optionNode = root.createElement("option"); | |
1844 optionNode.setAttribute("name",option.name); | |
1845 optionNode.textContent = option.text; | |
1846 node.appendChild(optionNode); | |
1847 } | |
1848 break; | |
1849 } | |
1850 return node; | |
1851 }; | |
1487 }; | 1852 }; |
1488 | 1853 this.construct = function(Collection) |
1489 // On construction: | 1854 { |
1490 if (Collection.length != 0) { | |
1491 Collection = Collection[0]; | |
1492 if (Collection.childElementCount != 0) { | 1855 if (Collection.childElementCount != 0) { |
1493 var child = Collection.firstElementChild; | 1856 var child = Collection.firstElementChild; |
1494 this.options.push(new this.OptionNode(child)); | 1857 var node = new this.OptionNode(); |
1858 node.decode(child); | |
1859 this.options.push(node); | |
1495 while (child.nextElementSibling != null) { | 1860 while (child.nextElementSibling != null) { |
1496 child = child.nextElementSibling; | 1861 child = child.nextElementSibling; |
1497 this.options.push(new this.OptionNode(child)); | 1862 node = new this.OptionNode(); |
1498 } | 1863 node.decode(child); |
1499 } | 1864 this.options.push(node); |
1500 } | 1865 } |
1501 }; | 1866 } |
1867 }; | |
1868 }; | |
1869 this.preTest = new this.prepostNode("pretest"); | |
1870 this.postTest = new this.prepostNode("posttest"); | |
1502 | 1871 |
1503 this.metricNode = function(name) { | 1872 this.metricNode = function(name) { |
1504 this.enabled = name; | 1873 this.enabled = name; |
1505 }; | 1874 }; |
1506 | 1875 |
1507 this.audioHolderNode = function(parent,xml) { | 1876 this.audioHolderNode = function(parent) { |
1508 this.type = 'audioHolder'; | 1877 this.type = 'audioHolder'; |
1509 this.presentedId = parent.audioHolders.length; | 1878 this.presentedId = undefined; |
1510 this.interfaceNode = function(DOM) { | 1879 this.id = undefined; |
1511 var title = DOM.getElementsByTagName('title'); | 1880 this.hostURL = undefined; |
1512 if (title.length == 0) {this.title = null;} | 1881 this.sampleRate = undefined; |
1513 else {this.title = title[0].textContent;} | 1882 this.randomiseOrder = undefined; |
1514 this.options = parent.commonInterface.options; | 1883 this.loop = undefined; |
1515 var scale = DOM.getElementsByTagName('scale'); | 1884 this.elementComments = undefined; |
1885 this.outsideReference = null; | |
1886 this.loudness = null; | |
1887 this.initialPosition = null; | |
1888 this.preTest = new parent.prepostNode("pretest"); | |
1889 this.postTest = new parent.prepostNode("pretest"); | |
1890 this.interfaces = []; | |
1891 this.commentBoxPrefix = "Comment on track"; | |
1892 this.audioElements = []; | |
1893 this.commentQuestions = []; | |
1894 | |
1895 this.decode = function(parent,xml) | |
1896 { | |
1897 this.presentedId = parent.audioHolders.length; | |
1898 this.id = xml.id; | |
1899 this.hostURL = xml.getAttribute('hostURL'); | |
1900 this.sampleRate = xml.getAttribute('sampleRate'); | |
1901 if (xml.getAttribute('randomiseOrder') == "true") {this.randomiseOrder = true;} | |
1902 else {this.randomiseOrder = false;} | |
1903 this.repeatCount = xml.getAttribute('repeatCount'); | |
1904 if (xml.getAttribute('loop') == 'true') {this.loop = true;} | |
1905 else {this.loop == false;} | |
1906 if (xml.getAttribute('elementComments') == "true") {this.elementComments = true;} | |
1907 else {this.elementComments = false;} | |
1908 if (typeof parent.loudness === "number") | |
1909 { | |
1910 this.loudness = parent.loudness; | |
1911 } | |
1912 if (typeof xml.getAttribute('initial-position') === "string") | |
1913 { | |
1914 var xmlInitialPosition = Number(xml.getAttribute('initial-position')); | |
1915 if (isNaN(xmlInitialPosition) == false) | |
1916 { | |
1917 if (xmlInitialPosition > 1) | |
1918 { | |
1919 xmlInitialPosition /= 100; | |
1920 } | |
1921 this.initialPosition = xmlInitialPosition; | |
1922 } | |
1923 } | |
1924 if (xml.getAttribute('loudness') != null) | |
1925 { | |
1926 var XMLloudness = xml.getAttribute('loudness'); | |
1927 if (isNaN(Number(XMLloudness)) == false) | |
1928 { | |
1929 this.loudness = Number(XMLloudness); | |
1930 } | |
1931 } | |
1932 var setupPreTestNode = xml.getElementsByTagName('PreTest'); | |
1933 if (setupPreTestNode.length != 0) | |
1934 { | |
1935 setupPreTestNode = setupPreTestNode[0]; | |
1936 this.preTest.construct(setupPreTestNode); | |
1937 } | |
1938 | |
1939 var setupPostTestNode = xml.getElementsByTagName('PostTest'); | |
1940 if (setupPostTestNode.length != 0) | |
1941 { | |
1942 setupPostTestNode = setupPostTestNode[0]; | |
1943 this.postTest.construct(setupPostTestNode); | |
1944 } | |
1945 | |
1946 var interfaceDOM = xml.getElementsByTagName('interface'); | |
1947 for (var i=0; i<interfaceDOM.length; i++) { | |
1948 var node = new this.interfaceNode(); | |
1949 node.decode(interfaceDOM[i]); | |
1950 this.interfaces.push(node); | |
1951 } | |
1952 this.commentBoxPrefix = xml.getElementsByTagName('commentBoxPrefix'); | |
1953 if (this.commentBoxPrefix.length != 0) { | |
1954 this.commentBoxPrefix = this.commentBoxPrefix[0].textContent; | |
1955 } else { | |
1956 this.commentBoxPrefix = "Comment on track"; | |
1957 } | |
1958 var audioElementsDOM = xml.getElementsByTagName('audioElements'); | |
1959 var outsideReferenceHolder = null; | |
1960 for (var i=0; i<audioElementsDOM.length; i++) { | |
1961 var node = new this.audioElementNode(); | |
1962 node.decode(this,audioElementsDOM[i]); | |
1963 if (audioElementsDOM[i].getAttribute('type') == 'outsidereference') { | |
1964 if (this.outsideReference == null) { | |
1965 outsideReferenceHolder = node; | |
1966 this.outsideReference = i; | |
1967 } else { | |
1968 console.log('Error only one audioelement can be of type outsidereference per audioholder'); | |
1969 this.audioElements.push(node); | |
1970 console.log('Element id '+audioElementsDOM[i].id+' made into normal node'); | |
1971 } | |
1972 } else { | |
1973 this.audioElements.push(node); | |
1974 } | |
1975 } | |
1976 | |
1977 if (this.randomiseOrder == true && typeof randomiseOrder === "function") | |
1978 { | |
1979 this.audioElements = randomiseOrder(this.audioElements); | |
1980 } | |
1981 if (outsideReferenceHolder != null) | |
1982 { | |
1983 this.audioElements.push(outsideReferenceHolder); | |
1984 this.outsideReference = this.audioElements.length-1; | |
1985 } | |
1986 | |
1987 | |
1988 var commentQuestionsDOM = xml.getElementsByTagName('CommentQuestion'); | |
1989 for (var i=0; i<commentQuestionsDOM.length; i++) { | |
1990 var node = new this.commentQuestionNode(); | |
1991 node.decode(commentQuestionsDOM[i]); | |
1992 this.commentQuestions.push(node); | |
1993 } | |
1994 }; | |
1995 | |
1996 this.encode = function(root) | |
1997 { | |
1998 var AHNode = root.createElement("audioHolder"); | |
1999 AHNode.id = this.id; | |
2000 AHNode.setAttribute("hostURL",this.hostURL); | |
2001 AHNode.setAttribute("sampleRate",this.sampleRate); | |
2002 AHNode.setAttribute("randomiseOrder",this.randomiseOrder); | |
2003 AHNode.setAttribute("repeatCount",this.repeatCount); | |
2004 AHNode.setAttribute("loop",this.loop); | |
2005 AHNode.setAttribute("elementComments",this.elementComments); | |
2006 if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);} | |
2007 if(this.initialPosition != null) { | |
2008 AHNode.setAttribute("loudness",this.initialPosition*100); | |
2009 } | |
2010 for (var i=0; i<this.interfaces.length; i++) | |
2011 { | |
2012 AHNode.appendChild(this.interfaces[i].encode(root)); | |
2013 } | |
2014 | |
2015 for (var i=0; i<this.audioElements.length; i++) { | |
2016 AHNode.appendChild(this.audioElements[i].encode(root)); | |
2017 } | |
2018 // Create <CommentQuestion> | |
2019 for (var i=0; i<this.commentQuestions.length; i++) | |
2020 { | |
2021 AHNode.appendChild(this.commentQuestions[i].exportXML(root)); | |
2022 } | |
2023 | |
2024 // Create <PreTest> | |
2025 var AHPreTest = root.createElement("PreTest"); | |
2026 for (var i=0; i<this.preTest.options.length; i++) | |
2027 { | |
2028 AHPreTest.appendChild(this.preTest.options[i].exportXML(root)); | |
2029 } | |
2030 | |
2031 var AHPostTest = root.createElement("PostTest"); | |
2032 for (var i=0; i<this.postTest.options.length; i++) | |
2033 { | |
2034 AHPostTest.appendChild(this.postTest.options[i].exportXML(root)); | |
2035 } | |
2036 AHNode.appendChild(AHPreTest); | |
2037 AHNode.appendChild(AHPostTest); | |
2038 return AHNode; | |
2039 }; | |
2040 | |
2041 this.interfaceNode = function() { | |
2042 this.title = undefined; | |
2043 this.options = []; | |
1516 this.scale = []; | 2044 this.scale = []; |
1517 for (var i=0; i<scale.length; i++) { | 2045 this.name = undefined; |
1518 var arr = [null, null]; | 2046 this.decode = function(DOM) |
1519 arr[0] = scale[i].getAttribute('position'); | 2047 { |
1520 arr[1] = scale[i].textContent; | 2048 var title = DOM.getElementsByTagName('title'); |
1521 this.scale.push(arr); | 2049 if (title.length == 0) {this.title = null;} |
1522 } | 2050 else {this.title = title[0].textContent;} |
2051 var name = DOM.getAttribute("name"); | |
2052 if (name != undefined) {this.name = name;} | |
2053 this.options = parent.commonInterface.options; | |
2054 var scale = DOM.getElementsByTagName('scale'); | |
2055 this.scale = []; | |
2056 for (var i=0; i<scale.length; i++) { | |
2057 var arr = [null, null]; | |
2058 arr[0] = scale[i].getAttribute('position'); | |
2059 arr[1] = scale[i].textContent; | |
2060 this.scale.push(arr); | |
2061 } | |
2062 }; | |
2063 this.encode = function(root) | |
2064 { | |
2065 var node = root.createElement("interface"); | |
2066 if (this.title != undefined) | |
2067 { | |
2068 var title = root.createElement("title"); | |
2069 title.textContent = this.title; | |
2070 node.appendChild(title); | |
2071 } | |
2072 for (var i=0; i<this.options.length; i++) | |
2073 { | |
2074 var optionNode = root.createElement(this.options[i].type); | |
2075 if (this.options[i].type == "option") | |
2076 { | |
2077 optionNode.setAttribute("name",this.options[i].name); | |
2078 } else if (this.options[i].type == "check") { | |
2079 optionNode.setAttribute("check",this.options[i].check); | |
2080 } else if (this.options[i].type == "scalerange") { | |
2081 optionNode.setAttribute("min",this.options[i].min*100); | |
2082 optionNode.setAttribute("max",this.options[i].max*100); | |
2083 } | |
2084 node.appendChild(optionNode); | |
2085 } | |
2086 for (var i=0; i<this.scale.length; i++) { | |
2087 var scale = root.createElement("scale"); | |
2088 scale.setAttribute("position",this.scale[i][0]); | |
2089 scale.textContent = this.scale[i][1]; | |
2090 node.appendChild(scale); | |
2091 } | |
2092 return node; | |
2093 }; | |
1523 }; | 2094 }; |
1524 | 2095 |
1525 this.audioElementNode = function(parent,xml) { | 2096 this.audioElementNode = function() { |
1526 this.url = xml.getAttribute('url'); | 2097 this.url = null; |
1527 this.id = xml.id; | 2098 this.id = null; |
1528 this.parent = parent; | 2099 this.parent = null; |
1529 this.type = xml.getAttribute('type'); | 2100 this.type = "normal"; |
1530 if (this.type == null) {this.type = "normal";} | 2101 this.marker = false; |
1531 if (this.type == 'anchor') {this.anchor = true;} | 2102 this.enforce = false; |
1532 else {this.anchor = false;} | 2103 this.gain = 1.0; |
1533 if (this.type == 'reference') {this.reference = true;} | 2104 this.decode = function(parent,xml) |
1534 else {this.reference = false;} | 2105 { |
1535 | 2106 this.url = xml.getAttribute('url'); |
1536 if (this.anchor == true || this.reference == true) | 2107 this.id = xml.id; |
1537 { | 2108 this.parent = parent; |
1538 this.marker = xml.getAttribute('marker'); | 2109 this.type = xml.getAttribute('type'); |
1539 if (this.marker != undefined) | 2110 var gain = xml.getAttribute('gain'); |
2111 if (isNaN(gain) == false && gain != null) | |
1540 { | 2112 { |
1541 this.marker = Number(this.marker); | 2113 this.gain = decibelToLinear(Number(gain)); |
1542 if (isNaN(this.marker) == false) | 2114 } |
2115 if (this.type == null) {this.type = "normal";} | |
2116 if (this.type == 'anchor') {this.anchor = true;} | |
2117 else {this.anchor = false;} | |
2118 if (this.type == 'reference') {this.reference = true;} | |
2119 else {this.reference = false;} | |
2120 if (this.anchor == true || this.reference == true) | |
2121 { | |
2122 this.marker = xml.getAttribute('marker'); | |
2123 if (this.marker != undefined) | |
1543 { | 2124 { |
1544 if (this.marker > 1) | 2125 this.marker = Number(this.marker); |
1545 { this.marker /= 100.0;} | 2126 if (isNaN(this.marker) == false) |
1546 if (this.marker >= 0 && this.marker <= 1) | |
1547 { | 2127 { |
1548 this.enforce = true; | 2128 if (this.marker > 1) |
1549 return; | 2129 { this.marker /= 100.0;} |
2130 if (this.marker >= 0 && this.marker <= 1) | |
2131 { | |
2132 this.enforce = true; | |
2133 return; | |
2134 } else { | |
2135 console.log("ERROR - Marker of audioElement "+this.id+" is not between 0 and 1 (float) or 0 and 100 (integer)!"); | |
2136 console.log("ERROR - Marker not enforced!"); | |
2137 } | |
1550 } else { | 2138 } else { |
1551 console.log("ERROR - Marker of audioElement "+this.id+" is not between 0 and 1 (float) or 0 and 100 (integer)!"); | 2139 console.log("ERROR - Marker of audioElement "+this.id+" is not a number!"); |
1552 console.log("ERROR - Marker not enforced!"); | 2140 console.log("ERROR - Marker not enforced!"); |
1553 } | 2141 } |
1554 } else { | |
1555 console.log("ERROR - Marker of audioElement "+this.id+" is not a number!"); | |
1556 console.log("ERROR - Marker not enforced!"); | |
1557 } | 2142 } |
1558 } | 2143 } |
1559 } | 2144 }; |
1560 this.marker = false; | 2145 this.encode = function(root) |
1561 this.enforce = false; | 2146 { |
2147 var AENode = root.createElement("audioElements"); | |
2148 AENode.id = this.id; | |
2149 AENode.setAttribute("url",this.url); | |
2150 AENode.setAttribute("type",this.type); | |
2151 AENode.setAttribute("gain",linearToDecibel(this.gain)); | |
2152 if (this.marker != false) | |
2153 { | |
2154 AENode.setAttribute("marker",this.marker*100); | |
2155 } | |
2156 return AENode; | |
2157 }; | |
1562 }; | 2158 }; |
1563 | 2159 |
1564 this.commentQuestionNode = function(xml) { | 2160 this.commentQuestionNode = function(xml) { |
1565 this.childOption = function(element) { | 2161 this.id = null; |
2162 this.type = undefined; | |
2163 this.question = undefined; | |
2164 this.options = []; | |
2165 this.statement = undefined; | |
2166 | |
2167 this.childOption = function() { | |
1566 this.type = 'option'; | 2168 this.type = 'option'; |
1567 this.name = element.getAttribute('name'); | 2169 this.name = null; |
1568 this.text = element.textContent; | 2170 this.text = null; |
1569 }; | 2171 }; |
1570 this.id = xml.id; | 2172 this.exportXML = function(root) |
1571 if (xml.getAttribute('mandatory') == 'true') {this.mandatory = true;} | 2173 { |
1572 else {this.mandatory = false;} | 2174 var CQNode = root.createElement("CommentQuestion"); |
1573 this.type = xml.getAttribute('type'); | 2175 CQNode.id = this.id; |
1574 if (this.type == undefined) {this.type = 'text';} | 2176 CQNode.setAttribute("type",this.type); |
1575 switch (this.type) { | 2177 switch(this.type) |
1576 case 'text': | 2178 { |
1577 this.question = xml.textContent; | 2179 case "text": |
1578 break; | 2180 CQNode.textContent = this.question; |
1579 case 'radio': | 2181 break; |
1580 var child = xml.firstElementChild; | 2182 case "radio": |
1581 this.options = []; | 2183 var statement = root.createElement("statement"); |
1582 while (child != undefined) { | 2184 statement.textContent = this.statement; |
1583 if (child.nodeName == 'statement' && this.statement == undefined) { | 2185 CQNode.appendChild(statement); |
1584 this.statement = child.textContent; | 2186 for (var i=0; i<this.options.length; i++) |
1585 } else if (child.nodeName == 'option') { | 2187 { |
1586 this.options.push(new this.childOption(child)); | 2188 var optionNode = root.createElement("option"); |
2189 optionNode.setAttribute("name",this.options[i].name); | |
2190 optionNode.textContent = this.options[i].text; | |
2191 CQNode.appendChild(optionNode); | |
1587 } | 2192 } |
1588 child = child.nextElementSibling; | 2193 break; |
1589 } | 2194 case "checkbox": |
1590 break; | 2195 var statement = root.createElement("statement"); |
1591 case 'checkbox': | 2196 statement.textContent = this.statement; |
1592 var child = xml.firstElementChild; | 2197 CQNode.appendChild(statement); |
1593 this.options = []; | 2198 for (var i=0; i<this.options.length; i++) |
1594 while (child != undefined) { | 2199 { |
1595 if (child.nodeName == 'statement' && this.statement == undefined) { | 2200 var optionNode = root.createElement("option"); |
1596 this.statement = child.textContent; | 2201 optionNode.setAttribute("name",this.options[i].name); |
1597 } else if (child.nodeName == 'option') { | 2202 optionNode.textContent = this.options[i].text; |
1598 this.options.push(new this.childOption(child)); | 2203 CQNode.appendChild(optionNode); |
1599 } | 2204 } |
1600 child = child.nextElementSibling; | 2205 break; |
1601 } | 2206 } |
1602 break; | 2207 return CQNode; |
1603 } | 2208 }; |
2209 this.decode = function(xml) { | |
2210 this.id = xml.id; | |
2211 if (xml.getAttribute('mandatory') == 'true') {this.mandatory = true;} | |
2212 else {this.mandatory = false;} | |
2213 this.type = xml.getAttribute('type'); | |
2214 if (this.type == undefined) {this.type = 'text';} | |
2215 switch (this.type) { | |
2216 case 'text': | |
2217 this.question = xml.textContent; | |
2218 break; | |
2219 case 'radio': | |
2220 var child = xml.firstElementChild; | |
2221 this.options = []; | |
2222 while (child != undefined) { | |
2223 if (child.nodeName == 'statement' && this.statement == undefined) { | |
2224 this.statement = child.textContent; | |
2225 } else if (child.nodeName == 'option') { | |
2226 var node = new this.childOption(); | |
2227 node.name = child.getAttribute('name'); | |
2228 node.text = child.textContent; | |
2229 this.options.push(node); | |
2230 } | |
2231 child = child.nextElementSibling; | |
2232 } | |
2233 break; | |
2234 case 'checkbox': | |
2235 var child = xml.firstElementChild; | |
2236 this.options = []; | |
2237 while (child != undefined) { | |
2238 if (child.nodeName == 'statement' && this.statement == undefined) { | |
2239 this.statement = child.textContent; | |
2240 } else if (child.nodeName == 'option') { | |
2241 var node = new this.childOption(); | |
2242 node.name = child.getAttribute('name'); | |
2243 node.text = child.textContent; | |
2244 this.options.push(node); | |
2245 } | |
2246 child = child.nextElementSibling; | |
2247 } | |
2248 break; | |
2249 } | |
2250 }; | |
1604 }; | 2251 }; |
1605 | |
1606 this.id = xml.id; | |
1607 this.hostURL = xml.getAttribute('hostURL'); | |
1608 this.sampleRate = xml.getAttribute('sampleRate'); | |
1609 if (xml.getAttribute('randomiseOrder') == "true") {this.randomiseOrder = true;} | |
1610 else {this.randomiseOrder = false;} | |
1611 this.repeatCount = xml.getAttribute('repeatCount'); | |
1612 if (xml.getAttribute('loop') == 'true') {this.loop = true;} | |
1613 else {this.loop == false;} | |
1614 if (xml.getAttribute('elementComments') == "true") {this.elementComments = true;} | |
1615 else {this.elementComments = false;} | |
1616 | |
1617 this.preTest = new parent.prepostNode('pretest',xml.getElementsByTagName('PreTest')); | |
1618 this.postTest = new parent.prepostNode('posttest',xml.getElementsByTagName('PostTest')); | |
1619 | |
1620 this.interfaces = []; | |
1621 var interfaceDOM = xml.getElementsByTagName('interface'); | |
1622 for (var i=0; i<interfaceDOM.length; i++) { | |
1623 this.interfaces.push(new this.interfaceNode(interfaceDOM[i])); | |
1624 } | |
1625 | |
1626 this.commentBoxPrefix = xml.getElementsByTagName('commentBoxPrefix'); | |
1627 if (this.commentBoxPrefix.length != 0) { | |
1628 this.commentBoxPrefix = this.commentBoxPrefix[0].textContent; | |
1629 } else { | |
1630 this.commentBoxPrefix = "Comment on track"; | |
1631 } | |
1632 | |
1633 this.audioElements =[]; | |
1634 var audioElementsDOM = xml.getElementsByTagName('audioElements'); | |
1635 this.outsideReference = null; | |
1636 for (var i=0; i<audioElementsDOM.length; i++) { | |
1637 if (audioElementsDOM[i].getAttribute('type') == 'outsidereference') { | |
1638 if (this.outsideReference == null) { | |
1639 this.outsideReference = new this.audioElementNode(this,audioElementsDOM[i]); | |
1640 } else { | |
1641 console.log('Error only one audioelement can be of type outsidereference per audioholder'); | |
1642 this.audioElements.push(new this.audioElementNode(this,audioElementsDOM[i])); | |
1643 console.log('Element id '+audioElementsDOM[i].id+' made into normal node'); | |
1644 } | |
1645 } else { | |
1646 this.audioElements.push(new this.audioElementNode(this,audioElementsDOM[i])); | |
1647 } | |
1648 } | |
1649 | |
1650 if (this.randomiseOrder) { | |
1651 this.audioElements = randomiseOrder(this.audioElements); | |
1652 } | |
1653 | |
1654 this.commentQuestions = []; | |
1655 var commentQuestionsDOM = xml.getElementsByTagName('CommentQuestion'); | |
1656 for (var i=0; i<commentQuestionsDOM.length; i++) { | |
1657 this.commentQuestions.push(new this.commentQuestionNode(commentQuestionsDOM[i])); | |
1658 } | |
1659 }; | 2252 }; |
1660 } | 2253 } |
1661 | 2254 |
1662 function Interface(specificationObject) { | 2255 function Interface(specificationObject) { |
1663 // This handles the bindings between the interface and the audioEngineContext; | 2256 // This handles the bindings between the interface and the audioEngineContext; |
1664 this.specification = specificationObject; | 2257 this.specification = specificationObject; |
1665 this.insertPoint = document.getElementById("topLevelBody"); | 2258 this.insertPoint = document.getElementById("topLevelBody"); |
2259 | |
2260 this.newPage = function(audioHolderObject) | |
2261 { | |
2262 audioEngineContext.newTestPage(); | |
2263 /// CHECK FOR SAMPLE RATE COMPATIBILITY | |
2264 if (audioHolderObject.sampleRate != undefined) { | |
2265 if (Number(audioHolderObject.sampleRate) != audioContext.sampleRate) { | |
2266 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.'; | |
2267 alert(errStr); | |
2268 return; | |
2269 } | |
2270 } | |
2271 | |
2272 audioEngineContext.loopPlayback = audioHolderObject.loop; | |
2273 // Delete any previous audioObjects associated with the audioEngine | |
2274 audioEngineContext.audioObjects = []; | |
2275 interfaceContext.deleteCommentBoxes(); | |
2276 interfaceContext.deleteCommentQuestions(); | |
2277 loadTest(audioHolderObject); | |
2278 }; | |
1666 | 2279 |
1667 // Bounded by interface!! | 2280 // Bounded by interface!! |
1668 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels | 2281 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels |
1669 // For example, APE returns the slider position normalised in a <value> tag. | 2282 // For example, APE returns the slider position normalised in a <value> tag. |
1670 this.interfaceObjects = []; | 2283 this.interfaceObjects = []; |
1671 this.interfaceObject = function(){}; | 2284 this.interfaceObject = function(){}; |
1672 | 2285 |
1673 this.resizeWindow = function(event) | 2286 this.resizeWindow = function(event) |
1674 { | 2287 { |
2288 popup.resize(event); | |
1675 for(var i=0; i<this.commentBoxes.length; i++) | 2289 for(var i=0; i<this.commentBoxes.length; i++) |
1676 {this.commentBoxes[i].resize();} | 2290 {this.commentBoxes[i].resize();} |
1677 for(var i=0; i<this.commentQuestions.length; i++) | 2291 for(var i=0; i<this.commentQuestions.length; i++) |
1678 {this.commentQuestions[i].resize();} | 2292 {this.commentQuestions[i].resize();} |
1679 try | 2293 try |
2087 this.playbackObject; | 2701 this.playbackObject; |
2088 | 2702 |
2089 this.setTimePerPixel = function(audioObject) { | 2703 this.setTimePerPixel = function(audioObject) { |
2090 //maxTime must be in seconds | 2704 //maxTime must be in seconds |
2091 this.playbackObject = audioObject; | 2705 this.playbackObject = audioObject; |
2092 this.maxTime = audioObject.buffer.duration; | 2706 this.maxTime = audioObject.buffer.buffer.duration; |
2093 var width = 490; //500 - 10, 5 each side of the tracker head | 2707 var width = 490; //500 - 10, 5 each side of the tracker head |
2094 this.timePerPixel = this.maxTime/490; | 2708 this.timePerPixel = this.maxTime/490; |
2095 if (this.maxTime < 60) { | 2709 if (this.maxTime < 60) { |
2096 this.curTimeSpan.textContent = '0.00'; | 2710 this.curTimeSpan.textContent = '0.00'; |
2097 } else { | 2711 } else { |
2199 var check_pass = true; | 2813 var check_pass = true; |
2200 var error_obj = []; | 2814 var error_obj = []; |
2201 for (var i = 0; i<audioEngineContext.audioObjects.length; i++) | 2815 for (var i = 0; i<audioEngineContext.audioObjects.length; i++) |
2202 { | 2816 { |
2203 var object = audioEngineContext.audioObjects[i]; | 2817 var object = audioEngineContext.audioObjects[i]; |
2204 var time = object.buffer.duration; | 2818 var time = object.buffer.buffer.duration; |
2205 var metric = object.metric; | 2819 var metric = object.metric; |
2206 var passed = false; | 2820 var passed = false; |
2207 for (var j=0; j<metric.listenTracker.length; j++) | 2821 for (var j=0; j<metric.listenTracker.length; j++) |
2208 { | 2822 { |
2209 var bt = metric.listenTracker[j].getElementsByTagName('buffertime'); | 2823 var bt = metric.listenTracker[j].getElementsByTagName('buffertime'); |
2223 error_obj.push(i); | 2837 error_obj.push(i); |
2224 } | 2838 } |
2225 } | 2839 } |
2226 if (check_pass == false) | 2840 if (check_pass == false) |
2227 { | 2841 { |
2228 var str_start = "You have not listened to fragments "; | 2842 var str_start = "You have not completely listened to fragments "; |
2229 for (var i=0; i<error_obj.length; i++) | 2843 for (var i=0; i<error_obj.length; i++) |
2230 { | 2844 { |
2231 str_start += error_obj[i]; | 2845 str_start += error_obj[i]; |
2232 if (i != error_obj.length-1) | 2846 if (i != error_obj.length-1) |
2233 { | 2847 { |
2237 str_start += ". Please keep listening"; | 2851 str_start += ". Please keep listening"; |
2238 console.log("[ALERT]: "+str_start); | 2852 console.log("[ALERT]: "+str_start); |
2239 alert(str_start); | 2853 alert(str_start); |
2240 } | 2854 } |
2241 }; | 2855 }; |
2856 this.checkAllMoved = function() | |
2857 { | |
2858 var str = "You have not moved "; | |
2859 var failed = []; | |
2860 for (var i in audioEngineContext.audioObjects) | |
2861 { | |
2862 if(audioEngineContext.audioObjects[i].metric.wasMoved == false && audioEngineContext.audioObjects[i].specification.type != 'outsidereference') | |
2863 { | |
2864 failed.push(audioEngineContext.audioObjects[i].id); | |
2865 } | |
2866 } | |
2867 if (failed.length == 0) | |
2868 { | |
2869 return true; | |
2870 } else if (failed.length == 1) | |
2871 { | |
2872 str += 'track '+failed[0]; | |
2873 } else { | |
2874 str += 'tracks '; | |
2875 for (var i=0; i<failed.length-1; i++) | |
2876 { | |
2877 str += failed[i]+', '; | |
2878 } | |
2879 str += 'and '+failed[i]; | |
2880 } | |
2881 str +='.'; | |
2882 alert(str); | |
2883 console.log(str); | |
2884 return false; | |
2885 }; | |
2886 this.checkAllPlayed = function() | |
2887 { | |
2888 var str = "You have not played "; | |
2889 var failed = []; | |
2890 for (var i in audioEngineContext.audioObjects) | |
2891 { | |
2892 if(audioEngineContext.audioObjects[i].metric.wasListenedTo == false) | |
2893 { | |
2894 failed.push(audioEngineContext.audioObjects[i].id); | |
2895 } | |
2896 } | |
2897 if (failed.length == 0) | |
2898 { | |
2899 return true; | |
2900 } else if (failed.length == 1) | |
2901 { | |
2902 str += 'track '+failed[0]; | |
2903 } else { | |
2904 str += 'tracks '; | |
2905 for (var i=0; i<failed.length-1; i++) | |
2906 { | |
2907 str += failed[i]+', '; | |
2908 } | |
2909 str += 'and '+failed[i]; | |
2910 } | |
2911 str +='.'; | |
2912 alert(str); | |
2913 console.log(str); | |
2914 return false; | |
2915 }; | |
2242 } | 2916 } |