comparison core.js @ 1090:c07b9e2312ba

Merge
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Wed, 09 Mar 2016 14:36:47 +0000
parents
children cfbd8ff10757
comparison
equal deleted inserted replaced
-1:000000000000 1090:c07b9e2312ba
1 /**
2 * core.js
3 *
4 * Main script to run, calls all other core functions and manages loading/store to backend.
5 * Also contains all global variables.
6 */
7
8 /* create the web audio API context and store in audioContext*/
9 var audioContext; // Hold the browser web audio API
10 var projectXML; // Hold the parsed setup XML
11 var schemaXSD; // Hold the parsed schema XSD
12 var specification;
13 var interfaceContext;
14 var storage;
15 var popup; // Hold the interfacePopup object
16 var testState;
17 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
18 var audioEngineContext; // The custome AudioEngine object
19 var projectReturn; // Hold the URL for the return
20
21
22 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
23 AudioBufferSourceNode.prototype.owner = undefined;
24 // Add a prototype to the bufferSourceNode to hold when the object was given a play command
25 AudioBufferSourceNode.prototype.playbackStartTime = undefined;
26 // Add a prototype to the bufferNode to hold the desired LINEAR gain
27 AudioBuffer.prototype.playbackGain = undefined;
28 // Add a prototype to the bufferNode to hold the computed LUFS loudness
29 AudioBuffer.prototype.lufs = undefined;
30
31 // Firefox does not have an XMLDocument.prototype.getElementsByName
32 // and there is no searchAll style command, this custom function will
33 // search all children recusrively for the name. Used for XSD where all
34 // element nodes must have a name and therefore can pull the schema node
35 XMLDocument.prototype.getAllElementsByName = function(name)
36 {
37 name = String(name);
38 var selected = this.documentElement.getAllElementsByName(name);
39 return selected;
40 }
41
42 Element.prototype.getAllElementsByName = function(name)
43 {
44 name = String(name);
45 var selected = [];
46 var node = this.firstElementChild;
47 while(node != null)
48 {
49 if (node.getAttribute('name') == name)
50 {
51 selected.push(node);
52 }
53 if (node.childElementCount > 0)
54 {
55 selected = selected.concat(node.getAllElementsByName(name));
56 }
57 node = node.nextElementSibling;
58 }
59 return selected;
60 }
61
62 XMLDocument.prototype.getAllElementsByTagName = function(name)
63 {
64 name = String(name);
65 var selected = this.documentElement.getAllElementsByTagName(name);
66 return selected;
67 }
68
69 Element.prototype.getAllElementsByTagName = function(name)
70 {
71 name = String(name);
72 var selected = [];
73 var node = this.firstElementChild;
74 while(node != null)
75 {
76 if (node.nodeName == name)
77 {
78 selected.push(node);
79 }
80 if (node.childElementCount > 0)
81 {
82 selected = selected.concat(node.getAllElementsByTagName(name));
83 }
84 node = node.nextElementSibling;
85 }
86 return selected;
87 }
88
89 // Firefox does not have an XMLDocument.prototype.getElementsByName
90 if (typeof XMLDocument.prototype.getElementsByName != "function") {
91 XMLDocument.prototype.getElementsByName = function(name)
92 {
93 name = String(name);
94 var node = this.documentElement.firstElementChild;
95 var selected = [];
96 while(node != null)
97 {
98 if (node.getAttribute('name') == name)
99 {
100 selected.push(node);
101 }
102 node = node.nextElementSibling;
103 }
104 return selected;
105 }
106 }
107
108 window.onload = function() {
109 // Function called once the browser has loaded all files.
110 // This should perform any initial commands such as structure / loading documents
111
112 // Create a web audio API context
113 // Fixed for cross-browser support
114 var AudioContext = window.AudioContext || window.webkitAudioContext;
115 audioContext = new AudioContext;
116
117 // Create test state
118 testState = new stateMachine();
119
120 // Create the popup interface object
121 popup = new interfacePopup();
122
123 // Create the specification object
124 specification = new Specification();
125
126 // Create the interface object
127 interfaceContext = new Interface(specification);
128
129 // Create the storage object
130 storage = new Storage();
131 // Define window callbacks for interface
132 window.onresize = function(event){interfaceContext.resizeWindow(event);};
133 };
134
135 function loadProjectSpec(url) {
136 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
137 // If url is null, request client to upload project XML document
138 var xmlhttp = new XMLHttpRequest();
139 xmlhttp.open("GET",'test-schema.xsd',true);
140 xmlhttp.onload = function()
141 {
142 schemaXSD = xmlhttp.response;
143 var parse = new DOMParser();
144 specification.schema = parse.parseFromString(xmlhttp.response,'text/xml');
145 var r = new XMLHttpRequest();
146 r.open('GET',url,true);
147 r.onload = function() {
148 loadProjectSpecCallback(r.response);
149 };
150 r.onerror = function() {
151 document.getElementsByTagName('body')[0].innerHTML = null;
152 var msg = document.createElement("h3");
153 msg.textContent = "FATAL ERROR";
154 var span = document.createElement("p");
155 span.textContent = "There was an error when loading your XML file. Please check your path in the URL. After the path to this page, there should be '?url=path/to/your/file.xml'. Check the spelling of your filename as well. If you are still having issues, check the log of the python server or your webserver distribution for 404 codes for your file.";
156 document.getElementsByTagName('body')[0].appendChild(msg);
157 document.getElementsByTagName('body')[0].appendChild(span);
158 }
159 r.send();
160 };
161 xmlhttp.send();
162 };
163
164 function loadProjectSpecCallback(response) {
165 // Function called after asynchronous download of XML project specification
166 //var decode = $.parseXML(response);
167 //projectXML = $(decode);
168
169 // Check if XML is new or a resumption
170 var parse = new DOMParser();
171 var responseDocument = parse.parseFromString(response,'text/xml');
172 var errorNode = responseDocument.getElementsByTagName('parsererror');
173 if (errorNode.length >= 1)
174 {
175 var msg = document.createElement("h3");
176 msg.textContent = "FATAL ERROR";
177 var span = document.createElement("span");
178 span.textContent = "The XML parser returned the following errors when decoding your XML file";
179 document.getElementsByTagName('body')[0].innerHTML = null;
180 document.getElementsByTagName('body')[0].appendChild(msg);
181 document.getElementsByTagName('body')[0].appendChild(span);
182 document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
183 return;
184 }
185 if (responseDocument.children[0].nodeName == "waet") {
186 // document is a specification
187
188 // Perform XML schema validation
189 var Module = {
190 xml: response,
191 schema: schemaXSD,
192 arguments:["--noout", "--schema", 'test-schema.xsd','document.xml']
193 };
194 projectXML = responseDocument;
195 var xmllint = validateXML(Module);
196 console.log(xmllint);
197 if(xmllint != 'document.xml validates\n')
198 {
199 document.getElementsByTagName('body')[0].innerHTML = null;
200 var msg = document.createElement("h3");
201 msg.textContent = "FATAL ERROR";
202 var span = document.createElement("h4");
203 span.textContent = "The XML validator returned the following errors when decoding your XML file";
204 document.getElementsByTagName('body')[0].appendChild(msg);
205 document.getElementsByTagName('body')[0].appendChild(span);
206 xmllint = xmllint.split('\n');
207 for (var i in xmllint)
208 {
209 document.getElementsByTagName('body')[0].appendChild(document.createElement('br'));
210 var span = document.createElement("span");
211 span.textContent = xmllint[i];
212 document.getElementsByTagName('body')[0].appendChild(span);
213 }
214 return;
215 }
216 // Build the specification
217 specification.decode(projectXML);
218 // Generate the session-key
219 storage.initialise();
220
221 } else if (responseDocument.children[0].nodeName == "waetresult") {
222 // document is a result
223 projectXML = responseDocument.getElementsByTagName('waet')[0];
224 // Build the specification
225 specification.decode(projectXML);
226 // Use the session-key
227 var sessionKey = responseDocument.children[0].getAttribute(key);
228 storage.initialise(sessionKey);
229 }
230 /// CHECK FOR SAMPLE RATE COMPATIBILITY
231 if (specification.sampleRate != undefined) {
232 if (Number(specification.sampleRate) != audioContext.sampleRate) {
233 var errStr = 'Sample rates do not match! Requested '+Number(specification.sampleRate)+', got '+audioContext.sampleRate+'. Please set the sample rate to match before completing this test.';
234 alert(errStr);
235 return;
236 }
237 }
238
239 // Detect the interface to use and load the relevant javascripts.
240 var interfaceJS = document.createElement('script');
241 interfaceJS.setAttribute("type","text/javascript");
242 switch(specification.interface)
243 {
244 case "APE":
245 interfaceJS.setAttribute("src","interfaces/ape.js");
246
247 // APE comes with a css file
248 var css = document.createElement('link');
249 css.rel = 'stylesheet';
250 css.type = 'text/css';
251 css.href = 'interfaces/ape.css';
252
253 document.getElementsByTagName("head")[0].appendChild(css);
254 break;
255
256 case "MUSHRA":
257 interfaceJS.setAttribute("src","interfaces/mushra.js");
258
259 // MUSHRA comes with a css file
260 var css = document.createElement('link');
261 css.rel = 'stylesheet';
262 css.type = 'text/css';
263 css.href = 'interfaces/mushra.css';
264
265 document.getElementsByTagName("head")[0].appendChild(css);
266 break;
267
268 case "AB":
269 interfaceJS.setAttribute("src","interfaces/AB.js");
270
271 // AB comes with a css file
272 var css = document.createElement('link');
273 css.rel = 'stylesheet';
274 css.type = 'text/css';
275 css.href = 'interfaces/AB.css';
276
277 document.getElementsByTagName("head")[0].appendChild(css);
278 break;
279 case "Bipolar":
280 case "ACR":
281 case "DCR":
282 case "CCR":
283 case "ABC":
284 // Above enumerate to horizontal sliders
285 interfaceJS.setAttribute("src","interfaces/horizontal-sliders.js");
286
287 // horizontal-sliders comes with a css file
288 var css = document.createElement('link');
289 css.rel = 'stylesheet';
290 css.type = 'text/css';
291 css.href = 'interfaces/horizontal-sliders.css';
292
293 document.getElementsByTagName("head")[0].appendChild(css);
294 break;
295 case "discrete":
296 case "likert":
297 // Above enumerate to horizontal discrete radios
298 interfaceJS.setAttribute("src","interfaces/discrete.js");
299
300 // horizontal-sliders comes with a css file
301 var css = document.createElement('link');
302 css.rel = 'stylesheet';
303 css.type = 'text/css';
304 css.href = 'interfaces/discrete.css';
305
306 document.getElementsByTagName("head")[0].appendChild(css);
307 break;
308 }
309 document.getElementsByTagName("head")[0].appendChild(interfaceJS);
310
311 // Create the audio engine object
312 audioEngineContext = new AudioEngine(specification);
313 }
314
315 function createProjectSave(destURL) {
316 // Save the data from interface into XML and send to destURL
317 // If destURL is null then download XML in client
318 // Now time to render file locally
319 var xmlDoc = interfaceXMLSave();
320 var parent = document.createElement("div");
321 parent.appendChild(xmlDoc);
322 var file = [parent.innerHTML];
323 if (destURL == "local") {
324 var bb = new Blob(file,{type : 'application/xml'});
325 var dnlk = window.URL.createObjectURL(bb);
326 var a = document.createElement("a");
327 a.hidden = '';
328 a.href = dnlk;
329 a.download = "save.xml";
330 a.textContent = "Save File";
331
332 popup.showPopup();
333 popup.popupContent.innerHTML = "</span>Please save the file below to give to your test supervisor</span><br>";
334 popup.popupContent.appendChild(a);
335 } else {
336 var xmlhttp = new XMLHttpRequest;
337 xmlhttp.open("POST",destURL,true);
338 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
339 xmlhttp.onerror = function(){
340 console.log('Error saving file to server! Presenting download locally');
341 createProjectSave(null);
342 };
343 xmlhttp.onload = function() {
344 console.log(xmlhttp);
345 if (this.status >= 300) {
346 console.log("WARNING - Could not update at this time");
347 } else {
348 var parser = new DOMParser();
349 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
350 var response = xmlDoc.getElementsByTagName('response')[0];
351 if (response.getAttribute("state") == "OK") {
352 var file = response.getElementsByTagName("file")[0];
353 console.log("Save: OK, written "+file.getAttribute("bytes")+"B");
354 } else {
355 var message = response.getElementsByTagName("message");
356 console.log("Save: Error! "+message.textContent);
357 createProjectSave("local");
358 }
359 }
360 };
361 xmlhttp.send(file);
362 popup.showPopup();
363 popup.popupContent.innerHTML = null;
364 popup.popupContent.textContent = "Submitting. Please Wait";
365 popup.hideNextButton();
366 popup.hidePreviousButton();
367 }
368 }
369
370 function errorSessionDump(msg){
371 // Create the partial interface XML save
372 // Include error node with message on why the dump occured
373 popup.showPopup();
374 popup.popupContent.innerHTML = null;
375 var err = document.createElement('error');
376 var parent = document.createElement("div");
377 if (typeof msg === "object")
378 {
379 err.appendChild(msg);
380 popup.popupContent.appendChild(msg);
381
382 } else {
383 err.textContent = msg;
384 popup.popupContent.innerHTML = "ERROR : "+msg;
385 }
386 var xmlDoc = interfaceXMLSave();
387 xmlDoc.appendChild(err);
388 parent.appendChild(xmlDoc);
389 var file = [parent.innerHTML];
390 var bb = new Blob(file,{type : 'application/xml'});
391 var dnlk = window.URL.createObjectURL(bb);
392 var a = document.createElement("a");
393 a.hidden = '';
394 a.href = dnlk;
395 a.download = "save.xml";
396 a.textContent = "Save File";
397
398
399
400 popup.popupContent.appendChild(a);
401 }
402
403 // Only other global function which must be defined in the interface class. Determines how to create the XML document.
404 function interfaceXMLSave(){
405 // Create the XML string to be exported with results
406 return storage.finish();
407 }
408
409 function linearToDecibel(gain)
410 {
411 return 20.0*Math.log10(gain);
412 }
413
414 function decibelToLinear(gain)
415 {
416 return Math.pow(10,gain/20.0);
417 }
418
419 function randomString(length) {
420 return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
421 }
422
423 function interfacePopup() {
424 // Creates an object to manage the popup
425 this.popup = null;
426 this.popupContent = null;
427 this.popupTitle = null;
428 this.popupResponse = null;
429 this.buttonProceed = null;
430 this.buttonPrevious = null;
431 this.popupOptions = null;
432 this.currentIndex = null;
433 this.node = null;
434 this.store = null;
435 $(window).keypress(function(e){
436 if (e.keyCode == 13 && popup.popup.style.visibility == 'visible')
437 {
438 console.log(e);
439 popup.buttonProceed.onclick();
440 e.preventDefault();
441 }
442 });
443
444 this.createPopup = function(){
445 // Create popup window interface
446 var insertPoint = document.getElementById("topLevelBody");
447
448 this.popup = document.getElementById('popupHolder');
449 this.popup.style.left = (window.innerWidth/2)-250 + 'px';
450 this.popup.style.top = (window.innerHeight/2)-125 + 'px';
451
452 this.popupContent = document.getElementById('popupContent');
453
454 this.popupTitle = document.getElementById('popupTitle');
455
456 this.popupResponse = document.getElementById('popupResponse');
457
458 this.buttonProceed = document.getElementById('popup-proceed');
459 this.buttonProceed.onclick = function(){popup.proceedClicked();};
460
461 this.buttonPrevious = document.getElementById('popup-previous');
462 this.buttonPrevious.onclick = function(){popup.previousClick();};
463
464 this.hidePopup();
465
466 this.popup.style.zIndex = -1;
467 this.popup.style.visibility = 'hidden';
468 };
469
470 this.showPopup = function(){
471 if (this.popup == null) {
472 this.createPopup();
473 }
474 this.popup.style.zIndex = 3;
475 this.popup.style.visibility = 'visible';
476 var blank = document.getElementsByClassName('testHalt')[0];
477 blank.style.zIndex = 2;
478 blank.style.visibility = 'visible';
479 };
480
481 this.hidePopup = function(){
482 this.popup.style.zIndex = -1;
483 this.popup.style.visibility = 'hidden';
484 var blank = document.getElementsByClassName('testHalt')[0];
485 blank.style.zIndex = -2;
486 blank.style.visibility = 'hidden';
487 this.buttonPrevious.style.visibility = 'inherit';
488 };
489
490 this.postNode = function() {
491 // This will take the node from the popupOptions and display it
492 var node = this.popupOptions[this.currentIndex];
493 this.popupResponse.innerHTML = null;
494 this.popupTitle.textContent = node.specification.statement;
495 if (node.specification.type == 'question') {
496 var textArea = document.createElement('textarea');
497 switch (node.specification.boxsize) {
498 case 'small':
499 textArea.cols = "20";
500 textArea.rows = "1";
501 break;
502 case 'normal':
503 textArea.cols = "30";
504 textArea.rows = "2";
505 break;
506 case 'large':
507 textArea.cols = "40";
508 textArea.rows = "5";
509 break;
510 case 'huge':
511 textArea.cols = "50";
512 textArea.rows = "10";
513 break;
514 }
515 if (node.response == undefined) {
516 node.response = "";
517 } else {
518 textArea.value = node.response;
519 }
520 this.popupResponse.appendChild(textArea);
521 textArea.focus();
522 this.popupResponse.style.textAlign="center";
523 this.popupResponse.style.left="0%";
524 } else if (node.specification.type == 'checkbox') {
525 if (node.response == undefined) {
526 node.response = Array(node.specification.options.length);
527 }
528 var index = 0;
529 var max_w = 0;
530 for (var option of node.specification.options) {
531 var input = document.createElement('input');
532 input.id = option.name;
533 input.type = 'checkbox';
534 var span = document.createElement('span');
535 span.textContent = option.text;
536 var hold = document.createElement('div');
537 hold.setAttribute('name','option');
538 hold.style.padding = '4px';
539 hold.appendChild(input);
540 hold.appendChild(span);
541 this.popupResponse.appendChild(hold);
542 if (node.response[index] != undefined){
543 if (node.response[index].checked == true) {
544 input.checked = "true";
545 }
546 }
547 var w = $(span).width();
548 if (w > max_w)
549 max_w = w;
550 index++;
551 }
552 max_w += 12;
553 this.popupResponse.style.textAlign="";
554 var leftP = ((max_w/500)/2)*100;
555 this.popupResponse.style.left=leftP+"%";
556 } else if (node.specification.type == 'radio') {
557 if (node.response == undefined) {
558 node.response = {name: "", text: ""};
559 }
560 var index = 0;
561 var max_w = 0;
562 for (var option of node.specification.options) {
563 var input = document.createElement('input');
564 input.id = option.name;
565 input.type = 'radio';
566 input.name = node.specification.id;
567 var span = document.createElement('span');
568 span.textContent = option.text;
569 var hold = document.createElement('div');
570 hold.setAttribute('name','option');
571 hold.style.padding = '4px';
572 hold.appendChild(input);
573 hold.appendChild(span);
574 this.popupResponse.appendChild(hold);
575 if (input.id == node.response.name) {
576 input.checked = "true";
577 }
578 var w = $(span).width();
579 if (w > max_w)
580 max_w = w;
581 }
582 max_w += 12;
583 this.popupResponse.style.textAlign="";
584 var leftP = ((max_w/500)/2)*100;
585 this.popupResponse.style.left=leftP+"%";
586 } else if (node.specification.type == 'number') {
587 var input = document.createElement('input');
588 input.type = 'textarea';
589 if (node.min != null) {input.min = node.specification.min;}
590 if (node.max != null) {input.max = node.specification.max;}
591 if (node.step != null) {input.step = node.specification.step;}
592 if (node.response != undefined) {
593 input.value = node.response;
594 }
595 this.popupResponse.appendChild(input);
596 this.popupResponse.style.textAlign="center";
597 this.popupResponse.style.left="0%";
598 }
599 if(this.currentIndex+1 == this.popupOptions.length) {
600 if (this.node.location == "pre") {
601 this.buttonProceed.textContent = 'Start';
602 } else {
603 this.buttonProceed.textContent = 'Submit';
604 }
605 } else {
606 this.buttonProceed.textContent = 'Next';
607 }
608 if(this.currentIndex > 0)
609 this.buttonPrevious.style.visibility = 'visible';
610 else
611 this.buttonPrevious.style.visibility = 'hidden';
612 };
613
614 this.initState = function(node,store) {
615 //Call this with your preTest and postTest nodes when needed to
616 // initialise the popup procedure.
617 if (node.options.length > 0) {
618 this.popupOptions = [];
619 this.node = node;
620 this.store = store;
621 for (var opt of node.options)
622 {
623 this.popupOptions.push({
624 specification: opt,
625 response: null
626 });
627 }
628 this.currentIndex = 0;
629 this.showPopup();
630 this.postNode();
631 } else {
632 advanceState();
633 }
634 };
635
636 this.proceedClicked = function() {
637 // Each time the popup button is clicked!
638 var node = this.popupOptions[this.currentIndex];
639 if (node.specification.type == 'question') {
640 // Must extract the question data
641 var textArea = $(popup.popupContent).find('textarea')[0];
642 if (node.specification.mandatory == true && textArea.value.length == 0) {
643 alert('This question is mandatory');
644 return;
645 } else {
646 // Save the text content
647 console.log("Question: "+ node.specification.statement);
648 console.log("Question Response: "+ textArea.value);
649 node.response = textArea.value;
650 }
651 } else if (node.specification.type == 'checkbox') {
652 // Must extract checkbox data
653 console.log("Checkbox: "+ node.specification.statement);
654 var inputs = this.popupResponse.getElementsByTagName('input');
655 node.response = [];
656 for (var i=0; i<node.specification.options.length; i++) {
657 node.response.push({
658 name: node.specification.options[i].name,
659 text: node.specification.options[i].text,
660 checked: inputs[i].checked
661 });
662 console.log(node.specification.options[i].name+": "+ inputs[i].checked);
663 }
664 } else if (node.specification.type == "radio") {
665 var optHold = this.popupResponse;
666 console.log("Radio: "+ node.specification.statement);
667 node.response = null;
668 var i=0;
669 var inputs = optHold.getElementsByTagName('input');
670 while(node.response == null) {
671 if (i == inputs.length)
672 {
673 if (node.specification.mandatory == true)
674 {
675 alert("This radio is mandatory");
676 } else {
677 node.response = -1;
678 }
679 return;
680 }
681 if (inputs[i].checked == true) {
682 node.response = node.specification.options[i];
683 console.log("Selected: "+ node.specification.options[i].name);
684 }
685 i++;
686 }
687 } else if (node.specification.type == "number") {
688 var input = this.popupContent.getElementsByTagName('input')[0];
689 if (node.mandatory == true && input.value.length == 0) {
690 alert('This question is mandatory. Please enter a number');
691 return;
692 }
693 var enteredNumber = Number(input.value);
694 if (isNaN(enteredNumber)) {
695 alert('Please enter a valid number');
696 return;
697 }
698 if (enteredNumber < node.min && node.min != null) {
699 alert('Number is below the minimum value of '+node.min);
700 return;
701 }
702 if (enteredNumber > node.max && node.max != null) {
703 alert('Number is above the maximum value of '+node.max);
704 return;
705 }
706 node.response = input.value;
707 }
708 this.currentIndex++;
709 if (this.currentIndex < this.popupOptions.length) {
710 this.postNode();
711 } else {
712 // Reached the end of the popupOptions
713 this.hidePopup();
714 for (var node of this.popupOptions)
715 {
716 this.store.postResult(node);
717 }
718 advanceState();
719 }
720 };
721
722 this.previousClick = function() {
723 // Triggered when the 'Back' button is clicked in the survey
724 if (this.currentIndex > 0) {
725 this.currentIndex--;
726 this.postNode();
727 }
728 };
729
730 this.resize = function(event)
731 {
732 // Called on window resize;
733 if (this.popup != null) {
734 this.popup.style.left = (window.innerWidth/2)-250 + 'px';
735 this.popup.style.top = (window.innerHeight/2)-125 + 'px';
736 var blank = document.getElementsByClassName('testHalt')[0];
737 blank.style.width = window.innerWidth;
738 blank.style.height = window.innerHeight;
739 }
740 };
741 this.hideNextButton = function() {
742 this.buttonProceed.style.visibility = "hidden";
743 }
744 this.hidePreviousButton = function() {
745 this.buttonPrevious.style.visibility = "hidden";
746 }
747 this.showNextButton = function() {
748 this.buttonProceed.style.visibility = "visible";
749 }
750 this.showPreviousButton = function() {
751 this.buttonPrevious.style.visibility = "visible";
752 }
753 }
754
755 function advanceState()
756 {
757 // Just for complete clarity
758 testState.advanceState();
759 }
760
761 function stateMachine()
762 {
763 // Object prototype for tracking and managing the test state
764 this.stateMap = [];
765 this.preTestSurvey = null;
766 this.postTestSurvey = null;
767 this.stateIndex = null;
768 this.currentStateMap = null;
769 this.currentStatePosition = null;
770 this.currentStore = null;
771 this.initialise = function(){
772
773 // Get the data from Specification
774 var pageHolder = [];
775 for (var page of specification.pages)
776 {
777 var repeat = page.repeatCount;
778 while(repeat >= 0)
779 {
780 pageHolder.push(page);
781 repeat--;
782 }
783 }
784 if (specification.randomiseOrder)
785 {
786 pageHolder = randomiseOrder(pageHolder);
787 }
788 for (var i=0; i<pageHolder.length; i++)
789 {
790 pageHolder[i].presentedId = i;
791 }
792 for (var i=0; i<specification.pages.length; i++)
793 {
794 if (specification.testPages <= i && specification.testPages != 0) {break;}
795 this.stateMap.push(pageHolder[i]);
796 storage.createTestPageStore(pageHolder[i]);
797 for (var element of pageHolder[i].audioElements) {
798 var URL = pageHolder[i].hostURL + element.url;
799 var buffer = null;
800 for (var buffObj of audioEngineContext.buffers) {
801 if (URL == buffObj.url) {
802 buffer = buffObj;
803 break;
804 }
805 }
806 if (buffer == null) {
807 buffer = new audioEngineContext.bufferObj();
808 buffer.getMedia(URL);
809 audioEngineContext.buffers.push(buffer);
810 }
811 }
812 }
813
814 if (specification.preTest != null) {this.preTestSurvey = specification.preTest;}
815 if (specification.postTest != null) {this.postTestSurvey = specification.postTest;}
816
817 if (this.stateMap.length > 0) {
818 if(this.stateIndex != null) {
819 console.log('NOTE - State already initialise');
820 }
821 this.stateIndex = -1;
822 } else {
823 console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
824 }
825 };
826 this.advanceState = function(){
827 if (this.stateIndex == null) {
828 this.initialise();
829 }
830 storage.update();
831 if (this.stateIndex == -1) {
832 this.stateIndex++;
833 console.log('Starting test...');
834 if (this.preTestSurvey != null)
835 {
836 popup.initState(this.preTestSurvey,storage.globalPreTest);
837 } else {
838 this.advanceState();
839 }
840 } else if (this.stateIndex == this.stateMap.length)
841 {
842 // All test pages complete, post test
843 console.log('Ending test ...');
844 this.stateIndex++;
845 if (this.postTestSurvey == null) {
846 this.advanceState();
847 } else {
848 popup.initState(this.postTestSurvey,storage.globalPostTest);
849 }
850 } else if (this.stateIndex > this.stateMap.length)
851 {
852 createProjectSave(specification.projectReturn);
853 }
854 else
855 {
856 if (this.currentStateMap == null)
857 {
858 this.currentStateMap = this.stateMap[this.stateIndex];
859 if (this.currentStateMap.randomiseOrder)
860 {
861 this.currentStateMap.audioElements = randomiseOrder(this.currentStateMap.audioElements);
862 }
863 this.currentStore = storage.testPages[this.stateIndex];
864 if (this.currentStateMap.preTest != null)
865 {
866 this.currentStatePosition = 'pre';
867 popup.initState(this.currentStateMap.preTest,storage.testPages[this.stateIndex].preTest);
868 } else {
869 this.currentStatePosition = 'test';
870 }
871 interfaceContext.newPage(this.currentStateMap,storage.testPages[this.stateIndex]);
872 return;
873 }
874 switch(this.currentStatePosition)
875 {
876 case 'pre':
877 this.currentStatePosition = 'test';
878 break;
879 case 'test':
880 this.currentStatePosition = 'post';
881 // Save the data
882 this.testPageCompleted();
883 if (this.currentStateMap.postTest == null)
884 {
885 this.advanceState();
886 return;
887 } else {
888 popup.initState(this.currentStateMap.postTest,storage.testPages[this.stateIndex].postTest);
889 }
890 break;
891 case 'post':
892 this.stateIndex++;
893 this.currentStateMap = null;
894 this.advanceState();
895 break;
896 };
897 }
898 };
899
900 this.testPageCompleted = function() {
901 // Function called each time a test page has been completed
902 var storePoint = storage.testPages[this.stateIndex];
903 // First get the test metric
904
905 var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
906 if (audioEngineContext.metric.enableTestTimer)
907 {
908 var testTime = storePoint.parent.document.createElement('metricresult');
909 testTime.id = 'testTime';
910 testTime.textContent = audioEngineContext.timer.testDuration;
911 metric.appendChild(testTime);
912 }
913
914 var audioObjects = audioEngineContext.audioObjects;
915 for (var ao of audioEngineContext.audioObjects)
916 {
917 ao.exportXMLDOM();
918 }
919 for (var element of interfaceContext.commentQuestions)
920 {
921 element.exportXMLDOM(storePoint);
922 }
923 pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
924 };
925 }
926
927 function AudioEngine(specification) {
928
929 // Create two output paths, the main outputGain and fooGain.
930 // Output gain is default to 1 and any items for playback route here
931 // Foo gain is used for analysis to ensure paths get processed, but are not heard
932 // because web audio will optimise and any route which does not go to the destination gets ignored.
933 this.outputGain = audioContext.createGain();
934 this.fooGain = audioContext.createGain();
935 this.fooGain.gain = 0;
936
937 // Use this to detect playback state: 0 - stopped, 1 - playing
938 this.status = 0;
939
940 // Connect both gains to output
941 this.outputGain.connect(audioContext.destination);
942 this.fooGain.connect(audioContext.destination);
943
944 // Create the timer Object
945 this.timer = new timer();
946 // Create session metrics
947 this.metric = new sessionMetrics(this,specification);
948
949 this.loopPlayback = false;
950
951 this.pageStore = null;
952
953 // Create store for new audioObjects
954 this.audioObjects = [];
955
956 this.buffers = [];
957 this.bufferObj = function()
958 {
959 this.url = null;
960 this.buffer = null;
961 this.xmlRequest = new XMLHttpRequest();
962 this.xmlRequest.parent = this;
963 this.users = [];
964 this.progress = 0;
965 this.status = 0;
966 this.ready = function()
967 {
968 if (this.status >= 2)
969 {
970 this.status = 3;
971 }
972 for (var i=0; i<this.users.length; i++)
973 {
974 this.users[i].state = 1;
975 if (this.users[i].interfaceDOM != null)
976 {
977 this.users[i].bufferLoaded(this);
978 }
979 }
980 };
981 this.getMedia = function(url) {
982 this.url = url;
983 this.xmlRequest.open('GET',this.url,true);
984 this.xmlRequest.responseType = 'arraybuffer';
985
986 var bufferObj = this;
987
988 // Create callback to decode the data asynchronously
989 this.xmlRequest.onloadend = function() {
990 // Use inbuilt WAVE decoder first
991 if (this.status == -1) {return;}
992 var waveObj = new WAVE();
993 if (waveObj.open(bufferObj.xmlRequest.response) == 0)
994 {
995 bufferObj.buffer = audioContext.createBuffer(waveObj.num_channels,waveObj.num_samples,waveObj.sample_rate);
996 for (var c=0; c<waveObj.num_channels; c++)
997 {
998 var buffer_ptr = bufferObj.buffer.getChannelData(c);
999 for (var n=0; n<waveObj.num_samples; n++)
1000 {
1001 buffer_ptr[n] = waveObj.decoded_data[c][n];
1002 }
1003 }
1004
1005 delete waveObj;
1006 } else {
1007 audioContext.decodeAudioData(bufferObj.xmlRequest.response, function(decodedData) {
1008 bufferObj.buffer = decodedData;
1009 }, function(e){
1010 // Should only be called if there was an error, but sometimes gets called continuously
1011 // Check here if the error is genuine
1012 if (bufferObj.xmlRequest.response == undefined) {
1013 // Genuine error
1014 console.log('FATAL - Error loading buffer on '+audioObj.id);
1015 if (request.status == 404)
1016 {
1017 console.log('FATAL - Fragment '+audioObj.id+' 404 error');
1018 console.log('URL: '+audioObj.url);
1019 errorSessionDump('Fragment '+audioObj.id+' 404 error');
1020 }
1021 this.parent.status = -1;
1022 }
1023 });
1024 }
1025 if (bufferObj.buffer != undefined)
1026 {
1027 bufferObj.status = 2;
1028 calculateLoudness(bufferObj,"I");
1029 }
1030 };
1031
1032 // Create callback for any error in loading
1033 this.xmlRequest.onerror = function() {
1034 this.parent.status = -1;
1035 for (var i=0; i<this.parent.users.length; i++)
1036 {
1037 this.parent.users[i].state = -1;
1038 if (this.parent.users[i].interfaceDOM != null)
1039 {
1040 this.parent.users[i].bufferLoaded(this);
1041 }
1042 }
1043 }
1044
1045 this.progress = 0;
1046 this.progressCallback = function(event){
1047 if (event.lengthComputable)
1048 {
1049 this.parent.progress = event.loaded / event.total;
1050 for (var i=0; i<this.parent.users.length; i++)
1051 {
1052 if(this.parent.users[i].interfaceDOM != null)
1053 {
1054 if (typeof this.parent.users[i].interfaceDOM.updateLoading === "function")
1055 {
1056 this.parent.users[i].interfaceDOM.updateLoading(this.parent.progress*100);
1057 }
1058 }
1059 }
1060 }
1061 };
1062 this.xmlRequest.addEventListener("progress", this.progressCallback);
1063 this.status = 1;
1064 this.xmlRequest.send();
1065 };
1066
1067 this.registerAudioObject = function(audioObject)
1068 {
1069 // Called by an audioObject to register to the buffer for use
1070 // First check if already in the register pool
1071 for (var objects of this.users)
1072 {
1073 if (audioObject.id == objects.id){return 0;}
1074 }
1075 this.users.push(audioObject);
1076 if (this.status == 3 || this.status == -1)
1077 {
1078 // The buffer is already ready, trigger bufferLoaded
1079 audioObject.bufferLoaded(this);
1080 }
1081 }
1082 };
1083
1084 this.play = function(id) {
1085 // Start the timer and set the audioEngine state to playing (1)
1086 if (this.status == 0 && this.loopPlayback) {
1087 // Check if all audioObjects are ready
1088 if(this.checkAllReady())
1089 {
1090 this.status = 1;
1091 this.setSynchronousLoop();
1092 }
1093 }
1094 else
1095 {
1096 this.status = 1;
1097 }
1098 if (this.status== 1) {
1099 this.timer.startTest();
1100 if (id == undefined) {
1101 id = -1;
1102 console.log('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
1103 return;
1104 } else {
1105 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
1106 }
1107 if (this.loopPlayback) {
1108 var setTime = audioContext.currentTime;
1109 for (var i=0; i<this.audioObjects.length; i++)
1110 {
1111 this.audioObjects[i].play(setTime);
1112 if (id == i) {
1113 this.audioObjects[i].loopStart(setTime);
1114 } else {
1115 this.audioObjects[i].loopStop(setTime);
1116 }
1117 }
1118 } else {
1119 var setTime = audioContext.currentTime+0.1;
1120 for (var i=0; i<this.audioObjects.length; i++)
1121 {
1122 if (i != id) {
1123 this.audioObjects[i].stop(setTime);
1124 } else if (i == id) {
1125 this.audioObjects[id].play(setTime);
1126 }
1127 }
1128 }
1129 interfaceContext.playhead.start();
1130 }
1131 };
1132
1133 this.stop = function() {
1134 // Send stop and reset command to all playback buffers
1135 if (this.status == 1) {
1136 var setTime = audioContext.currentTime+0.1;
1137 for (var i=0; i<this.audioObjects.length; i++)
1138 {
1139 this.audioObjects[i].stop(setTime);
1140 }
1141 interfaceContext.playhead.stop();
1142 }
1143 };
1144
1145 this.newTrack = function(element) {
1146 // Pull data from given URL into new audio buffer
1147 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
1148
1149 // Create the audioObject with ID of the new track length;
1150 audioObjectId = this.audioObjects.length;
1151 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
1152
1153 // Check if audioObject buffer is currently stored by full URL
1154 var URL = testState.currentStateMap.hostURL + element.url;
1155 var buffer = null;
1156 for (var i=0; i<this.buffers.length; i++)
1157 {
1158 if (URL == this.buffers[i].url)
1159 {
1160 buffer = this.buffers[i];
1161 break;
1162 }
1163 }
1164 if (buffer == null)
1165 {
1166 console.log("[WARN]: Buffer was not loaded in pre-test! "+URL);
1167 buffer = new this.bufferObj();
1168 this.buffers.push(buffer);
1169 buffer.getMedia(URL);
1170 }
1171 this.audioObjects[audioObjectId].specification = element;
1172 this.audioObjects[audioObjectId].url = URL;
1173 // Obtain store node
1174 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
1175 for (var i=0; i<aeNodes.length; i++)
1176 {
1177 if(aeNodes[i].id == element.id)
1178 {
1179 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
1180 break;
1181 }
1182 }
1183 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
1184 return this.audioObjects[audioObjectId];
1185 };
1186
1187 this.newTestPage = function(audioHolderObject,store) {
1188 this.pageStore = store;
1189 this.status = 0;
1190 this.audioObjectsReady = false;
1191 this.metric.reset();
1192 for (var i=0; i < this.buffers.length; i++)
1193 {
1194 this.buffers[i].users = [];
1195 }
1196 this.audioObjects = [];
1197 this.timer = new timer();
1198 this.loopPlayback = audioHolderObject.loop;
1199 };
1200
1201 this.checkAllPlayed = function() {
1202 arr = [];
1203 for (var id=0; id<this.audioObjects.length; id++) {
1204 if (this.audioObjects[id].metric.wasListenedTo == false) {
1205 arr.push(this.audioObjects[id].id);
1206 }
1207 }
1208 return arr;
1209 };
1210
1211 this.checkAllReady = function() {
1212 var ready = true;
1213 for (var i=0; i<this.audioObjects.length; i++) {
1214 if (this.audioObjects[i].state == 0) {
1215 // Track not ready
1216 console.log('WAIT -- audioObject '+i+' not ready yet!');
1217 ready = false;
1218 };
1219 }
1220 return ready;
1221 };
1222
1223 this.setSynchronousLoop = function() {
1224 // Pads the signals so they are all exactly the same length
1225 var length = 0;
1226 var maxId;
1227 for (var i=0; i<this.audioObjects.length; i++)
1228 {
1229 if (length < this.audioObjects[i].buffer.buffer.length)
1230 {
1231 length = this.audioObjects[i].buffer.buffer.length;
1232 maxId = i;
1233 }
1234 }
1235 // Extract the audio and zero-pad
1236 for (var i=0; i<this.audioObjects.length; i++)
1237 {
1238 var orig = this.audioObjects[i].buffer.buffer;
1239 var hold = audioContext.createBuffer(orig.numberOfChannels,length,orig.sampleRate);
1240 for (var c=0; c<orig.numberOfChannels; c++)
1241 {
1242 var inData = hold.getChannelData(c);
1243 var outData = orig.getChannelData(c);
1244 for (var n=0; n<orig.length; n++)
1245 {inData[n] = outData[n];}
1246 }
1247 hold.playbackGain = orig.playbackGain;
1248 hold.lufs = orig.lufs;
1249 this.audioObjects[i].buffer.buffer = hold;
1250 }
1251 };
1252
1253 this.exportXML = function()
1254 {
1255
1256 };
1257
1258 }
1259
1260 function audioObject(id) {
1261 // The main buffer object with common control nodes to the AudioEngine
1262
1263 this.specification;
1264 this.id = id;
1265 this.state = 0; // 0 - no data, 1 - ready
1266 this.url = null; // Hold the URL given for the output back to the results.
1267 this.metric = new metricTracker(this);
1268 this.storeDOM = null;
1269
1270 // Bindings for GUI
1271 this.interfaceDOM = null;
1272 this.commentDOM = null;
1273
1274 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
1275 this.bufferNode = undefined;
1276 this.outputGain = audioContext.createGain();
1277
1278 this.onplayGain = 1.0;
1279
1280 // Connect buffer to the audio graph
1281 this.outputGain.connect(audioEngineContext.outputGain);
1282
1283 // the audiobuffer is not designed for multi-start playback
1284 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
1285 this.buffer;
1286
1287 this.bufferLoaded = function(callee)
1288 {
1289 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
1290 // audioObject and trigger the interfaceDOM.enable() function for user feedback
1291 if (callee.status == -1) {
1292 // ERROR
1293 this.state = -1;
1294 if (this.interfaceDOM != null) {this.interfaceDOM.error();}
1295 this.buffer = callee;
1296 return;
1297 }
1298 if (audioEngineContext.loopPlayback){
1299 // First copy the buffer into this.buffer
1300 this.buffer = new audioEngineContext.bufferObj();
1301 this.buffer.url = callee.url;
1302 this.buffer.buffer = audioContext.createBuffer(callee.buffer.numberOfChannels, callee.buffer.length, callee.buffer.sampleRate);
1303 for (var c=0; c<callee.buffer.numberOfChannels; c++)
1304 {
1305 var src = callee.buffer.getChannelData(c);
1306 var dst = this.buffer.buffer.getChannelData(c);
1307 for (var n=0; n<src.length; n++)
1308 {
1309 dst[n] = src[n];
1310 }
1311 }
1312 } else {
1313 this.buffer = callee;
1314 }
1315 this.state = 1;
1316 this.buffer.buffer.playbackGain = callee.buffer.playbackGain;
1317 this.buffer.buffer.lufs = callee.buffer.lufs;
1318 var targetLUFS = this.specification.parent.loudness || specification.loudness;
1319 if (typeof targetLUFS === "number")
1320 {
1321 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
1322 } else {
1323 this.buffer.buffer.playbackGain = 1.0;
1324 }
1325 if (this.interfaceDOM != null) {
1326 this.interfaceDOM.enable();
1327 }
1328 this.onplayGain = decibelToLinear(this.specification.gain)*this.buffer.buffer.playbackGain;
1329 this.storeDOM.setAttribute('playGain',linearToDecibel(this.onplayGain));
1330 };
1331
1332 this.bindInterface = function(interfaceObject)
1333 {
1334 this.interfaceDOM = interfaceObject;
1335 this.metric.initialise(interfaceObject.getValue());
1336 if (this.state == 1)
1337 {
1338 this.interfaceDOM.enable();
1339 } else if (this.state == -1) {
1340 // ERROR
1341 this.interfaceDOM.error();
1342 return;
1343 }
1344 this.storeDOM.setAttribute('presentedId',interfaceObject.getPresentedId());
1345 };
1346
1347 this.loopStart = function(setTime) {
1348 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain,setTime);
1349 this.metric.startListening(audioEngineContext.timer.getTestTime());
1350 this.interfaceDOM.startPlayback();
1351 };
1352
1353 this.loopStop = function(setTime) {
1354 if (this.outputGain.gain.value != 0.0) {
1355 this.outputGain.gain.linearRampToValueAtTime(0.0,setTime);
1356 this.metric.stopListening(audioEngineContext.timer.getTestTime());
1357 }
1358 this.interfaceDOM.stopPlayback();
1359 };
1360
1361 this.play = function(startTime) {
1362 if (this.bufferNode == undefined && this.buffer.buffer != undefined) {
1363 this.bufferNode = audioContext.createBufferSource();
1364 this.bufferNode.owner = this;
1365 this.bufferNode.connect(this.outputGain);
1366 this.bufferNode.buffer = this.buffer.buffer;
1367 this.bufferNode.loop = audioEngineContext.loopPlayback;
1368 this.bufferNode.onended = function(event) {
1369 // Safari does not like using 'this' to reference the calling object!
1370 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
1371 if (event.currentTarget != null) {
1372 event.currentTarget.owner.stop(audioContext.currentTime+1);
1373 }
1374 };
1375 if (this.bufferNode.loop == false) {
1376 this.metric.startListening(audioEngineContext.timer.getTestTime());
1377 this.outputGain.gain.setValueAtTime(this.onplayGain,startTime);
1378 this.interfaceDOM.startPlayback();
1379 } else {
1380 this.outputGain.gain.setValueAtTime(0.0,startTime);
1381 }
1382 this.bufferNode.start(startTime);
1383 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
1384 }
1385 };
1386
1387 this.stop = function(stopTime) {
1388 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
1389 if (this.bufferNode != undefined)
1390 {
1391 this.metric.stopListening(audioEngineContext.timer.getTestTime(),this.getCurrentPosition());
1392 this.bufferNode.stop(stopTime);
1393 this.bufferNode = undefined;
1394 }
1395 this.outputGain.gain.value = 0.0;
1396 this.interfaceDOM.stopPlayback();
1397 };
1398
1399 this.getCurrentPosition = function() {
1400 var time = audioEngineContext.timer.getTestTime();
1401 if (this.bufferNode != undefined) {
1402 var position = (time - this.bufferNode.playbackStartTime)%this.buffer.buffer.duration;
1403 if (isNaN(position)){return 0;}
1404 return position;
1405 } else {
1406 return 0;
1407 }
1408 };
1409
1410 this.exportXMLDOM = function() {
1411 var file = storage.document.createElement('file');
1412 file.setAttribute('sampleRate',this.buffer.buffer.sampleRate);
1413 file.setAttribute('channels',this.buffer.buffer.numberOfChannels);
1414 file.setAttribute('sampleCount',this.buffer.buffer.length);
1415 file.setAttribute('duration',this.buffer.buffer.duration);
1416 this.storeDOM.appendChild(file);
1417 if (this.specification.type != 'outside-reference') {
1418 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
1419 if (interfaceXML != null)
1420 {
1421 if (interfaceXML.length == undefined) {
1422 this.storeDOM.appendChild(interfaceXML);
1423 } else {
1424 for (var i=0; i<interfaceXML.length; i++)
1425 {
1426 this.storeDOM.appendChild(interfaceXML[i]);
1427 }
1428 }
1429 }
1430 if (this.commentDOM != null) {
1431 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
1432 }
1433 }
1434 var nodes = this.metric.exportXMLDOM();
1435 var mroot = this.storeDOM.getElementsByTagName('metric')[0];
1436 for (var i=0; i<nodes.length; i++)
1437 {
1438 mroot.appendChild(nodes[i]);
1439 }
1440 };
1441 }
1442
1443 function timer()
1444 {
1445 /* Timer object used in audioEngine to keep track of session timings
1446 * Uses the timer of the web audio API, so sample resolution
1447 */
1448 this.testStarted = false;
1449 this.testStartTime = 0;
1450 this.testDuration = 0;
1451 this.minimumTestTime = 0; // No minimum test time
1452 this.startTest = function()
1453 {
1454 if (this.testStarted == false)
1455 {
1456 this.testStartTime = audioContext.currentTime;
1457 this.testStarted = true;
1458 this.updateTestTime();
1459 audioEngineContext.metric.initialiseTest();
1460 }
1461 };
1462 this.stopTest = function()
1463 {
1464 if (this.testStarted)
1465 {
1466 this.testDuration = this.getTestTime();
1467 this.testStarted = false;
1468 } else {
1469 console.log('ERR: Test tried to end before beginning');
1470 }
1471 };
1472 this.updateTestTime = function()
1473 {
1474 if (this.testStarted)
1475 {
1476 this.testDuration = audioContext.currentTime - this.testStartTime;
1477 }
1478 };
1479 this.getTestTime = function()
1480 {
1481 this.updateTestTime();
1482 return this.testDuration;
1483 };
1484 }
1485
1486 function sessionMetrics(engine,specification)
1487 {
1488 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
1489 */
1490 this.engine = engine;
1491 this.lastClicked = -1;
1492 this.data = -1;
1493 this.reset = function() {
1494 this.lastClicked = -1;
1495 this.data = -1;
1496 };
1497
1498 this.enableElementInitialPosition = false;
1499 this.enableElementListenTracker = false;
1500 this.enableElementTimer = false;
1501 this.enableElementTracker = false;
1502 this.enableFlagListenedTo = false;
1503 this.enableFlagMoved = false;
1504 this.enableTestTimer = false;
1505 // Obtain the metrics enabled
1506 for (var i=0; i<specification.metrics.enabled.length; i++)
1507 {
1508 var node = specification.metrics.enabled[i];
1509 switch(node)
1510 {
1511 case 'testTimer':
1512 this.enableTestTimer = true;
1513 break;
1514 case 'elementTimer':
1515 this.enableElementTimer = true;
1516 break;
1517 case 'elementTracker':
1518 this.enableElementTracker = true;
1519 break;
1520 case 'elementListenTracker':
1521 this.enableElementListenTracker = true;
1522 break;
1523 case 'elementInitialPosition':
1524 this.enableElementInitialPosition = true;
1525 break;
1526 case 'elementFlagListenedTo':
1527 this.enableFlagListenedTo = true;
1528 break;
1529 case 'elementFlagMoved':
1530 this.enableFlagMoved = true;
1531 break;
1532 case 'elementFlagComments':
1533 this.enableFlagComments = true;
1534 break;
1535 }
1536 }
1537 this.initialiseTest = function(){};
1538 }
1539
1540 function metricTracker(caller)
1541 {
1542 /* Custom object to track and collect metric data
1543 * Used only inside the audioObjects object.
1544 */
1545
1546 this.listenedTimer = 0;
1547 this.listenStart = 0;
1548 this.listenHold = false;
1549 this.initialPosition = -1;
1550 this.movementTracker = [];
1551 this.listenTracker =[];
1552 this.wasListenedTo = false;
1553 this.wasMoved = false;
1554 this.hasComments = false;
1555 this.parent = caller;
1556
1557 this.initialise = function(position)
1558 {
1559 if (this.initialPosition == -1) {
1560 this.initialPosition = position;
1561 this.moved(0,position);
1562 }
1563 };
1564
1565 this.moved = function(time,position)
1566 {
1567 if (time > 0) {this.wasMoved = true;}
1568 this.movementTracker[this.movementTracker.length] = [time, position];
1569 };
1570
1571 this.startListening = function(time)
1572 {
1573 if (this.listenHold == false)
1574 {
1575 this.wasListenedTo = true;
1576 this.listenStart = time;
1577 this.listenHold = true;
1578
1579 var evnt = document.createElement('event');
1580 var testTime = document.createElement('testTime');
1581 testTime.setAttribute('start',time);
1582 var bufferTime = document.createElement('bufferTime');
1583 bufferTime.setAttribute('start',this.parent.getCurrentPosition());
1584 evnt.appendChild(testTime);
1585 evnt.appendChild(bufferTime);
1586 this.listenTracker.push(evnt);
1587
1588 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
1589 }
1590 };
1591
1592 this.stopListening = function(time,bufferStopTime)
1593 {
1594 if (this.listenHold == true)
1595 {
1596 var diff = time - this.listenStart;
1597 this.listenedTimer += (diff);
1598 this.listenStart = 0;
1599 this.listenHold = false;
1600
1601 var evnt = this.listenTracker[this.listenTracker.length-1];
1602 var testTime = evnt.getElementsByTagName('testTime')[0];
1603 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
1604 testTime.setAttribute('stop',time);
1605 if (bufferStopTime == undefined) {
1606 bufferTime.setAttribute('stop',this.parent.getCurrentPosition());
1607 } else {
1608 bufferTime.setAttribute('stop',bufferStopTime);
1609 }
1610 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
1611 }
1612 };
1613
1614 this.exportXMLDOM = function() {
1615 var storeDOM = [];
1616 if (audioEngineContext.metric.enableElementTimer) {
1617 var mElementTimer = storage.document.createElement('metricresult');
1618 mElementTimer.setAttribute('name','enableElementTimer');
1619 mElementTimer.textContent = this.listenedTimer;
1620 storeDOM.push(mElementTimer);
1621 }
1622 if (audioEngineContext.metric.enableElementTracker) {
1623 var elementTrackerFull = storage.document.createElement('metricResult');
1624 elementTrackerFull.setAttribute('name','elementTrackerFull');
1625 for (var k=0; k<this.movementTracker.length; k++)
1626 {
1627 var timePos = storage.document.createElement('movement');
1628 timePos.setAttribute("time",this.movementTracker[k][0]);
1629 timePos.setAttribute("value",this.movementTracker[k][1]);
1630 elementTrackerFull.appendChild(timePos);
1631 }
1632 storeDOM.push(elementTrackerFull);
1633 }
1634 if (audioEngineContext.metric.enableElementListenTracker) {
1635 var elementListenTracker = storage.document.createElement('metricResult');
1636 elementListenTracker.setAttribute('name','elementListenTracker');
1637 for (var k=0; k<this.listenTracker.length; k++) {
1638 elementListenTracker.appendChild(this.listenTracker[k]);
1639 }
1640 storeDOM.push(elementListenTracker);
1641 }
1642 if (audioEngineContext.metric.enableElementInitialPosition) {
1643 var elementInitial = storage.document.createElement('metricResult');
1644 elementInitial.setAttribute('name','elementInitialPosition');
1645 elementInitial.textContent = this.initialPosition;
1646 storeDOM.push(elementInitial);
1647 }
1648 if (audioEngineContext.metric.enableFlagListenedTo) {
1649 var flagListenedTo = storage.document.createElement('metricResult');
1650 flagListenedTo.setAttribute('name','elementFlagListenedTo');
1651 flagListenedTo.textContent = this.wasListenedTo;
1652 storeDOM.push(flagListenedTo);
1653 }
1654 if (audioEngineContext.metric.enableFlagMoved) {
1655 var flagMoved = storage.document.createElement('metricResult');
1656 flagMoved.setAttribute('name','elementFlagMoved');
1657 flagMoved.textContent = this.wasMoved;
1658 storeDOM.push(flagMoved);
1659 }
1660 if (audioEngineContext.metric.enableFlagComments) {
1661 var flagComments = storage.document.createElement('metricResult');
1662 flagComments.setAttribute('name','elementFlagComments');
1663 if (this.parent.commentDOM == null)
1664 {flag.textContent = 'false';}
1665 else if (this.parent.commentDOM.textContent.length == 0)
1666 {flag.textContent = 'false';}
1667 else
1668 {flag.textContet = 'true';}
1669 storeDOM.push(flagComments);
1670 }
1671 return storeDOM;
1672 };
1673 }
1674
1675 function randomiseOrder(input)
1676 {
1677 // This takes an array of information and randomises the order
1678 var N = input.length;
1679
1680 var inputSequence = []; // For safety purposes: keep track of randomisation
1681 for (var counter = 0; counter < N; ++counter)
1682 inputSequence.push(counter) // Fill array
1683 var inputSequenceClone = inputSequence.slice(0);
1684
1685 var holdArr = [];
1686 var outputSequence = [];
1687 for (var n=0; n<N; n++)
1688 {
1689 // First pick a random number
1690 var r = Math.random();
1691 // Multiply and floor by the number of elements left
1692 r = Math.floor(r*input.length);
1693 // Pick out that element and delete from the array
1694 holdArr.push(input.splice(r,1)[0]);
1695 // Do the same with sequence
1696 outputSequence.push(inputSequence.splice(r,1)[0]);
1697 }
1698 console.log(inputSequenceClone.toString()); // print original array to console
1699 console.log(outputSequence.toString()); // print randomised array to console
1700 return holdArr;
1701 }
1702
1703 function returnDateNode()
1704 {
1705 // Create an XML Node for the Date and Time a test was conducted
1706 // Structure is
1707 // <datetime>
1708 // <date year="##" month="##" day="##">DD/MM/YY</date>
1709 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
1710 // </datetime>
1711 var dateTime = new Date();
1712 var year = document.createAttribute('year');
1713 var month = document.createAttribute('month');
1714 var day = document.createAttribute('day');
1715 var hour = document.createAttribute('hour');
1716 var minute = document.createAttribute('minute');
1717 var secs = document.createAttribute('secs');
1718
1719 year.nodeValue = dateTime.getFullYear();
1720 month.nodeValue = dateTime.getMonth()+1;
1721 day.nodeValue = dateTime.getDate();
1722 hour.nodeValue = dateTime.getHours();
1723 minute.nodeValue = dateTime.getMinutes();
1724 secs.nodeValue = dateTime.getSeconds();
1725
1726 var hold = document.createElement("datetime");
1727 var date = document.createElement("date");
1728 date.textContent = year.nodeValue+'/'+month.nodeValue+'/'+day.nodeValue;
1729 var time = document.createElement("time");
1730 time.textContent = hour.nodeValue+':'+minute.nodeValue+':'+secs.nodeValue;
1731
1732 date.setAttributeNode(year);
1733 date.setAttributeNode(month);
1734 date.setAttributeNode(day);
1735 time.setAttributeNode(hour);
1736 time.setAttributeNode(minute);
1737 time.setAttributeNode(secs);
1738
1739 hold.appendChild(date);
1740 hold.appendChild(time);
1741 return hold;
1742
1743 }
1744
1745 function Specification() {
1746 // Handles the decoding of the project specification XML into a simple JavaScript Object.
1747
1748 this.interface = null;
1749 this.projectReturn = "null";
1750 this.randomiseOrder = null;
1751 this.testPages = null;
1752 this.pages = [];
1753 this.metrics = null;
1754 this.interfaces = null;
1755 this.loudness = null;
1756 this.errors = [];
1757 this.schema = null;
1758
1759 this.processAttribute = function(attribute,schema)
1760 {
1761 // attribute is the string returned from getAttribute on the XML
1762 // schema is the <xs:attribute> node
1763 if (schema.getAttribute('name') == undefined && schema.getAttribute('ref') != undefined)
1764 {
1765 schema = this.schema.getAllElementsByName(schema.getAttribute('ref'))[0];
1766 }
1767 var defaultOpt = schema.getAttribute('default');
1768 if (attribute == null) {
1769 attribute = defaultOpt;
1770 }
1771 var dataType = schema.getAttribute('type');
1772 if (typeof dataType == "string") { dataType = dataType.substr(3);}
1773 else {dataType = "string";}
1774 if (attribute == null)
1775 {
1776 return attribute;
1777 }
1778 switch(dataType)
1779 {
1780 case "boolean":
1781 if (attribute == 'true'){attribute = true;}else{attribute=false;}
1782 break;
1783 case "negativeInteger":
1784 case "positiveInteger":
1785 case "nonNegativeInteger":
1786 case "nonPositiveInteger":
1787 case "integer":
1788 case "decimal":
1789 case "short":
1790 attribute = Number(attribute);
1791 break;
1792 case "string":
1793 default:
1794 attribute = String(attribute);
1795 break;
1796 }
1797 return attribute;
1798 };
1799
1800 this.decode = function(projectXML) {
1801 this.errors = [];
1802 // projectXML - DOM Parsed document
1803 this.projectXML = projectXML.childNodes[0];
1804 var setupNode = projectXML.getElementsByTagName('setup')[0];
1805 var schemaSetup = this.schema.getAllElementsByName('setup')[0];
1806 // First decode the attributes
1807 var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
1808 for (var i in attributes)
1809 {
1810 if (isNaN(Number(i)) == true){break;}
1811 var attributeName = attributes[i].getAttribute('name');
1812 var projectAttr = setupNode.getAttribute(attributeName);
1813 projectAttr = this.processAttribute(projectAttr,attributes[i]);
1814 switch(typeof projectAttr)
1815 {
1816 case "number":
1817 case "boolean":
1818 eval('this.'+attributeName+' = '+projectAttr);
1819 break;
1820 case "string":
1821 eval('this.'+attributeName+' = "'+projectAttr+'"');
1822 break;
1823 }
1824
1825 }
1826
1827 this.metrics = new this.metricNode();
1828
1829 this.metrics.decode(this,setupNode.getElementsByTagName('metric')[0]);
1830
1831 // Now process the survey node options
1832 var survey = setupNode.getElementsByTagName('survey');
1833 for (var i in survey) {
1834 if (isNaN(Number(i)) == true){break;}
1835 var location = survey[i].getAttribute('location');
1836 if (location == 'pre' || location == 'before')
1837 {
1838 if (this.preTest != null){this.errors.push("Already a pre/before test survey defined! Ignoring second!!");}
1839 else {
1840 this.preTest = new this.surveyNode();
1841 this.preTest.decode(this,survey[i]);
1842 }
1843 } else if (location == 'post' || location == 'after') {
1844 if (this.postTest != null){this.errors.push("Already a post/after test survey defined! Ignoring second!!");}
1845 else {
1846 this.postTest = new this.surveyNode();
1847 this.postTest.decode(this,survey[i]);
1848 }
1849 }
1850 }
1851
1852 var interfaceNode = setupNode.getElementsByTagName('interface');
1853 if (interfaceNode.length > 1)
1854 {
1855 this.errors.push("Only one <interface> node in the <setup> node allowed! Others except first ingnored!");
1856 }
1857 this.interfaces = new this.interfaceNode();
1858 if (interfaceNode.length != 0)
1859 {
1860 interfaceNode = interfaceNode[0];
1861 this.interfaces.decode(this,interfaceNode,this.schema.getAllElementsByName('interface')[1]);
1862 }
1863
1864 // Page tags
1865 var pageTags = projectXML.getElementsByTagName('page');
1866 var pageSchema = this.schema.getAllElementsByName('page')[0];
1867 for (var i=0; i<pageTags.length; i++)
1868 {
1869 var node = new this.page();
1870 node.decode(this,pageTags[i],pageSchema);
1871 this.pages.push(node);
1872 }
1873 };
1874
1875 this.encode = function()
1876 {
1877 var RootDocument = document.implementation.createDocument(null,"waet");
1878 var root = RootDocument.children[0];
1879 root.setAttribute("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");
1880 root.setAttribute("xsi:noNamespaceSchemaLocation","test-schema.xsd");
1881 // Build setup node
1882 var setup = RootDocument.createElement("setup");
1883 var schemaSetup = this.schema.getAllElementsByName('setup')[0];
1884 // First decode the attributes
1885 var attributes = schemaSetup.getAllElementsByTagName('xs:attribute');
1886 for (var i=0; i<attributes.length; i++)
1887 {
1888 var name = attributes[i].getAttribute("name");
1889 if (name == undefined) {
1890 name = attributes[i].getAttribute("ref");
1891 }
1892 if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
1893 {
1894 eval("setup.setAttribute('"+name+"',this."+name+")");
1895 }
1896 }
1897 root.appendChild(setup);
1898 // Survey node
1899 setup.appendChild(this.preTest.encode(RootDocument));
1900 setup.appendChild(this.postTest.encode(RootDocument));
1901 setup.appendChild(this.metrics.encode(RootDocument));
1902 setup.appendChild(this.interfaces.encode(RootDocument));
1903 for (var page of this.pages)
1904 {
1905 root.appendChild(page.encode(RootDocument));
1906 }
1907 return RootDocument;
1908 };
1909
1910 this.surveyNode = function() {
1911 this.location = null;
1912 this.options = [];
1913 this.schema = specification.schema.getAllElementsByName('survey')[0];
1914
1915 this.OptionNode = function() {
1916 this.type = undefined;
1917 this.schema = specification.schema.getAllElementsByName('surveyentry')[0];
1918 this.id = undefined;
1919 this.mandatory = undefined;
1920 this.statement = undefined;
1921 this.boxsize = undefined;
1922 this.options = [];
1923 this.min = undefined;
1924 this.max = undefined;
1925 this.step = undefined;
1926
1927 this.decode = function(parent,child)
1928 {
1929 var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
1930 for (var i in attributeMap){
1931 if(isNaN(Number(i)) == true){break;}
1932 var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
1933 var projectAttr = child.getAttribute(attributeName);
1934 projectAttr = parent.processAttribute(projectAttr,attributeMap[i]);
1935 switch(typeof projectAttr)
1936 {
1937 case "number":
1938 case "boolean":
1939 eval('this.'+attributeName+' = '+projectAttr);
1940 break;
1941 case "string":
1942 eval('this.'+attributeName+' = "'+projectAttr+'"');
1943 break;
1944 }
1945 }
1946 this.statement = child.getElementsByTagName('statement')[0].textContent;
1947 if (this.type == "checkbox" || this.type == "radio") {
1948 var children = child.getElementsByTagName('option');
1949 if (children.length == null) {
1950 console.log('Malformed' +child.nodeName+ 'entry');
1951 this.statement = 'Malformed' +child.nodeName+ 'entry';
1952 this.type = 'statement';
1953 } else {
1954 this.options = [];
1955 for (var i in children)
1956 {
1957 if (isNaN(Number(i))==true){break;}
1958 this.options.push({
1959 name: children[i].getAttribute('name'),
1960 text: children[i].textContent
1961 });
1962 }
1963 }
1964 }
1965 };
1966
1967 this.exportXML = function(doc)
1968 {
1969 var node = doc.createElement('surveyentry');
1970 node.setAttribute('type',this.type);
1971 var statement = doc.createElement('statement');
1972 statement.textContent = this.statement;
1973 node.appendChild(statement);
1974 switch(this.type)
1975 {
1976 case "statement":
1977 break;
1978 case "question":
1979 node.id = this.id;
1980 if (this.mandatory != undefined) { node.setAttribute("mandatory",this.mandatory);}
1981 if (this.boxsize != undefined) {node.setAttribute("boxsize",this.boxsize);}
1982 break;
1983 case "number":
1984 node.id = this.id;
1985 if (this.mandatory != undefined) { node.setAttribute("mandatory",this.mandatory);}
1986 if (this.min != undefined) {node.setAttribute("min", this.min);}
1987 if (this.max != undefined) {node.setAttribute("max", this.max);}
1988 break;
1989 case "checkbox":
1990 case "radio":
1991 node.id = this.id;
1992 for (var i=0; i<this.options.length; i++)
1993 {
1994 var option = this.options[i];
1995 var optionNode = doc.createElement("option");
1996 optionNode.setAttribute("name",option.name);
1997 optionNode.textContent = option.text;
1998 node.appendChild(optionNode);
1999 }
2000 break;
2001 }
2002 return node;
2003 };
2004 };
2005 this.decode = function(parent,xml) {
2006 this.location = xml.getAttribute('location');
2007 if (this.location == 'before'){this.location = 'pre';}
2008 else if (this.location == 'after'){this.location = 'post';}
2009 for (var i in xml.children)
2010 {
2011 if(isNaN(Number(i))==true){break;}
2012 var node = new this.OptionNode();
2013 node.decode(parent,xml.children[i]);
2014 this.options.push(node);
2015 }
2016 };
2017 this.encode = function(doc) {
2018 var node = doc.createElement('survey');
2019 node.setAttribute('location',this.location);
2020 for (var i=0; i<this.options.length; i++)
2021 {
2022 node.appendChild(this.options[i].exportXML(doc));
2023 }
2024 return node;
2025 };
2026 };
2027
2028 this.interfaceNode = function()
2029 {
2030 this.title = null;
2031 this.name = null;
2032 this.options = [];
2033 this.scales = [];
2034 this.schema = specification.schema.getAllElementsByName('interface')[1];
2035
2036 this.decode = function(parent,xml) {
2037 this.name = xml.getAttribute('name');
2038 var titleNode = xml.getElementsByTagName('title');
2039 if (titleNode.length == 1)
2040 {
2041 this.title = titleNode[0].textContent;
2042 }
2043 var interfaceOptionNodes = xml.getElementsByTagName('interfaceoption');
2044 // Extract interfaceoption node schema
2045 var interfaceOptionNodeSchema = this.schema.getAllElementsByName('interfaceoption')[0];
2046 var attributeMap = interfaceOptionNodeSchema.getAllElementsByTagName('xs:attribute');
2047 for (var i=0; i<interfaceOptionNodes.length; i++)
2048 {
2049 var ioNode = interfaceOptionNodes[i];
2050 var option = {};
2051 for (var j=0; j<attributeMap.length; j++)
2052 {
2053 var attributeName = attributeMap[j].getAttribute('name') || attributeMap[j].getAttribute('ref');
2054 var projectAttr = ioNode.getAttribute(attributeName);
2055 projectAttr = parent.processAttribute(projectAttr,attributeMap[j]);
2056 switch(typeof projectAttr)
2057 {
2058 case "number":
2059 case "boolean":
2060 eval('option.'+attributeName+' = '+projectAttr);
2061 break;
2062 case "string":
2063 eval('option.'+attributeName+' = "'+projectAttr+'"');
2064 break;
2065 }
2066 }
2067 this.options.push(option);
2068 }
2069
2070 // Now the scales nodes
2071 var scaleParent = xml.getElementsByTagName('scales');
2072 if (scaleParent.length == 1) {
2073 scaleParent = scaleParent[0];
2074 for (var i=0; i<scaleParent.children.length; i++) {
2075 var child = scaleParent.children[i];
2076 this.scales.push({
2077 text: child.textContent,
2078 position: Number(child.getAttribute('position'))
2079 });
2080 }
2081 }
2082 };
2083
2084 this.encode = function(doc) {
2085 var node = doc.createElement("interface");
2086 if (typeof name == "string")
2087 node.setAttribute("name",this.name);
2088 for (var option of this.options)
2089 {
2090 var child = doc.createElement("interfaceoption");
2091 child.setAttribute("type",option.type);
2092 child.setAttribute("name",option.name);
2093 node.appendChild(child);
2094 }
2095 if (this.scales.length != 0) {
2096 var scales = doc.createElement("scales");
2097 for (var scale of this.scales)
2098 {
2099 var child = doc.createElement("scalelabel");
2100 child.setAttribute("position",scale.position);
2101 child.textContent = scale.text;
2102 scales.appendChild(child);
2103 }
2104 node.appendChild(scales);
2105 }
2106 return node;
2107 };
2108 };
2109
2110 this.metricNode = function() {
2111 this.enabled = [];
2112 this.decode = function(parent, xml) {
2113 var children = xml.getElementsByTagName('metricenable');
2114 for (var i in children) {
2115 if (isNaN(Number(i)) == true){break;}
2116 this.enabled.push(children[i].textContent);
2117 }
2118 }
2119 this.encode = function(doc) {
2120 var node = doc.createElement('metric');
2121 for (var i in this.enabled)
2122 {
2123 if (isNaN(Number(i)) == true){break;}
2124 var child = doc.createElement('metricenable');
2125 child.textContent = this.enabled[i];
2126 node.appendChild(child);
2127 }
2128 return node;
2129 }
2130 }
2131
2132 this.page = function() {
2133 this.presentedId = undefined;
2134 this.id = undefined;
2135 this.hostURL = undefined;
2136 this.randomiseOrder = undefined;
2137 this.loop = undefined;
2138 this.showElementComments = undefined;
2139 this.outsideReference = null;
2140 this.loudness = null;
2141 this.preTest = null;
2142 this.postTest = null;
2143 this.interfaces = [];
2144 this.commentBoxPrefix = "Comment on track";
2145 this.audioElements = [];
2146 this.commentQuestions = [];
2147 this.schema = specification.schema.getAllElementsByName("page")[0];
2148
2149 this.decode = function(parent,xml)
2150 {
2151 var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
2152 for (var i=0; i<attributeMap.length; i++)
2153 {
2154 var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
2155 var projectAttr = xml.getAttribute(attributeName);
2156 projectAttr = parent.processAttribute(projectAttr,attributeMap[i]);
2157 switch(typeof projectAttr)
2158 {
2159 case "number":
2160 case "boolean":
2161 eval('this.'+attributeName+' = '+projectAttr);
2162 break;
2163 case "string":
2164 eval('this.'+attributeName+' = "'+projectAttr+'"');
2165 break;
2166 }
2167 }
2168
2169 // Get the Comment Box Prefix
2170 var CBP = xml.getElementsByTagName('commentboxprefix');
2171 if (CBP.length != 0) {
2172 this.commentBoxPrefix = CBP[0].textContent;
2173 }
2174
2175 // Now decode the interfaces
2176 var interfaceNode = xml.getElementsByTagName('interface');
2177 for (var i=0; i<interfaceNode.length; i++)
2178 {
2179 var node = new parent.interfaceNode();
2180 node.decode(this,interfaceNode[i],parent.schema.getAllElementsByName('interface')[1]);
2181 this.interfaces.push(node);
2182 }
2183
2184 // Now process the survey node options
2185 var survey = xml.getElementsByTagName('survey');
2186 var surveySchema = parent.schema.getAllElementsByName('survey')[0];
2187 for (var i in survey) {
2188 if (isNaN(Number(i)) == true){break;}
2189 var location = survey[i].getAttribute('location');
2190 if (location == 'pre' || location == 'before')
2191 {
2192 if (this.preTest != null){this.errors.push("Already a pre/before test survey defined! Ignoring second!!");}
2193 else {
2194 this.preTest = new parent.surveyNode();
2195 this.preTest.decode(parent,survey[i],surveySchema);
2196 }
2197 } else if (location == 'post' || location == 'after') {
2198 if (this.postTest != null){this.errors.push("Already a post/after test survey defined! Ignoring second!!");}
2199 else {
2200 this.postTest = new parent.surveyNode();
2201 this.postTest.decode(parent,survey[i],surveySchema);
2202 }
2203 }
2204 }
2205
2206 // Now process the audioelement tags
2207 var audioElements = xml.getElementsByTagName('audioelement');
2208 for (var i=0; i<audioElements.length; i++)
2209 {
2210 var node = new this.audioElementNode();
2211 node.decode(this,audioElements[i]);
2212 this.audioElements.push(node);
2213 }
2214
2215 // Now decode the commentquestions
2216 var commentQuestions = xml.getElementsByTagName('commentquestion');
2217 for (var i=0; i<commentQuestions.length; i++)
2218 {
2219 var node = new this.commentQuestionNode();
2220 node.decode(parent,commentQuestions[i]);
2221 this.commentQuestions.push(node);
2222 }
2223 };
2224
2225 this.encode = function(root)
2226 {
2227 var AHNode = root.createElement("page");
2228 // First decode the attributes
2229 var attributes = this.schema.getAllElementsByTagName('xs:attribute');
2230 for (var i=0; i<attributes.length; i++)
2231 {
2232 var name = attributes[i].getAttribute("name");
2233 if (name == undefined) {
2234 name = attributes[i].getAttribute("ref");
2235 }
2236 if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
2237 {
2238 eval("AHNode.setAttribute('"+name+"',this."+name+")");
2239 }
2240 }
2241 if(this.loudness != null) {AHNode.setAttribute("loudness",this.loudness);}
2242 // <commentboxprefix>
2243 var commentboxprefix = root.createElement("commentboxprefix");
2244 commentboxprefix.textContent = this.commentBoxPrefix;
2245 AHNode.appendChild(commentboxprefix);
2246
2247 for (var i=0; i<this.interfaces.length; i++)
2248 {
2249 AHNode.appendChild(this.interfaces[i].encode(root));
2250 }
2251
2252 for (var i=0; i<this.audioElements.length; i++) {
2253 AHNode.appendChild(this.audioElements[i].encode(root));
2254 }
2255 // Create <CommentQuestion>
2256 for (var i=0; i<this.commentQuestions.length; i++)
2257 {
2258 AHNode.appendChild(this.commentQuestions[i].encode(root));
2259 }
2260
2261 AHNode.appendChild(this.preTest.encode(root));
2262 AHNode.appendChild(this.postTest.encode(root));
2263 return AHNode;
2264 };
2265
2266 this.commentQuestionNode = function() {
2267 this.id = null;
2268 this.type = undefined;
2269 this.options = [];
2270 this.statement = undefined;
2271 this.schema = specification.schema.getAllElementsByName('commentquestion')[0];
2272 this.decode = function(parent,xml)
2273 {
2274 this.id = xml.id;
2275 this.type = xml.getAttribute('type');
2276 this.statement = xml.getElementsByTagName('statement')[0].textContent;
2277 var optNodes = xml.getElementsByTagName('option');
2278 for (var i=0; i<optNodes.length; i++)
2279 {
2280 var optNode = optNodes[i];
2281 this.options.push({
2282 name: optNode.getAttribute('name'),
2283 text: optNode.textContent
2284 });
2285 }
2286 };
2287
2288 this.encode = function(root)
2289 {
2290 var node = root.createElement("commentquestion");
2291 node.id = this.id;
2292 node.setAttribute("type",this.type);
2293 var statement = root.createElement("statement");
2294 statement.textContent = this.statement;
2295 node.appendChild(statement);
2296 for (var option of this.options)
2297 {
2298 var child = root.createElement("option");
2299 child.setAttribute("name",option.name);
2300 child.textContent = option.text;
2301 node.appendChild(child);
2302 }
2303 return node;
2304 };
2305 };
2306
2307 this.audioElementNode = function() {
2308 this.url = null;
2309 this.id = null;
2310 this.parent = null;
2311 this.type = null;
2312 this.marker = null;
2313 this.enforce = false;
2314 this.gain = 0.0;
2315 this.schema = specification.schema.getAllElementsByName('audioelement')[0];;
2316 this.parent = null;
2317 this.decode = function(parent,xml)
2318 {
2319 this.parent = parent;
2320 var attributeMap = this.schema.getAllElementsByTagName('xs:attribute');
2321 for (var i=0; i<attributeMap.length; i++)
2322 {
2323 var attributeName = attributeMap[i].getAttribute('name') || attributeMap[i].getAttribute('ref');
2324 var projectAttr = xml.getAttribute(attributeName);
2325 projectAttr = specification.processAttribute(projectAttr,attributeMap[i]);
2326 switch(typeof projectAttr)
2327 {
2328 case "number":
2329 case "boolean":
2330 eval('this.'+attributeName+' = '+projectAttr);
2331 break;
2332 case "string":
2333 eval('this.'+attributeName+' = "'+projectAttr+'"');
2334 break;
2335 }
2336 }
2337
2338 };
2339 this.encode = function(root)
2340 {
2341 var AENode = root.createElement("audioelement");
2342 var attributes = this.schema.getAllElementsByTagName('xs:attribute');
2343 for (var i=0; i<attributes.length; i++)
2344 {
2345 var name = attributes[i].getAttribute("name");
2346 if (name == undefined) {
2347 name = attributes[i].getAttribute("ref");
2348 }
2349 if(eval("this."+name+" != undefined") || attributes[i].getAttribute("use") == "required")
2350 {
2351 eval("AENode.setAttribute('"+name+"',this."+name+")");
2352 }
2353 }
2354 return AENode;
2355 };
2356 };
2357 };
2358 }
2359
2360 function Interface(specificationObject) {
2361 // This handles the bindings between the interface and the audioEngineContext;
2362 this.specification = specificationObject;
2363 this.insertPoint = document.getElementById("topLevelBody");
2364
2365 this.newPage = function(audioHolderObject,store)
2366 {
2367 audioEngineContext.newTestPage(audioHolderObject,store);
2368 interfaceContext.commentBoxes.deleteCommentBoxes();
2369 interfaceContext.deleteCommentQuestions();
2370 loadTest(audioHolderObject,store);
2371 };
2372
2373 // Bounded by interface!!
2374 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
2375 // For example, APE returns the slider position normalised in a <value> tag.
2376 this.interfaceObjects = [];
2377 this.interfaceObject = function(){};
2378
2379 this.resizeWindow = function(event)
2380 {
2381 popup.resize(event);
2382 for(var i=0; i<this.commentBoxes.length; i++)
2383 {this.commentBoxes[i].resize();}
2384 for(var i=0; i<this.commentQuestions.length; i++)
2385 {this.commentQuestions[i].resize();}
2386 try
2387 {
2388 resizeWindow(event);
2389 }
2390 catch(err)
2391 {
2392 console.log("Warning - Interface does not have Resize option");
2393 console.log(err);
2394 }
2395 };
2396
2397 this.returnNavigator = function()
2398 {
2399 var node = storage.document.createElement("navigator");
2400 var platform = storage.document.createElement("platform");
2401 platform.textContent = navigator.platform;
2402 var vendor = storage.document.createElement("vendor");
2403 vendor.textContent = navigator.vendor;
2404 var userAgent = storage.document.createElement("uagent");
2405 userAgent.textContent = navigator.userAgent;
2406 var screen = storage.document.createElement("window");
2407 screen.setAttribute('innerWidth',window.innerWidth);
2408 screen.setAttribute('innerHeight',window.innerHeight);
2409 node.appendChild(platform);
2410 node.appendChild(vendor);
2411 node.appendChild(userAgent);
2412 node.appendChild(screen);
2413 return node;
2414 };
2415
2416 this.commentBoxes = new function() {
2417 this.boxes = [];
2418 this.injectPoint = null;
2419 this.elementCommentBox = function(audioObject) {
2420 var element = audioObject.specification;
2421 this.audioObject = audioObject;
2422 this.id = audioObject.id;
2423 var audioHolderObject = audioObject.specification.parent;
2424 // Create document objects to hold the comment boxes
2425 this.trackComment = document.createElement('div');
2426 this.trackComment.className = 'comment-div';
2427 this.trackComment.id = 'comment-div-'+audioObject.id;
2428 // Create a string next to each comment asking for a comment
2429 this.trackString = document.createElement('span');
2430 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix+' '+audioObject.interfaceDOM.getPresentedId();
2431 // Create the HTML5 comment box 'textarea'
2432 this.trackCommentBox = document.createElement('textarea');
2433 this.trackCommentBox.rows = '4';
2434 this.trackCommentBox.cols = '100';
2435 this.trackCommentBox.name = 'trackComment'+audioObject.id;
2436 this.trackCommentBox.className = 'trackComment';
2437 var br = document.createElement('br');
2438 // Add to the holder.
2439 this.trackComment.appendChild(this.trackString);
2440 this.trackComment.appendChild(br);
2441 this.trackComment.appendChild(this.trackCommentBox);
2442
2443 this.exportXMLDOM = function() {
2444 var root = document.createElement('comment');
2445 var question = document.createElement('question');
2446 question.textContent = this.trackString.textContent;
2447 var response = document.createElement('response');
2448 response.textContent = this.trackCommentBox.value;
2449 console.log("Comment frag-"+this.id+": "+response.textContent);
2450 root.appendChild(question);
2451 root.appendChild(response);
2452 return root;
2453 };
2454 this.resize = function()
2455 {
2456 var boxwidth = (window.innerWidth-100)/2;
2457 if (boxwidth >= 600)
2458 {
2459 boxwidth = 600;
2460 }
2461 else if (boxwidth < 400)
2462 {
2463 boxwidth = 400;
2464 }
2465 this.trackComment.style.width = boxwidth+"px";
2466 this.trackCommentBox.style.width = boxwidth-6+"px";
2467 };
2468 this.resize();
2469 };
2470 this.createCommentBox = function(audioObject) {
2471 var node = new this.elementCommentBox(audioObject);
2472 this.boxes.push(node);
2473 audioObject.commentDOM = node;
2474 return node;
2475 };
2476 this.sortCommentBoxes = function() {
2477 this.boxes.sort(function(a,b){return a.id - b.id;});
2478 };
2479
2480 this.showCommentBoxes = function(inject, sort) {
2481 this.injectPoint = inject;
2482 if (sort) {this.sortCommentBoxes();}
2483 for (var box of this.boxes) {
2484 inject.appendChild(box.trackComment);
2485 }
2486 };
2487
2488 this.deleteCommentBoxes = function() {
2489 if (this.injectPoint != null) {
2490 for (var box of this.boxes) {
2491 this.injectPoint.removeChild(box.trackComment);
2492 }
2493 this.injectPoint = null;
2494 }
2495 this.boxes = [];
2496 };
2497 }
2498
2499 this.commentQuestions = [];
2500
2501 this.commentBox = function(commentQuestion) {
2502 this.specification = commentQuestion;
2503 // Create document objects to hold the comment boxes
2504 this.holder = document.createElement('div');
2505 this.holder.className = 'comment-div';
2506 // Create a string next to each comment asking for a comment
2507 this.string = document.createElement('span');
2508 this.string.innerHTML = commentQuestion.statement;
2509 // Create the HTML5 comment box 'textarea'
2510 this.textArea = document.createElement('textarea');
2511 this.textArea.rows = '4';
2512 this.textArea.cols = '100';
2513 this.textArea.className = 'trackComment';
2514 var br = document.createElement('br');
2515 // Add to the holder.
2516 this.holder.appendChild(this.string);
2517 this.holder.appendChild(br);
2518 this.holder.appendChild(this.textArea);
2519
2520 this.exportXMLDOM = function(storePoint) {
2521 var root = storePoint.parent.document.createElement('comment');
2522 root.id = this.specification.id;
2523 root.setAttribute('type',this.specification.type);
2524 console.log("Question: "+this.string.textContent);
2525 console.log("Response: "+root.textContent);
2526 var question = storePoint.parent.document.createElement('question');
2527 question.textContent = this.string.textContent;
2528 var response = storePoint.parent.document.createElement('response');
2529 response.textContent = this.textArea.value;
2530 root.appendChild(question);
2531 root.appendChild(response);
2532 storePoint.XMLDOM.appendChild(root);
2533 return root;
2534 };
2535 this.resize = function()
2536 {
2537 var boxwidth = (window.innerWidth-100)/2;
2538 if (boxwidth >= 600)
2539 {
2540 boxwidth = 600;
2541 }
2542 else if (boxwidth < 400)
2543 {
2544 boxwidth = 400;
2545 }
2546 this.holder.style.width = boxwidth+"px";
2547 this.textArea.style.width = boxwidth-6+"px";
2548 };
2549 this.resize();
2550 };
2551
2552 this.radioBox = function(commentQuestion) {
2553 this.specification = commentQuestion;
2554 // Create document objects to hold the comment boxes
2555 this.holder = document.createElement('div');
2556 this.holder.className = 'comment-div';
2557 // Create a string next to each comment asking for a comment
2558 this.string = document.createElement('span');
2559 this.string.innerHTML = commentQuestion.statement;
2560 var br = document.createElement('br');
2561 // Add to the holder.
2562 this.holder.appendChild(this.string);
2563 this.holder.appendChild(br);
2564 this.options = [];
2565 this.inputs = document.createElement('div');
2566 this.span = document.createElement('div');
2567 this.inputs.align = 'center';
2568 this.inputs.style.marginLeft = '12px';
2569 this.span.style.marginLeft = '12px';
2570 this.span.align = 'center';
2571 this.span.style.marginTop = '15px';
2572
2573 var optCount = commentQuestion.options.length;
2574 for (var optNode of commentQuestion.options)
2575 {
2576 var div = document.createElement('div');
2577 div.style.width = '80px';
2578 div.style.float = 'left';
2579 var input = document.createElement('input');
2580 input.type = 'radio';
2581 input.name = commentQuestion.id;
2582 input.setAttribute('setvalue',optNode.name);
2583 input.className = 'comment-radio';
2584 div.appendChild(input);
2585 this.inputs.appendChild(div);
2586
2587
2588 div = document.createElement('div');
2589 div.style.width = '80px';
2590 div.style.float = 'left';
2591 div.align = 'center';
2592 var span = document.createElement('span');
2593 span.textContent = optNode.text;
2594 span.className = 'comment-radio-span';
2595 div.appendChild(span);
2596 this.span.appendChild(div);
2597 this.options.push(input);
2598 }
2599 this.holder.appendChild(this.span);
2600 this.holder.appendChild(this.inputs);
2601
2602 this.exportXMLDOM = function(storePoint) {
2603 var root = storePoint.parent.document.createElement('comment');
2604 root.id = this.specification.id;
2605 root.setAttribute('type',this.specification.type);
2606 var question = document.createElement('question');
2607 question.textContent = this.string.textContent;
2608 var response = document.createElement('response');
2609 var i=0;
2610 while(this.options[i].checked == false) {
2611 i++;
2612 if (i >= this.options.length) {
2613 break;
2614 }
2615 }
2616 if (i >= this.options.length) {
2617 response.textContent = 'null';
2618 } else {
2619 response.textContent = this.options[i].getAttribute('setvalue');
2620 response.setAttribute('number',i);
2621 }
2622 console.log('Comment: '+question.textContent);
2623 console.log('Response: '+response.textContent);
2624 root.appendChild(question);
2625 root.appendChild(response);
2626 storePoint.XMLDOM.appendChild(root);
2627 return root;
2628 };
2629 this.resize = function()
2630 {
2631 var boxwidth = (window.innerWidth-100)/2;
2632 if (boxwidth >= 600)
2633 {
2634 boxwidth = 600;
2635 }
2636 else if (boxwidth < 400)
2637 {
2638 boxwidth = 400;
2639 }
2640 this.holder.style.width = boxwidth+"px";
2641 var text = this.holder.children[2];
2642 var options = this.holder.children[3];
2643 var optCount = options.children.length;
2644 var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px';
2645 var options = options.firstChild;
2646 var text = text.firstChild;
2647 options.style.marginRight = spanMargin;
2648 options.style.marginLeft = spanMargin;
2649 text.style.marginRight = spanMargin;
2650 text.style.marginLeft = spanMargin;
2651 while(options.nextSibling != undefined)
2652 {
2653 options = options.nextSibling;
2654 text = text.nextSibling;
2655 options.style.marginRight = spanMargin;
2656 options.style.marginLeft = spanMargin;
2657 text.style.marginRight = spanMargin;
2658 text.style.marginLeft = spanMargin;
2659 }
2660 };
2661 this.resize();
2662 };
2663
2664 this.checkboxBox = function(commentQuestion) {
2665 this.specification = commentQuestion;
2666 // Create document objects to hold the comment boxes
2667 this.holder = document.createElement('div');
2668 this.holder.className = 'comment-div';
2669 // Create a string next to each comment asking for a comment
2670 this.string = document.createElement('span');
2671 this.string.innerHTML = commentQuestion.statement;
2672 var br = document.createElement('br');
2673 // Add to the holder.
2674 this.holder.appendChild(this.string);
2675 this.holder.appendChild(br);
2676 this.options = [];
2677 this.inputs = document.createElement('div');
2678 this.span = document.createElement('div');
2679 this.inputs.align = 'center';
2680 this.inputs.style.marginLeft = '12px';
2681 this.span.style.marginLeft = '12px';
2682 this.span.align = 'center';
2683 this.span.style.marginTop = '15px';
2684
2685 var optCount = commentQuestion.options.length;
2686 for (var i=0; i<optCount; i++)
2687 {
2688 var div = document.createElement('div');
2689 div.style.width = '80px';
2690 div.style.float = 'left';
2691 var input = document.createElement('input');
2692 input.type = 'checkbox';
2693 input.name = commentQuestion.id;
2694 input.setAttribute('setvalue',commentQuestion.options[i].name);
2695 input.className = 'comment-radio';
2696 div.appendChild(input);
2697 this.inputs.appendChild(div);
2698
2699
2700 div = document.createElement('div');
2701 div.style.width = '80px';
2702 div.style.float = 'left';
2703 div.align = 'center';
2704 var span = document.createElement('span');
2705 span.textContent = commentQuestion.options[i].text;
2706 span.className = 'comment-radio-span';
2707 div.appendChild(span);
2708 this.span.appendChild(div);
2709 this.options.push(input);
2710 }
2711 this.holder.appendChild(this.span);
2712 this.holder.appendChild(this.inputs);
2713
2714 this.exportXMLDOM = function(storePoint) {
2715 var root = storePoint.parent.document.createElement('comment');
2716 root.id = this.specification.id;
2717 root.setAttribute('type',this.specification.type);
2718 var question = document.createElement('question');
2719 question.textContent = this.string.textContent;
2720 root.appendChild(question);
2721 console.log('Comment: '+question.textContent);
2722 for (var i=0; i<this.options.length; i++) {
2723 var response = document.createElement('response');
2724 response.textContent = this.options[i].checked;
2725 response.setAttribute('name',this.options[i].getAttribute('setvalue'));
2726 root.appendChild(response);
2727 console.log('Response '+response.getAttribute('name') +': '+response.textContent);
2728 }
2729 storePoint.XMLDOM.appendChild(root);
2730 return root;
2731 };
2732 this.resize = function()
2733 {
2734 var boxwidth = (window.innerWidth-100)/2;
2735 if (boxwidth >= 600)
2736 {
2737 boxwidth = 600;
2738 }
2739 else if (boxwidth < 400)
2740 {
2741 boxwidth = 400;
2742 }
2743 this.holder.style.width = boxwidth+"px";
2744 var text = this.holder.children[2];
2745 var options = this.holder.children[3];
2746 var optCount = options.children.length;
2747 var spanMargin = Math.floor(((boxwidth-20-(optCount*80))/(optCount))/2)+'px';
2748 var options = options.firstChild;
2749 var text = text.firstChild;
2750 options.style.marginRight = spanMargin;
2751 options.style.marginLeft = spanMargin;
2752 text.style.marginRight = spanMargin;
2753 text.style.marginLeft = spanMargin;
2754 while(options.nextSibling != undefined)
2755 {
2756 options = options.nextSibling;
2757 text = text.nextSibling;
2758 options.style.marginRight = spanMargin;
2759 options.style.marginLeft = spanMargin;
2760 text.style.marginRight = spanMargin;
2761 text.style.marginLeft = spanMargin;
2762 }
2763 };
2764 this.resize();
2765 };
2766
2767 this.createCommentQuestion = function(element) {
2768 var node;
2769 if (element.type == 'question') {
2770 node = new this.commentBox(element);
2771 } else if (element.type == 'radio') {
2772 node = new this.radioBox(element);
2773 } else if (element.type == 'checkbox') {
2774 node = new this.checkboxBox(element);
2775 }
2776 this.commentQuestions.push(node);
2777 return node;
2778 };
2779
2780 this.deleteCommentQuestions = function()
2781 {
2782 this.commentQuestions = [];
2783 };
2784
2785 this.playhead = new function()
2786 {
2787 this.object = document.createElement('div');
2788 this.object.className = 'playhead';
2789 this.object.align = 'left';
2790 var curTime = document.createElement('div');
2791 curTime.style.width = '50px';
2792 this.curTimeSpan = document.createElement('span');
2793 this.curTimeSpan.textContent = '00:00';
2794 curTime.appendChild(this.curTimeSpan);
2795 this.object.appendChild(curTime);
2796 this.scrubberTrack = document.createElement('div');
2797 this.scrubberTrack.className = 'playhead-scrub-track';
2798
2799 this.scrubberHead = document.createElement('div');
2800 this.scrubberHead.id = 'playhead-scrubber';
2801 this.scrubberTrack.appendChild(this.scrubberHead);
2802 this.object.appendChild(this.scrubberTrack);
2803
2804 this.timePerPixel = 0;
2805 this.maxTime = 0;
2806
2807 this.playbackObject;
2808
2809 this.setTimePerPixel = function(audioObject) {
2810 //maxTime must be in seconds
2811 this.playbackObject = audioObject;
2812 this.maxTime = audioObject.buffer.buffer.duration;
2813 var width = 490; //500 - 10, 5 each side of the tracker head
2814 this.timePerPixel = this.maxTime/490;
2815 if (this.maxTime < 60) {
2816 this.curTimeSpan.textContent = '0.00';
2817 } else {
2818 this.curTimeSpan.textContent = '00:00';
2819 }
2820 };
2821
2822 this.update = function() {
2823 // Update the playhead position, startPlay must be called
2824 if (this.timePerPixel > 0) {
2825 var time = this.playbackObject.getCurrentPosition();
2826 if (time > 0 && time < this.maxTime) {
2827 var width = 490;
2828 var pix = Math.floor(time/this.timePerPixel);
2829 this.scrubberHead.style.left = pix+'px';
2830 if (this.maxTime > 60.0) {
2831 var secs = time%60;
2832 var mins = Math.floor((time-secs)/60);
2833 secs = secs.toString();
2834 secs = secs.substr(0,2);
2835 mins = mins.toString();
2836 this.curTimeSpan.textContent = mins+':'+secs;
2837 } else {
2838 time = time.toString();
2839 this.curTimeSpan.textContent = time.substr(0,4);
2840 }
2841 } else {
2842 this.scrubberHead.style.left = '0px';
2843 if (this.maxTime < 60) {
2844 this.curTimeSpan.textContent = '0.00';
2845 } else {
2846 this.curTimeSpan.textContent = '00:00';
2847 }
2848 }
2849 }
2850 };
2851
2852 this.interval = undefined;
2853
2854 this.start = function() {
2855 if (this.playbackObject != undefined && this.interval == undefined) {
2856 if (this.maxTime < 60) {
2857 this.interval = setInterval(function(){interfaceContext.playhead.update();},10);
2858 } else {
2859 this.interval = setInterval(function(){interfaceContext.playhead.update();},100);
2860 }
2861 }
2862 };
2863 this.stop = function() {
2864 clearInterval(this.interval);
2865 this.interval = undefined;
2866 this.scrubberHead.style.left = '0px';
2867 if (this.maxTime < 60) {
2868 this.curTimeSpan.textContent = '0.00';
2869 } else {
2870 this.curTimeSpan.textContent = '00:00';
2871 }
2872 };
2873 };
2874
2875 this.volume = new function()
2876 {
2877 // An in-built volume module which can be viewed on page
2878 // Includes trackers on page-by-page data
2879 // Volume does NOT reset to 0dB on each page load
2880 this.valueLin = 1.0;
2881 this.valueDB = 0.0;
2882 this.object = document.createElement('div');
2883 this.object.id = 'master-volume-holder';
2884 this.slider = document.createElement('input');
2885 this.slider.id = 'master-volume-control';
2886 this.slider.type = 'range';
2887 this.valueText = document.createElement('span');
2888 this.valueText.id = 'master-volume-feedback';
2889 this.valueText.textContent = '0dB';
2890
2891 this.slider.min = -60;
2892 this.slider.max = 12;
2893 this.slider.value = 0;
2894 this.slider.step = 1;
2895 this.slider.onmousemove = function(event)
2896 {
2897 interfaceContext.volume.valueDB = event.currentTarget.value;
2898 interfaceContext.volume.valueLin = decibelToLinear(interfaceContext.volume.valueDB);
2899 interfaceContext.volume.valueText.textContent = interfaceContext.volume.valueDB+'dB';
2900 audioEngineContext.outputGain.gain.value = interfaceContext.volume.valueLin;
2901 }
2902 this.slider.onmouseup = function(event)
2903 {
2904 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
2905 if (storePoint.length == 0)
2906 {
2907 storePoint = storage.document.createElement('metricresult');
2908 storePoint.setAttribute('name','volumeTracker');
2909 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
2910 }
2911 else {
2912 storePoint = storePoint[0];
2913 }
2914 var node = storage.document.createElement('movement');
2915 node.setAttribute('test-time',audioEngineContext.timer.getTestTime());
2916 node.setAttribute('volume',interfaceContext.volume.valueDB);
2917 node.setAttribute('format','dBFS');
2918 storePoint.appendChild(node);
2919 }
2920
2921 var title = document.createElement('div');
2922 title.innerHTML = '<span>Master Volume Control</span>';
2923 title.style.fontSize = '0.75em';
2924 title.style.width = "100%";
2925 title.align = 'center';
2926 this.object.appendChild(title);
2927
2928 this.object.appendChild(this.slider);
2929 this.object.appendChild(this.valueText);
2930 }
2931 // Global Checkers
2932 // These functions will help enforce the checkers
2933 this.checkHiddenAnchor = function()
2934 {
2935 for (var ao of audioEngineContext.audioObjects)
2936 {
2937 if (ao.specification.type == "anchor")
2938 {
2939 if (ao.interfaceDOM.getValue() > (ao.specification.marker/100) && ao.specification.marker > 0) {
2940 // Anchor is not set below
2941 console.log('Anchor node not below marker value');
2942 alert('Please keep listening');
2943 this.storeErrorNode('Anchor node not below marker value');
2944 return false;
2945 }
2946 }
2947 }
2948 return true;
2949 };
2950
2951 this.checkHiddenReference = function()
2952 {
2953 for (var ao of audioEngineContext.audioObjects)
2954 {
2955 if (ao.specification.type == "reference")
2956 {
2957 if (ao.interfaceDOM.getValue() < (ao.specification.marker/100) && ao.specification.marker > 0) {
2958 // Anchor is not set below
2959 console.log('Reference node not above marker value');
2960 this.storeErrorNode('Reference node not above marker value');
2961 alert('Please keep listening');
2962 return false;
2963 }
2964 }
2965 }
2966 return true;
2967 };
2968
2969 this.checkFragmentsFullyPlayed = function ()
2970 {
2971 // Checks the entire file has been played back
2972 // NOTE ! This will return true IF playback is Looped!!!
2973 if (audioEngineContext.loopPlayback)
2974 {
2975 console.log("WARNING - Looped source: Cannot check fragments are fully played");
2976 return true;
2977 }
2978 var check_pass = true;
2979 var error_obj = [];
2980 for (var i = 0; i<audioEngineContext.audioObjects.length; i++)
2981 {
2982 var object = audioEngineContext.audioObjects[i];
2983 var time = object.buffer.buffer.duration;
2984 var metric = object.metric;
2985 var passed = false;
2986 for (var j=0; j<metric.listenTracker.length; j++)
2987 {
2988 var bt = metric.listenTracker[j].getElementsByTagName('buffertime');
2989 var start_time = Number(bt[0].getAttribute('start'));
2990 var stop_time = Number(bt[0].getAttribute('stop'));
2991 var delta = stop_time - start_time;
2992 if (delta >= time)
2993 {
2994 passed = true;
2995 break;
2996 }
2997 }
2998 if (passed == false)
2999 {
3000 check_pass = false;
3001 console.log("Continue listening to track-"+audioEngineContext.audioObjects.interfaceDOM.getPresentedId());
3002 error_obj.push(audioEngineContext.audioObjects.interfaceDOM.getPresentedId());
3003 }
3004 }
3005 if (check_pass == false)
3006 {
3007 var str_start = "You have not completely listened to fragments ";
3008 for (var i=0; i<error_obj.length; i++)
3009 {
3010 str_start += error_obj[i];
3011 if (i != error_obj.length-1)
3012 {
3013 str_start += ', ';
3014 }
3015 }
3016 str_start += ". Please keep listening";
3017 console.log("[ALERT]: "+str_start);
3018 this.storeErrorNode("[ALERT]: "+str_start);
3019 alert(str_start);
3020 }
3021 };
3022 this.checkAllMoved = function()
3023 {
3024 var str = "You have not moved ";
3025 var failed = [];
3026 for (var ao of audioEngineContext.audioObjects)
3027 {
3028 if(ao.metric.wasMoved == false && ao.interfaceDOM.canMove() == true)
3029 {
3030 failed.push(ao.interfaceDOM.getPresentedId());
3031 }
3032 }
3033 if (failed.length == 0)
3034 {
3035 return true;
3036 } else if (failed.length == 1)
3037 {
3038 str += 'track '+failed[0];
3039 } else {
3040 str += 'tracks ';
3041 for (var i=0; i<failed.length-1; i++)
3042 {
3043 str += failed[i]+', ';
3044 }
3045 str += 'and '+failed[i];
3046 }
3047 str +='.';
3048 alert(str);
3049 console.log(str);
3050 this.storeErrorNode(str);
3051 return false;
3052 };
3053 this.checkAllPlayed = function()
3054 {
3055 var str = "You have not played ";
3056 var failed = [];
3057 for (var ao of audioEngineContext.audioObjects)
3058 {
3059 if(ao.metric.wasListenedTo == false)
3060 {
3061 failed.push(ao.interfaceDOM.getPresentedId());
3062 }
3063 }
3064 if (failed.length == 0)
3065 {
3066 return true;
3067 } else if (failed.length == 1)
3068 {
3069 str += 'track '+failed[0];
3070 } else {
3071 str += 'tracks ';
3072 for (var i=0; i<failed.length-1; i++)
3073 {
3074 str += failed[i]+', ';
3075 }
3076 str += 'and '+failed[i];
3077 }
3078 str +='.';
3079 alert(str);
3080 console.log(str);
3081 this.storeErrorNode(str);
3082 return false;
3083 };
3084
3085 this.storeErrorNode = function(errorMessage)
3086 {
3087 var time = audioEngineContext.timer.getTestTime();
3088 var node = storage.document.createElement('error');
3089 node.setAttribute('time',time);
3090 node.textContent = errorMessage;
3091 testState.currentStore.XMLDOM.appendChild(node);
3092 };
3093 }
3094
3095 function Storage()
3096 {
3097 // Holds results in XML format until ready for collection
3098 this.globalPreTest = null;
3099 this.globalPostTest = null;
3100 this.testPages = [];
3101 this.document = document.implementation.createDocument(null,"waetresult");
3102 this.root = this.document.childNodes[0];
3103 this.state = 0;
3104
3105 this.initialise = function(sessionKey)
3106 {
3107 if (sessionKey == undefined) {
3108 // We need to get the sessionKey
3109 this.SessionKey.generateKey();
3110 var projectDocument = specification.projectXML;
3111 projectDocument.setAttribute('file-name',url);
3112 this.root.appendChild(projectDocument);
3113 this.root.appendChild(returnDateNode());
3114 this.root.appendChild(interfaceContext.returnNavigator());
3115 } else {
3116 this.SessionKey.key = sessionKey;
3117 }
3118 if (specification.preTest != undefined){this.globalPreTest = new this.surveyNode(this,this.root,specification.preTest);}
3119 if (specification.postTest != undefined){this.globalPostTest = new this.surveyNode(this,this.root,specification.postTest);}
3120 };
3121
3122 this.SessionKey = {
3123 key: null,
3124 request: new XMLHttpRequest(),
3125 parent: this,
3126 handleEvent: function() {
3127 var parse = new DOMParser();
3128 var xml = parse.parseFromString(this.request.response,"text/xml");
3129 if (xml.getAllElementsByTagName("state")[0].textContent == "OK") {
3130 this.key = xml.getAllElementsByTagName("key")[0].textContent;
3131 this.parent.root.setAttribute("key",this.key);
3132 this.parent.root.setAttribute("state","empty");
3133 } else {
3134 this.generateKey();
3135 }
3136 },
3137 generateKey: function() {
3138 var temp_key = randomString(32);
3139 this.request.open("GET","keygen.php?key="+temp_key,true);
3140 this.request.addEventListener("load",this);
3141 this.request.send();
3142 },
3143 update: function() {
3144 this.parent.root.setAttribute("state","update");
3145 var xmlhttp = new XMLHttpRequest();
3146 xmlhttp.open("POST",specification.projectReturn+"?key="+this.key);
3147 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
3148 xmlhttp.onerror = function(){
3149 console.log('Error updating file to server!');
3150 };
3151 var hold = document.createElement("div");
3152 var clone = this.parent.root.cloneNode(true);
3153 hold.appendChild(clone);
3154 xmlhttp.onload = function() {
3155 if (this.status >= 300) {
3156 console.log("WARNING - Could not update at this time");
3157 } else {
3158 var parser = new DOMParser();
3159 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
3160 var response = xmlDoc.getElementsByTagName('response')[0];
3161 if (response.getAttribute("state") == "OK") {
3162 var file = response.getElementsByTagName("file")[0];
3163 console.log("Intermediate save: OK, written "+file.getAttribute("bytes")+"B");
3164 } else {
3165 var message = response.getElementsByTagName("message");
3166 console.log("Intermediate save: Error! "+message.textContent);
3167 }
3168 }
3169 }
3170 xmlhttp.send([hold.innerHTML]);
3171 }
3172 }
3173
3174 this.createTestPageStore = function(specification)
3175 {
3176 var store = new this.pageNode(this,specification);
3177 this.testPages.push(store);
3178 return this.testPages[this.testPages.length-1];
3179 };
3180
3181 this.surveyNode = function(parent,root,specification)
3182 {
3183 this.specification = specification;
3184 this.parent = parent;
3185 this.XMLDOM = this.parent.document.createElement('survey');
3186 this.XMLDOM.setAttribute('location',this.specification.location);
3187 for (var optNode of this.specification.options)
3188 {
3189 if (optNode.type != 'statement')
3190 {
3191 var node = this.parent.document.createElement('surveyresult');
3192 node.id = optNode.id;
3193 node.setAttribute('type',optNode.type);
3194 this.XMLDOM.appendChild(node);
3195 }
3196 }
3197 root.appendChild(this.XMLDOM);
3198
3199 this.postResult = function(node)
3200 {
3201 // From popup: node is the popupOption node containing both spec. and results
3202 // ID is the position
3203 if (node.specification.type == 'statement'){return;}
3204 var surveyresult = this.parent.document.getElementById(node.specification.id);
3205 switch(node.specification.type)
3206 {
3207 case "number":
3208 case "question":
3209 var child = this.parent.document.createElement('response');
3210 child.textContent = node.response;
3211 surveyresult.appendChild(child);
3212 break;
3213 case "radio":
3214 var child = this.parent.document.createElement('response');
3215 child.setAttribute('name',node.response.name);
3216 child.textContent = node.response.text;
3217 surveyresult.appendChild(child);
3218 break;
3219 case "checkbox":
3220 for (var i=0; i<node.response.length; i++)
3221 {
3222 var checkNode = this.parent.document.createElement('response');
3223 checkNode.setAttribute('name',node.response[i].name);
3224 checkNode.setAttribute('checked',node.response[i].checked);
3225 surveyresult.appendChild(checkNode);
3226 }
3227 break;
3228 }
3229 };
3230 };
3231
3232 this.pageNode = function(parent,specification)
3233 {
3234 // Create one store per test page
3235 this.specification = specification;
3236 this.parent = parent;
3237 this.XMLDOM = this.parent.document.createElement('page');
3238 this.XMLDOM.setAttribute('id',specification.id);
3239 this.XMLDOM.setAttribute('presentedId',specification.presentedId);
3240 if (specification.preTest != undefined){this.preTest = new this.parent.surveyNode(this.parent,this.XMLDOM,this.specification.preTest);}
3241 if (specification.postTest != undefined){this.postTest = new this.parent.surveyNode(this.parent,this.XMLDOM,this.specification.postTest);}
3242
3243 // Add any page metrics
3244 var page_metric = this.parent.document.createElement('metric');
3245 this.XMLDOM.appendChild(page_metric);
3246
3247 // Add the audioelement
3248 for (var element of this.specification.audioElements)
3249 {
3250 var aeNode = this.parent.document.createElement('audioelement');
3251 aeNode.id = element.id;
3252 aeNode.setAttribute('type',element.type);
3253 aeNode.setAttribute('url', element.url);
3254 aeNode.setAttribute('gain', element.gain);
3255 if (element.type == 'anchor' || element.type == 'reference')
3256 {
3257 if (element.marker > 0)
3258 {
3259 aeNode.setAttribute('marker',element.marker);
3260 }
3261 }
3262 var ae_metric = this.parent.document.createElement('metric');
3263 aeNode.appendChild(ae_metric);
3264 this.XMLDOM.appendChild(aeNode);
3265 }
3266
3267 this.parent.root.appendChild(this.XMLDOM);
3268 };
3269 this.update = function() {
3270 this.SessionKey.update();
3271 }
3272 this.finish = function()
3273 {
3274 if (this.state == 0)
3275 {
3276 this.update();
3277 }
3278 this.state = 1;
3279 return this.root;
3280 };
3281 }