annotate js/core.js @ 2530:6461c2012340

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