annotate core.js @ 1634:90c5e3c53066

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