annotate core.js @ 650:ce3d4d6d01b8 Dev_main

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