annotate core.js @ 496:cb348f6208b2 Dev_main

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