annotate core.js @ 932:ec9a867e4512

Bug #1258 Fixed. audioObject.metric.startListening now writes to console.
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Fri, 29 May 2015 16:27:05 +0100
parents
children 20823d1467b1
rev   line source
n@932 1 /**
n@932 2 * core.js
n@932 3 *
n@932 4 * Main script to run, calls all other core functions and manages loading/store to backend.
n@932 5 * Also contains all global variables.
n@932 6 */
n@932 7
n@932 8 /* create the web audio API context and store in audioContext*/
n@932 9 var audioContext; // Hold the browser web audio API
n@932 10 var projectXML; // Hold the parsed setup XML
n@932 11 var popup; // Hold the interfacePopup object
n@932 12 var testState;
n@932 13 var currentState; // Keep track of the current state (pre/post test, which test, final test? first test?)
n@932 14 //var testXMLSetups = []; // Hold the parsed test instances
n@932 15 //var testResultsHolders =[]; // Hold the results from each test for publishing to XML
n@932 16 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
n@932 17 //var currentTestHolder; // Hold any intermediate results during test - metrics
n@932 18 var audioEngineContext; // The custome AudioEngine object
n@932 19 var projectReturn; // Hold the URL for the return
n@932 20 //var preTestQuestions = document.createElement('PreTest'); // Store any pre-test question response
n@932 21 //var postTestQuestions = document.createElement('PostTest'); // Store any post-test question response
n@932 22
n@932 23 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
n@932 24 AudioBufferSourceNode.prototype.owner = undefined;
n@932 25
n@932 26 window.onload = function() {
n@932 27 // Function called once the browser has loaded all files.
n@932 28 // This should perform any initial commands such as structure / loading documents
n@932 29
n@932 30 // Create a web audio API context
n@932 31 // Fixed for cross-browser support
n@932 32 var AudioContext = window.AudioContext || window.webkitAudioContext;
n@932 33 audioContext = new AudioContext;
n@932 34
n@932 35 // Create test state
n@932 36 testState = new stateMachine();
n@932 37
n@932 38 // Create the audio engine object
n@932 39 audioEngineContext = new AudioEngine();
n@932 40
n@932 41 // Create the popup interface object
n@932 42 popup = new interfacePopup();
n@932 43 };
n@932 44
n@932 45 function interfacePopup() {
n@932 46 // Creates an object to manage the popup
n@932 47 this.popup = null;
n@932 48 this.popupContent = null;
n@932 49 this.popupButton = null;
n@932 50 this.popupOptions = null;
n@932 51 this.currentIndex = null;
n@932 52 this.responses = null;
n@932 53 this.createPopup = function(){
n@932 54 // Create popup window interface
n@932 55 var insertPoint = document.getElementById("topLevelBody");
n@932 56 var blank = document.createElement('div');
n@932 57 blank.className = 'testHalt';
n@932 58
n@932 59 this.popup = document.createElement('div');
n@932 60 this.popup.id = 'popupHolder';
n@932 61 this.popup.className = 'popupHolder';
n@932 62 this.popup.style.position = 'absolute';
n@932 63 this.popup.style.left = (window.innerWidth/2)-250 + 'px';
n@932 64 this.popup.style.top = (window.innerHeight/2)-125 + 'px';
n@932 65
n@932 66 this.popupContent = document.createElement('div');
n@932 67 this.popupContent.id = 'popupContent';
n@932 68 this.popupContent.style.marginTop = '25px';
n@932 69 this.popupContent.align = 'center';
n@932 70 this.popup.appendChild(this.popupContent);
n@932 71
n@932 72 this.popupButton = document.createElement('button');
n@932 73 this.popupButton.className = 'popupButton';
n@932 74 this.popupButton.innerHTML = 'Next';
n@932 75 this.popupButton.onclick = function(){popup.buttonClicked();};
n@932 76 insertPoint.appendChild(this.popup);
n@932 77 insertPoint.appendChild(blank);
n@932 78 };
n@932 79
n@932 80 this.showPopup = function(){
n@932 81 if (this.popup == null || this.popup == undefined) {
n@932 82 this.createPopup();
n@932 83 }
n@932 84 this.popup.style.zIndex = 3;
n@932 85 this.popup.style.visibility = 'visible';
n@932 86 var blank = document.getElementsByClassName('testHalt')[0];
n@932 87 blank.style.zIndex = 2;
n@932 88 blank.style.visibility = 'visible';
n@932 89 };
n@932 90
n@932 91 this.hidePopup = function(){
n@932 92 this.popup.style.zIndex = -1;
n@932 93 this.popup.style.visibility = 'hidden';
n@932 94 var blank = document.getElementsByClassName('testHalt')[0];
n@932 95 blank.style.zIndex = -2;
n@932 96 blank.style.visibility = 'hidden';
n@932 97 };
n@932 98
n@932 99 this.postNode = function() {
n@932 100 // This will take the node from the popupOptions and display it
n@932 101 var node = this.popupOptions[this.currentIndex];
n@932 102 this.popupContent.innerHTML = null;
n@932 103 if (node.nodeName == 'statement') {
n@932 104 var span = document.createElement('span');
n@932 105 span.textContent = node.textContent;
n@932 106 this.popupContent.appendChild(span);
n@932 107 } else if (node.nodeName == 'question') {
n@932 108 var span = document.createElement('span');
n@932 109 span.textContent = node.textContent;
n@932 110 var textArea = document.createElement('textarea');
n@932 111 var br = document.createElement('br');
n@932 112 this.popupContent.appendChild(span);
n@932 113 this.popupContent.appendChild(br);
n@932 114 this.popupContent.appendChild(textArea);
n@932 115 }
n@932 116 this.popupContent.appendChild(this.popupButton);
n@932 117 }
n@932 118
n@932 119 this.initState = function(node) {
n@932 120 //Call this with your preTest and postTest nodes when needed to
n@932 121 // initialise the popup procedure.
n@932 122 this.popupOptions = $(node).children();
n@932 123 if (this.popupOptions.length > 0) {
n@932 124 if (node.nodeName == 'preTest' || node.nodeName == 'PreTest') {
n@932 125 this.responses = document.createElement('PreTest');
n@932 126 } else if (node.nodeName == 'postTest' || node.nodeName == 'PostTest') {
n@932 127 this.responses = document.createElement('PostTest');
n@932 128 } else {
n@932 129 console.log ('WARNING - popup node neither pre or post!');
n@932 130 this.responses = document.createElement('responses');
n@932 131 }
n@932 132 this.currentIndex = 0;
n@932 133 this.showPopup();
n@932 134 this.postNode();
n@932 135 }
n@932 136 }
n@932 137
n@932 138 this.buttonClicked = function() {
n@932 139 // Each time the popup button is clicked!
n@932 140 var node = this.popupOptions[this.currentIndex];
n@932 141 if (node.nodeName == 'question') {
n@932 142 // Must extract the question data
n@932 143 var mandatory = node.attributes['mandatory'];
n@932 144 if (mandatory == undefined) {
n@932 145 mandatory = false;
n@932 146 } else {
n@932 147 if (mandatory.value == 'true'){mandatory = true;}
n@932 148 else {mandatory = false;}
n@932 149 }
n@932 150 var textArea = $(popup.popupContent).find('textarea')[0];
n@932 151 if (mandatory == true && textArea.value.length == 0) {
n@932 152 alert('This question is mandatory');
n@932 153 return;
n@932 154 } else {
n@932 155 // Save the text content
n@932 156 var hold = document.createElement('comment');
n@932 157 hold.id = node.attributes['id'].value;
n@932 158 hold.innerHTML = textArea.value;
n@932 159 console.log("Question: "+ node.textContent);
n@932 160 console.log("Question Response: "+ textArea.value);
n@932 161 this.responses.appendChild(hold);
n@932 162 }
n@932 163 }
n@932 164 this.currentIndex++;
n@932 165 if (this.currentIndex < this.popupOptions.length) {
n@932 166 this.postNode();
n@932 167 } else {
n@932 168 // Reached the end of the popupOptions
n@932 169 this.hidePopup();
n@932 170 if (this.responses.nodeName == testState.stateResults[testState.stateIndex].nodeName) {
n@932 171 testState.stateResults[testState.stateIndex] = this.responses;
n@932 172 } else {
n@932 173 testState.stateResults[testState.stateIndex].appendChild(this.responses);
n@932 174 }
n@932 175 advanceState();
n@932 176 }
n@932 177 }
n@932 178 }
n@932 179
n@932 180 function advanceState()
n@932 181 {
n@932 182 // Just for complete clarity
n@932 183 testState.advanceState();
n@932 184 }
n@932 185
n@932 186 function stateMachine()
n@932 187 {
n@932 188 // Object prototype for tracking and managing the test state
n@932 189 this.stateMap = [];
n@932 190 this.stateIndex = null;
n@932 191 this.currentStateMap = [];
n@932 192 this.currentIndex = null;
n@932 193 this.currentTestId = 0;
n@932 194 this.stateResults = [];
n@932 195 this.timerCallBackHolders = null;
n@932 196 this.initialise = function(){
n@932 197 if (this.stateMap.length > 0) {
n@932 198 if(this.stateIndex != null) {
n@932 199 console.log('NOTE - State already initialise');
n@932 200 }
n@932 201 this.stateIndex = -1;
n@932 202 var that = this;
n@932 203 for (var id=0; id<this.stateMap.length; id++){
n@932 204 var name = this.stateMap[id].nodeName;
n@932 205 var obj = document.createElement(name);
n@932 206 this.stateResults.push(obj);
n@932 207 }
n@932 208 } else {
n@932 209 conolse.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
n@932 210 }
n@932 211 };
n@932 212 this.advanceState = function(){
n@932 213 if (this.stateIndex == null) {
n@932 214 this.initialise();
n@932 215 }
n@932 216 if (this.stateIndex == -1) {
n@932 217 console.log('Starting test...');
n@932 218 }
n@932 219 if (this.currentIndex == null){
n@932 220 if (this.currentStateMap.nodeName == "audioHolder") {
n@932 221 // Save current page
n@932 222 this.testPageCompleted(this.stateResults[this.stateIndex],this.currentStateMap,this.currentTestId);
n@932 223 this.currentTestId++;
n@932 224 }
n@932 225 this.stateIndex++;
n@932 226 if (this.stateIndex >= this.stateMap.length) {
n@932 227 console.log('Test Completed');
n@932 228 createProjectSave(projectReturn);
n@932 229 } else {
n@932 230 this.currentStateMap = this.stateMap[this.stateIndex];
n@932 231 if (this.currentStateMap.nodeName == "audioHolder") {
n@932 232 console.log('Loading test page');
n@932 233 loadTest(this.currentStateMap);
n@932 234 this.initialiseInnerState(this.currentStateMap);
n@932 235 } else if (this.currentStateMap.nodeName == "PreTest" || this.currentStateMap.nodeName == "PostTest") {
n@932 236 if (this.currentStateMap.childElementCount >= 1) {
n@932 237 popup.initState(this.currentStateMap);
n@932 238 } else {
n@932 239 this.advanceState();
n@932 240 }
n@932 241 } else {
n@932 242 this.advanceState();
n@932 243 }
n@932 244 }
n@932 245 } else {
n@932 246 this.advanceInnerState();
n@932 247 }
n@932 248 };
n@932 249
n@932 250 this.testPageCompleted = function(store, testXML, testId) {
n@932 251 // Function called each time a test page has been completed
n@932 252 // Can be used to over-rule default behaviour
n@932 253
n@932 254 pageXMLSave(store, testXML, testId);
n@932 255 }
n@932 256
n@932 257 this.initialiseInnerState = function(testXML) {
n@932 258 // Parses the received testXML for pre and post test options
n@932 259 this.currentStateMap = [];
n@932 260 var preTest = $(testXML).find('PreTest')[0];
n@932 261 var postTest = $(testXML).find('PostTest')[0];
n@932 262 if (preTest == undefined) {preTest = document.createElement("preTest");}
n@932 263 if (postTest == undefined){postTest= document.createElement("postTest");}
n@932 264 this.currentStateMap.push(preTest);
n@932 265 this.currentStateMap.push(testXML);
n@932 266 this.currentStateMap.push(postTest);
n@932 267 this.currentIndex = -1;
n@932 268 this.advanceInnerState();
n@932 269 }
n@932 270
n@932 271 this.advanceInnerState = function() {
n@932 272 this.currentIndex++;
n@932 273 if (this.currentIndex >= this.currentStateMap.length) {
n@932 274 this.currentIndex = null;
n@932 275 this.currentStateMap = this.stateMap[this.stateIndex];
n@932 276 this.advanceState();
n@932 277 } else {
n@932 278 if (this.currentStateMap[this.currentIndex].nodeName == "audioHolder") {
n@932 279 console.log("Loading test page"+this.currentTestId);
n@932 280 } else if (this.currentStateMap[this.currentIndex].nodeName == "PreTest") {
n@932 281 popup.initState(this.currentStateMap[this.currentIndex]);
n@932 282 } else if (this.currentStateMap[this.currentIndex].nodeName == "PostTest") {
n@932 283 popup.initState(this.currentStateMap[this.currentIndex]);
n@932 284 } else {
n@932 285 this.advanceInnerState();
n@932 286 }
n@932 287 }
n@932 288 }
n@932 289
n@932 290 this.previousState = function(){};
n@932 291 }
n@932 292
n@932 293 function testEnded(testId)
n@932 294 {
n@932 295 pageXMLSave(testId);
n@932 296 if (testXMLSetups.length-1 > testId)
n@932 297 {
n@932 298 // Yes we have another test to perform
n@932 299 testId = (Number(testId)+1);
n@932 300 currentState = 'testRun-'+testId;
n@932 301 loadTest(testId);
n@932 302 } else {
n@932 303 console.log('Testing Completed!');
n@932 304 currentState = 'postTest';
n@932 305 // Check for any post tests
n@932 306 var xmlSetup = projectXML.find('setup');
n@932 307 var postTest = xmlSetup.find('PostTest')[0];
n@932 308 popup.initState(postTest);
n@932 309 }
n@932 310 }
n@932 311
n@932 312 function loadProjectSpec(url) {
n@932 313 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
n@932 314 // If url is null, request client to upload project XML document
n@932 315 var r = new XMLHttpRequest();
n@932 316 r.open('GET',url,true);
n@932 317 r.onload = function() {
n@932 318 loadProjectSpecCallback(r.response);
n@932 319 };
n@932 320 r.send();
n@932 321 };
n@932 322
n@932 323 function loadProjectSpecCallback(response) {
n@932 324 // Function called after asynchronous download of XML project specification
n@932 325 var decode = $.parseXML(response);
n@932 326 projectXML = $(decode);
n@932 327
n@932 328 // Now extract the setup tag
n@932 329 var xmlSetup = projectXML.find('setup');
n@932 330 // Detect the interface to use and load the relevant javascripts.
n@932 331 var interfaceType = xmlSetup[0].attributes['interface'];
n@932 332 var interfaceJS = document.createElement('script');
n@932 333 interfaceJS.setAttribute("type","text/javascript");
n@932 334 if (interfaceType.value == 'APE') {
n@932 335 interfaceJS.setAttribute("src","ape.js");
n@932 336
n@932 337 // APE comes with a css file
n@932 338 var css = document.createElement('link');
n@932 339 css.rel = 'stylesheet';
n@932 340 css.type = 'text/css';
n@932 341 css.href = 'ape.css';
n@932 342
n@932 343 document.getElementsByTagName("head")[0].appendChild(css);
n@932 344 }
n@932 345 document.getElementsByTagName("head")[0].appendChild(interfaceJS);
n@932 346
n@932 347 // Define window callbacks for interface
n@932 348 window.onresize = function(event){resizeWindow(event);};
n@932 349 }
n@932 350
n@932 351 function createProjectSave(destURL) {
n@932 352 // Save the data from interface into XML and send to destURL
n@932 353 // If destURL is null then download XML in client
n@932 354 // Now time to render file locally
n@932 355 var xmlDoc = interfaceXMLSave();
n@932 356 var parent = document.createElement("div");
n@932 357 parent.appendChild(xmlDoc);
n@932 358 var file = [parent.innerHTML];
n@932 359 if (destURL == "null" || destURL == undefined) {
n@932 360 var bb = new Blob(file,{type : 'application/xml'});
n@932 361 var dnlk = window.URL.createObjectURL(bb);
n@932 362 var a = document.createElement("a");
n@932 363 a.hidden = '';
n@932 364 a.href = dnlk;
n@932 365 a.download = "save.xml";
n@932 366 a.textContent = "Save File";
n@932 367
n@932 368 var submitDiv = document.getElementById('download-point');
n@932 369 submitDiv.appendChild(a);
n@932 370 popup.showPopup();
n@932 371 popup.popupContent.innerHTML = null;
n@932 372 popup.popupContent.appendChild(submitDiv)
n@932 373 } else {
n@932 374 var xmlhttp = new XMLHttpRequest;
n@932 375 xmlhttp.open("POST",destURL,true);
n@932 376 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
n@932 377 xmlhttp.onerror = function(){
n@932 378 console.log('Error saving file to server! Presenting download locally');
n@932 379 createProjectSave(null);
n@932 380 };
n@932 381 xmlhttp.send(file);
n@932 382 }
n@932 383 return submitDiv;
n@932 384 }
n@932 385
n@932 386 // Only other global function which must be defined in the interface class. Determines how to create the XML document.
n@932 387 function interfaceXMLSave(){
n@932 388 // Create the XML string to be exported with results
n@932 389 var xmlDoc = document.createElement("BrowserEvaluationResult");
n@932 390 xmlDoc.appendChild(returnDateNode());
n@932 391 for (var i=0; i<testState.stateResults.length; i++)
n@932 392 {
n@932 393 xmlDoc.appendChild(testState.stateResults[i]);
n@932 394 }
n@932 395
n@932 396 return xmlDoc;
n@932 397 }
n@932 398
n@932 399 function AudioEngine() {
n@932 400
n@932 401 // Create two output paths, the main outputGain and fooGain.
n@932 402 // Output gain is default to 1 and any items for playback route here
n@932 403 // Foo gain is used for analysis to ensure paths get processed, but are not heard
n@932 404 // because web audio will optimise and any route which does not go to the destination gets ignored.
n@932 405 this.outputGain = audioContext.createGain();
n@932 406 this.fooGain = audioContext.createGain();
n@932 407 this.fooGain.gain = 0;
n@932 408
n@932 409 // Use this to detect playback state: 0 - stopped, 1 - playing
n@932 410 this.status = 0;
n@932 411 this.audioObjectsReady = false;
n@932 412
n@932 413 // Connect both gains to output
n@932 414 this.outputGain.connect(audioContext.destination);
n@932 415 this.fooGain.connect(audioContext.destination);
n@932 416
n@932 417 // Create the timer Object
n@932 418 this.timer = new timer();
n@932 419 // Create session metrics
n@932 420 this.metric = new sessionMetrics(this);
n@932 421
n@932 422 this.loopPlayback = false;
n@932 423
n@932 424 // Create store for new audioObjects
n@932 425 this.audioObjects = [];
n@932 426
n@932 427 this.play = function() {
n@932 428 // Start the timer and set the audioEngine state to playing (1)
n@932 429 if (this.status == 0) {
n@932 430 // Check if all audioObjects are ready
n@932 431 if (this.audioObjectsReady == false) {
n@932 432 this.audioObjectsReady = this.checkAllReady();
n@932 433 }
n@932 434 if (this.audioObjectsReady == true) {
n@932 435 this.timer.startTest();
n@932 436 if (this.loopPlayback) {
n@932 437 for(var i=0; i<this.audioObjects.length; i++) {
n@932 438 this.audioObjects[i].play(this.timer.getTestTime()+1);
n@932 439 }
n@932 440 }
n@932 441 this.status = 1;
n@932 442 }
n@932 443 }
n@932 444 };
n@932 445
n@932 446 this.stop = function() {
n@932 447 // Send stop and reset command to all playback buffers and set audioEngine state to stopped (1)
n@932 448 if (this.status == 1) {
n@932 449 for (var i=0; i<this.audioObjects.length; i++)
n@932 450 {
n@932 451 this.audioObjects[i].stop();
n@932 452 }
n@932 453 this.status = 0;
n@932 454 }
n@932 455 };
n@932 456
n@932 457
n@932 458 this.newTrack = function(url) {
n@932 459 // Pull data from given URL into new audio buffer
n@932 460 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
n@932 461
n@932 462 // Create the audioObject with ID of the new track length;
n@932 463 audioObjectId = this.audioObjects.length;
n@932 464 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
n@932 465
n@932 466 // AudioObject will get track itself.
n@932 467 this.audioObjects[audioObjectId].constructTrack(url);
n@932 468 };
n@932 469
n@932 470 this.newTestPage = function() {
n@932 471 this.state = 0;
n@932 472 this.audioObjectsReady = false;
n@932 473 this.metric.reset();
n@932 474 this.audioObjects = [];
n@932 475 };
n@932 476
n@932 477 this.checkAllPlayed = function() {
n@932 478 arr = [];
n@932 479 for (var id=0; id<this.audioObjects.length; id++) {
n@932 480 if (this.audioObjects[id].played == false) {
n@932 481 arr.push(this.audioObjects[id].id);
n@932 482 }
n@932 483 }
n@932 484 return arr;
n@932 485 };
n@932 486
n@932 487 this.checkAllReady = function() {
n@932 488 var ready = true;
n@932 489 for (var i=0; i<this.audioObjects.length; i++) {
n@932 490 if (this.audioObjects[i].state == 0) {
n@932 491 // Track not ready
n@932 492 console.log('WAIT -- audioObject '+i+' not ready yet!');
n@932 493 ready = false;
n@932 494 };
n@932 495 }
n@932 496 if (ready == false) {
n@932 497 var holder = document.getElementById('testWaitIndicator');
n@932 498 holder.style.visibility = "visible";
n@932 499 setInterval(function() {
n@932 500 document.getElementById('testWaitIndicator').style.visibility = "hidden";
n@932 501 }, 10000);
n@932 502 }
n@932 503 return ready;
n@932 504 };
n@932 505
n@932 506 }
n@932 507
n@932 508 function audioObject(id) {
n@932 509 // The main buffer object with common control nodes to the AudioEngine
n@932 510
n@932 511 this.id = id;
n@932 512 this.state = 0; // 0 - no data, 1 - ready
n@932 513 this.url = null; // Hold the URL given for the output back to the results.
n@932 514 this.metric = new metricTracker(this);
n@932 515
n@932 516 this.played = false;
n@932 517
n@932 518 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
n@932 519 this.bufferNode = undefined;
n@932 520 this.outputGain = audioContext.createGain();
n@932 521
n@932 522 // Default output gain to be zero
n@932 523 this.outputGain.gain.value = 0.0;
n@932 524
n@932 525 // Connect buffer to the audio graph
n@932 526 this.outputGain.connect(audioEngineContext.outputGain);
n@932 527
n@932 528 // the audiobuffer is not designed for multi-start playback
n@932 529 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
n@932 530 this.buffer;
n@932 531
n@932 532 this.flagAsPlayed = function() { // to be called explicitly when not in loop mode
n@932 533 this.played = true;
n@932 534 }
n@932 535
n@932 536 this.loopStart = function() {
n@932 537 this.outputGain.gain.value = 1.0;
n@932 538 this.metric.startListening(audioEngineContext.timer.getTestTime());
n@932 539 this.played = true;
n@932 540 }
n@932 541
n@932 542 this.loopStop = function() {
n@932 543 if (this.outputGain.gain.value != 0.0) {
n@932 544 this.outputGain.gain.value = 0.0;
n@932 545 this.metric.stopListening(audioEngineContext.timer.getTestTime());
n@932 546 }
n@932 547 }
n@932 548
n@932 549 this.play = function(startTime) {
n@932 550 this.bufferNode = audioContext.createBufferSource();
n@932 551 this.bufferNode.owner = this;
n@932 552 this.bufferNode.connect(this.outputGain);
n@932 553 this.bufferNode.buffer = this.buffer;
n@932 554 this.bufferNode.loop = audioEngineContext.loopPlayback;
n@932 555 this.bufferNode.onended = function() {
n@932 556 this.owner.metric.stopListening(audioEngineContext.timer.getTestTime());
n@932 557 };
n@932 558 if (this.bufferNode.loop == false) {
n@932 559 this.metric.startListening(audioEngineContext.timer.getTestTime());
n@932 560 }
n@932 561 this.bufferNode.start(startTime);
n@932 562 };
n@932 563
n@932 564 this.stop = function() {
n@932 565 if (this.bufferNode != undefined)
n@932 566 {
n@932 567 this.bufferNode.stop(0);
n@932 568 this.bufferNode = undefined;
n@932 569 this.metric.stopListening(audioEngineContext.timer.getTestTime());
n@932 570 }
n@932 571 };
n@932 572
n@932 573 this.constructTrack = function(url) {
n@932 574 var request = new XMLHttpRequest();
n@932 575 this.url = url;
n@932 576 request.open('GET',url,true);
n@932 577 request.responseType = 'arraybuffer';
n@932 578
n@932 579 var audioObj = this;
n@932 580
n@932 581 // Create callback to decode the data asynchronously
n@932 582 request.onloadend = function() {
n@932 583 audioContext.decodeAudioData(request.response, function(decodedData) {
n@932 584 audioObj.buffer = decodedData;
n@932 585 audioObj.state = 1;
n@932 586 }, function(){
n@932 587 // Should only be called if there was an error, but sometimes gets called continuously
n@932 588 // Check here if the error is genuine
n@932 589 if (audioObj.state == 0 || audioObj.buffer == undefined) {
n@932 590 // Genuine error
n@932 591 console.log('FATAL - Error loading buffer on '+audioObj.id);
n@932 592 }
n@932 593 });
n@932 594 };
n@932 595 request.send();
n@932 596 };
n@932 597
n@932 598 }
n@932 599
n@932 600 function timer()
n@932 601 {
n@932 602 /* Timer object used in audioEngine to keep track of session timings
n@932 603 * Uses the timer of the web audio API, so sample resolution
n@932 604 */
n@932 605 this.testStarted = false;
n@932 606 this.testStartTime = 0;
n@932 607 this.testDuration = 0;
n@932 608 this.minimumTestTime = 0; // No minimum test time
n@932 609 this.startTest = function()
n@932 610 {
n@932 611 if (this.testStarted == false)
n@932 612 {
n@932 613 this.testStartTime = audioContext.currentTime;
n@932 614 this.testStarted = true;
n@932 615 this.updateTestTime();
n@932 616 audioEngineContext.metric.initialiseTest();
n@932 617 }
n@932 618 };
n@932 619 this.stopTest = function()
n@932 620 {
n@932 621 if (this.testStarted)
n@932 622 {
n@932 623 this.testDuration = this.getTestTime();
n@932 624 this.testStarted = false;
n@932 625 } else {
n@932 626 console.log('ERR: Test tried to end before beginning');
n@932 627 }
n@932 628 };
n@932 629 this.updateTestTime = function()
n@932 630 {
n@932 631 if (this.testStarted)
n@932 632 {
n@932 633 this.testDuration = audioContext.currentTime - this.testStartTime;
n@932 634 }
n@932 635 };
n@932 636 this.getTestTime = function()
n@932 637 {
n@932 638 this.updateTestTime();
n@932 639 return this.testDuration;
n@932 640 };
n@932 641 }
n@932 642
n@932 643 function sessionMetrics(engine)
n@932 644 {
n@932 645 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
n@932 646 */
n@932 647 this.engine = engine;
n@932 648 this.lastClicked = -1;
n@932 649 this.data = -1;
n@932 650 this.reset = function() {
n@932 651 this.lastClicked = -1;
n@932 652 this.data = -1;
n@932 653 };
n@932 654 this.initialiseTest = function(){};
n@932 655 }
n@932 656
n@932 657 function metricTracker(caller)
n@932 658 {
n@932 659 /* Custom object to track and collect metric data
n@932 660 * Used only inside the audioObjects object.
n@932 661 */
n@932 662
n@932 663 this.listenedTimer = 0;
n@932 664 this.listenStart = 0;
n@932 665 this.listenHold = false;
n@932 666 this.initialPosition = -1;
n@932 667 this.movementTracker = [];
n@932 668 this.wasListenedTo = false;
n@932 669 this.wasMoved = false;
n@932 670 this.hasComments = false;
n@932 671 this.parent = caller;
n@932 672
n@932 673 this.initialised = function(position)
n@932 674 {
n@932 675 if (this.initialPosition == -1) {
n@932 676 this.initialPosition = position;
n@932 677 }
n@932 678 };
n@932 679
n@932 680 this.moved = function(time,position)
n@932 681 {
n@932 682 this.wasMoved = true;
n@932 683 this.movementTracker[this.movementTracker.length] = [time, position];
n@932 684 };
n@932 685
n@932 686 this.startListening = function(time)
n@932 687 {
n@932 688 if (this.listenHold == false)
n@932 689 {
n@932 690 this.wasListenedTo = true;
n@932 691 this.listenStart = time;
n@932 692 this.listenHold = true;
n@932 693 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
n@932 694 }
n@932 695 };
n@932 696
n@932 697 this.stopListening = function(time)
n@932 698 {
n@932 699 if (this.listenHold == true)
n@932 700 {
n@932 701 this.listenedTimer += (time - this.listenStart);
n@932 702 this.listenStart = 0;
n@932 703 this.listenHold = false;
n@932 704 }
n@932 705 };
n@932 706 }
n@932 707
n@932 708 function randomiseOrder(input)
n@932 709 {
n@932 710 // This takes an array of information and randomises the order
n@932 711 var N = input.length;
n@932 712 var K = N;
n@932 713 var holdArr = [];
n@932 714 for (var n=0; n<N; n++)
n@932 715 {
n@932 716 // First pick a random number
n@932 717 var r = Math.random();
n@932 718 // Multiply and floor by the number of elements left
n@932 719 r = Math.floor(r*input.length);
n@932 720 // Pick out that element and delete from the array
n@932 721 holdArr.push(input.splice(r,1)[0]);
n@932 722 }
n@932 723 return holdArr;
n@932 724 }
n@932 725
n@932 726 function returnDateNode()
n@932 727 {
n@932 728 // Create an XML Node for the Date and Time a test was conducted
n@932 729 // Structure is
n@932 730 // <datetime>
n@932 731 // <date year="##" month="##" day="##">DD/MM/YY</date>
n@932 732 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
n@932 733 // </datetime>
n@932 734 var dateTime = new Date();
n@932 735 var year = document.createAttribute('year');
n@932 736 var month = document.createAttribute('month');
n@932 737 var day = document.createAttribute('day');
n@932 738 var hour = document.createAttribute('hour');
n@932 739 var minute = document.createAttribute('minute');
n@932 740 var secs = document.createAttribute('secs');
n@932 741
n@932 742 year.nodeValue = dateTime.getFullYear();
n@932 743 month.nodeValue = dateTime.getMonth()+1;
n@932 744 day.nodeValue = dateTime.getDate();
n@932 745 hour.nodeValue = dateTime.getHours();
n@932 746 minute.nodeValue = dateTime.getMinutes();
n@932 747 secs.nodeValue = dateTime.getSeconds();
n@932 748
n@932 749 var hold = document.createElement("datetime");
n@932 750 var date = document.createElement("date");
n@932 751 date.textContent = year.nodeValue+'/'+month.nodeValue+'/'+day.nodeValue;
n@932 752 var time = document.createElement("time");
n@932 753 time.textContent = hour.nodeValue+':'+minute.nodeValue+':'+secs.nodeValue;
n@932 754
n@932 755 date.setAttributeNode(year);
n@932 756 date.setAttributeNode(month);
n@932 757 date.setAttributeNode(day);
n@932 758 time.setAttributeNode(hour);
n@932 759 time.setAttributeNode(minute);
n@932 760 time.setAttributeNode(secs);
n@932 761
n@932 762 hold.appendChild(date);
n@932 763 hold.appendChild(time);
n@932 764 return hold
n@932 765
n@932 766 }
n@932 767
n@932 768 function testWaitIndicator() {
n@932 769 var hold = document.createElement("div");
n@932 770 hold.id = "testWaitIndicator";
n@932 771 hold.style.position = "absolute";
n@932 772 hold.style.left = "100px";
n@932 773 hold.style.top = "10px";
n@932 774 hold.style.width = "500px";
n@932 775 hold.style.height = "100px";
n@932 776 hold.style.padding = "20px";
n@932 777 hold.style.backgroundColor = "rgb(100,200,200)";
n@932 778 hold.style.visibility = "hidden";
n@932 779 var span = document.createElement("span");
n@932 780 span.textContent = "Please wait! Elements still loading";
n@932 781 hold.appendChild(span);
n@932 782 var body = document.getElementsByTagName('body')[0];
n@932 783 body.appendChild(hold);
n@932 784 }
n@932 785
n@932 786 var hidetestwait = null;