annotate core.js @ 969:917676cbdcc8

Fixed bug where 'all played' wasn't checked anymore in loop mode. Now in non-loop mode, 'flagAsPlayed' needs to be called explicitly from ape.js (may want to change)
author Brecht De Man <BrechtDeMan@users.noreply.github.com>
date Wed, 27 May 2015 20:24:17 +0100
parents 16852cf211eb
children dea30ed2b549
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();
nicholas@966 435 if (this.loopPlayback) {
nicholas@966 436 for(var i=0; i<this.audioObjects.length; i++) {
nicholas@966 437 this.audioObjects[i].play(this.timer.getTestTime()+1);
nicholas@966 438 }
nicholas@966 439 }
n@950 440 this.status = 1;
n@950 441 }
n@950 442 }
n@950 443 };
BrechtDeMan@938 444
n@950 445 this.stop = function() {
n@950 446 // Send stop and reset command to all playback buffers and set audioEngine state to stopped (1)
n@950 447 if (this.status == 1) {
n@950 448 for (var i=0; i<this.audioObjects.length; i++)
n@950 449 {
n@950 450 this.audioObjects[i].stop();
n@950 451 }
n@950 452 this.status = 0;
n@950 453 }
n@950 454 };
BrechtDeMan@938 455
BrechtDeMan@938 456
BrechtDeMan@938 457 this.newTrack = function(url) {
BrechtDeMan@938 458 // Pull data from given URL into new audio buffer
BrechtDeMan@938 459 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
BrechtDeMan@938 460
BrechtDeMan@938 461 // Create the audioObject with ID of the new track length;
BrechtDeMan@938 462 audioObjectId = this.audioObjects.length;
BrechtDeMan@938 463 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
BrechtDeMan@938 464
BrechtDeMan@938 465 // AudioObject will get track itself.
BrechtDeMan@938 466 this.audioObjects[audioObjectId].constructTrack(url);
BrechtDeMan@938 467 };
BrechtDeMan@938 468
n@950 469 this.newTestPage = function() {
n@950 470 this.state = 0;
n@950 471 this.audioObjectsReady = false;
n@950 472 this.metric.reset();
n@950 473 this.audioObjects = [];
n@950 474 };
n@950 475
nicholas@944 476 this.checkAllPlayed = function() {
nicholas@944 477 arr = [];
nicholas@944 478 for (var id=0; id<this.audioObjects.length; id++) {
nicholas@944 479 if (this.audioObjects[id].played == false) {
nicholas@944 480 arr.push(this.audioObjects[id].id);
nicholas@944 481 }
nicholas@944 482 }
nicholas@944 483 return arr;
nicholas@944 484 };
nicholas@944 485
n@950 486 this.checkAllReady = function() {
n@950 487 var ready = true;
n@950 488 for (var i=0; i<this.audioObjects.length; i++) {
n@950 489 if (this.audioObjects[i].state == 0) {
n@950 490 // Track not ready
n@950 491 console.log('WAIT -- audioObject '+i+' not ready yet!');
n@950 492 ready = false;
n@950 493 };
n@950 494 }
n@950 495 return ready;
n@950 496 };
n@950 497
BrechtDeMan@938 498 }
BrechtDeMan@938 499
BrechtDeMan@938 500 function audioObject(id) {
BrechtDeMan@938 501 // The main buffer object with common control nodes to the AudioEngine
BrechtDeMan@938 502
BrechtDeMan@938 503 this.id = id;
BrechtDeMan@938 504 this.state = 0; // 0 - no data, 1 - ready
BrechtDeMan@938 505 this.url = null; // Hold the URL given for the output back to the results.
BrechtDeMan@938 506 this.metric = new metricTracker();
BrechtDeMan@938 507
nicholas@944 508 this.played = false;
nicholas@944 509
BrechtDeMan@938 510 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
BrechtDeMan@938 511 this.bufferNode = undefined;
BrechtDeMan@938 512 this.outputGain = audioContext.createGain();
BrechtDeMan@938 513
BrechtDeMan@938 514 // Default output gain to be zero
BrechtDeMan@938 515 this.outputGain.gain.value = 0.0;
BrechtDeMan@938 516
BrechtDeMan@938 517 // Connect buffer to the audio graph
BrechtDeMan@938 518 this.outputGain.connect(audioEngineContext.outputGain);
BrechtDeMan@938 519
BrechtDeMan@938 520 // the audiobuffer is not designed for multi-start playback
BrechtDeMan@938 521 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
BrechtDeMan@938 522 this.buffer;
BrechtDeMan@938 523
BrechtDeMan@969 524 this.flagAsPlayed = function() { // to be called explicitly when not in loop mode
BrechtDeMan@969 525 this.played = true;
BrechtDeMan@969 526 }
BrechtDeMan@969 527
nicholas@967 528 this.loopStart = function() {
nicholas@967 529 this.outputGain.gain.value = 1.0;
nicholas@967 530 this.metric.startListening(audioEngineContext.timer.getTestTime());
BrechtDeMan@969 531 this.played = true;
nicholas@967 532 }
nicholas@967 533
nicholas@967 534 this.loopStop = function() {
nicholas@967 535 if (this.outputGain.gain.value != 0.0) {
nicholas@967 536 this.outputGain.gain.value = 0.0;
nicholas@967 537 this.metric.stopListening(audioEngineContext.timer.getTestTime());
nicholas@967 538 }
nicholas@967 539 }
nicholas@967 540
BrechtDeMan@938 541 this.play = function(startTime) {
BrechtDeMan@938 542 this.bufferNode = audioContext.createBufferSource();
nicholas@947 543 this.bufferNode.owner = this;
BrechtDeMan@938 544 this.bufferNode.connect(this.outputGain);
BrechtDeMan@938 545 this.bufferNode.buffer = this.buffer;
BrechtDeMan@938 546 this.bufferNode.loop = audioEngineContext.loopPlayback;
nicholas@967 547 this.bufferNode.onended = function() {
nicholas@967 548 this.owner.metric.stopListening(audioEngineContext.timer.getTestTime());
nicholas@967 549 };
nicholas@947 550 if (this.bufferNode.loop == false) {
nicholas@967 551 this.metric.startListening(audioEngineContext.timer.getTestTime());
nicholas@947 552 }
BrechtDeMan@938 553 this.bufferNode.start(startTime);
BrechtDeMan@938 554 };
BrechtDeMan@938 555
BrechtDeMan@938 556 this.stop = function() {
BrechtDeMan@937 557 if (this.bufferNode != undefined)
BrechtDeMan@937 558 {
BrechtDeMan@937 559 this.bufferNode.stop(0);
BrechtDeMan@937 560 this.bufferNode = undefined;
nicholas@967 561 this.metric.stopListening(audioEngineContext.timer.getTestTime());
BrechtDeMan@937 562 }
BrechtDeMan@938 563 };
BrechtDeMan@938 564
BrechtDeMan@938 565 this.constructTrack = function(url) {
BrechtDeMan@938 566 var request = new XMLHttpRequest();
BrechtDeMan@938 567 this.url = url;
BrechtDeMan@938 568 request.open('GET',url,true);
BrechtDeMan@938 569 request.responseType = 'arraybuffer';
BrechtDeMan@938 570
BrechtDeMan@938 571 var audioObj = this;
BrechtDeMan@938 572
BrechtDeMan@938 573 // Create callback to decode the data asynchronously
BrechtDeMan@938 574 request.onloadend = function() {
BrechtDeMan@938 575 audioContext.decodeAudioData(request.response, function(decodedData) {
BrechtDeMan@938 576 audioObj.buffer = decodedData;
BrechtDeMan@938 577 audioObj.state = 1;
BrechtDeMan@938 578 }, function(){
BrechtDeMan@938 579 // Should only be called if there was an error, but sometimes gets called continuously
BrechtDeMan@938 580 // Check here if the error is genuine
BrechtDeMan@938 581 if (audioObj.state == 0 || audioObj.buffer == undefined) {
BrechtDeMan@938 582 // Genuine error
BrechtDeMan@938 583 console.log('FATAL - Error loading buffer on '+audioObj.id);
BrechtDeMan@938 584 }
BrechtDeMan@938 585 });
BrechtDeMan@938 586 };
BrechtDeMan@938 587 request.send();
BrechtDeMan@938 588 };
BrechtDeMan@938 589
BrechtDeMan@938 590 }
BrechtDeMan@938 591
BrechtDeMan@938 592 function timer()
BrechtDeMan@938 593 {
BrechtDeMan@938 594 /* Timer object used in audioEngine to keep track of session timings
BrechtDeMan@938 595 * Uses the timer of the web audio API, so sample resolution
BrechtDeMan@938 596 */
BrechtDeMan@938 597 this.testStarted = false;
BrechtDeMan@938 598 this.testStartTime = 0;
BrechtDeMan@938 599 this.testDuration = 0;
BrechtDeMan@938 600 this.minimumTestTime = 0; // No minimum test time
BrechtDeMan@938 601 this.startTest = function()
BrechtDeMan@938 602 {
BrechtDeMan@938 603 if (this.testStarted == false)
BrechtDeMan@938 604 {
BrechtDeMan@938 605 this.testStartTime = audioContext.currentTime;
BrechtDeMan@938 606 this.testStarted = true;
BrechtDeMan@938 607 this.updateTestTime();
BrechtDeMan@938 608 audioEngineContext.metric.initialiseTest();
BrechtDeMan@938 609 }
BrechtDeMan@938 610 };
BrechtDeMan@938 611 this.stopTest = function()
BrechtDeMan@938 612 {
BrechtDeMan@938 613 if (this.testStarted)
BrechtDeMan@938 614 {
BrechtDeMan@938 615 this.testDuration = this.getTestTime();
BrechtDeMan@938 616 this.testStarted = false;
BrechtDeMan@938 617 } else {
BrechtDeMan@938 618 console.log('ERR: Test tried to end before beginning');
BrechtDeMan@938 619 }
BrechtDeMan@938 620 };
BrechtDeMan@938 621 this.updateTestTime = function()
BrechtDeMan@938 622 {
BrechtDeMan@938 623 if (this.testStarted)
BrechtDeMan@938 624 {
BrechtDeMan@938 625 this.testDuration = audioContext.currentTime - this.testStartTime;
BrechtDeMan@938 626 }
BrechtDeMan@938 627 };
BrechtDeMan@938 628 this.getTestTime = function()
BrechtDeMan@938 629 {
BrechtDeMan@938 630 this.updateTestTime();
BrechtDeMan@938 631 return this.testDuration;
BrechtDeMan@938 632 };
BrechtDeMan@938 633 }
BrechtDeMan@938 634
BrechtDeMan@938 635 function sessionMetrics(engine)
BrechtDeMan@938 636 {
BrechtDeMan@938 637 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
BrechtDeMan@938 638 */
BrechtDeMan@938 639 this.engine = engine;
BrechtDeMan@938 640 this.lastClicked = -1;
BrechtDeMan@938 641 this.data = -1;
n@950 642 this.reset = function() {
n@950 643 this.lastClicked = -1;
n@950 644 this.data = -1;
n@950 645 };
BrechtDeMan@938 646 this.initialiseTest = function(){};
BrechtDeMan@938 647 }
BrechtDeMan@938 648
BrechtDeMan@938 649 function metricTracker()
BrechtDeMan@938 650 {
BrechtDeMan@938 651 /* Custom object to track and collect metric data
BrechtDeMan@938 652 * Used only inside the audioObjects object.
BrechtDeMan@938 653 */
BrechtDeMan@938 654
BrechtDeMan@938 655 this.listenedTimer = 0;
BrechtDeMan@938 656 this.listenStart = 0;
nicholas@947 657 this.listenHold = false;
BrechtDeMan@938 658 this.initialPosition = -1;
BrechtDeMan@938 659 this.movementTracker = [];
BrechtDeMan@938 660 this.wasListenedTo = false;
BrechtDeMan@938 661 this.wasMoved = false;
BrechtDeMan@938 662 this.hasComments = false;
BrechtDeMan@938 663
BrechtDeMan@938 664 this.initialised = function(position)
BrechtDeMan@938 665 {
BrechtDeMan@938 666 if (this.initialPosition == -1) {
BrechtDeMan@938 667 this.initialPosition = position;
BrechtDeMan@938 668 }
BrechtDeMan@938 669 };
BrechtDeMan@938 670
BrechtDeMan@938 671 this.moved = function(time,position)
BrechtDeMan@938 672 {
BrechtDeMan@938 673 this.wasMoved = true;
BrechtDeMan@938 674 this.movementTracker[this.movementTracker.length] = [time, position];
BrechtDeMan@938 675 };
BrechtDeMan@938 676
nicholas@967 677 this.startListening = function(time)
BrechtDeMan@938 678 {
nicholas@947 679 if (this.listenHold == false)
BrechtDeMan@938 680 {
BrechtDeMan@938 681 this.wasListenedTo = true;
BrechtDeMan@938 682 this.listenStart = time;
nicholas@947 683 this.listenHold = true;
nicholas@967 684 }
nicholas@967 685 }
nicholas@967 686
nicholas@967 687 this.stopListening = function(time)
nicholas@967 688 {
nicholas@967 689 if (this.listenHold == true)
nicholas@967 690 {
BrechtDeMan@938 691 this.listenedTimer += (time - this.listenStart);
BrechtDeMan@938 692 this.listenStart = 0;
nicholas@947 693 this.listenHold = false;
BrechtDeMan@938 694 }
BrechtDeMan@938 695 };
BrechtDeMan@938 696 }
BrechtDeMan@938 697
BrechtDeMan@938 698 function randomiseOrder(input)
BrechtDeMan@938 699 {
BrechtDeMan@938 700 // This takes an array of information and randomises the order
BrechtDeMan@938 701 var N = input.length;
BrechtDeMan@938 702 var K = N;
BrechtDeMan@938 703 var holdArr = [];
BrechtDeMan@938 704 for (var n=0; n<N; n++)
BrechtDeMan@938 705 {
BrechtDeMan@938 706 // First pick a random number
BrechtDeMan@938 707 var r = Math.random();
BrechtDeMan@938 708 // Multiply and floor by the number of elements left
BrechtDeMan@938 709 r = Math.floor(r*input.length);
BrechtDeMan@938 710 // Pick out that element and delete from the array
BrechtDeMan@938 711 holdArr.push(input.splice(r,1)[0]);
BrechtDeMan@938 712 }
BrechtDeMan@938 713 return holdArr;
n@961 714 }
n@961 715
n@961 716 function returnDateNode()
n@961 717 {
n@961 718 // Create an XML Node for the Date and Time a test was conducted
n@961 719 // Structure is
n@961 720 // <datetime>
n@961 721 // <date year="##" month="##" day="##">DD/MM/YY</date>
n@961 722 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
n@961 723 // </datetime>
n@961 724 var dateTime = new Date();
n@961 725 var year = document.createAttribute('year');
n@961 726 var month = document.createAttribute('month');
n@961 727 var day = document.createAttribute('day');
n@961 728 var hour = document.createAttribute('hour');
n@961 729 var minute = document.createAttribute('minute');
n@961 730 var secs = document.createAttribute('secs');
n@961 731
n@961 732 year.nodeValue = dateTime.getFullYear();
n@961 733 month.nodeValue = dateTime.getMonth()+1;
n@961 734 day.nodeValue = dateTime.getDate();
n@961 735 hour.nodeValue = dateTime.getHours();
n@961 736 minute.nodeValue = dateTime.getMinutes();
n@961 737 secs.nodeValue = dateTime.getSeconds();
n@961 738
n@961 739 var hold = document.createElement("datetime");
n@961 740 var date = document.createElement("date");
n@961 741 date.textContent = year.nodeValue+'/'+month.nodeValue+'/'+day.nodeValue;
n@961 742 var time = document.createElement("time");
n@961 743 time.textContent = hour.nodeValue+':'+minute.nodeValue+':'+secs.nodeValue;
n@961 744
n@961 745 date.setAttributeNode(year);
n@961 746 date.setAttributeNode(month);
n@961 747 date.setAttributeNode(day);
n@961 748 time.setAttributeNode(hour);
n@961 749 time.setAttributeNode(minute);
n@961 750 time.setAttributeNode(secs);
n@961 751
n@961 752 hold.appendChild(date);
n@961 753 hold.appendChild(time);
n@961 754 return hold
n@961 755
nicholas@964 756 }