annotate core.js @ 964:9c09cb530ec1

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