comparison core.js @ 1316:279930a008ca

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