comparison js/core.js @ 2224:760719986df3

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