annotate core.js @ 1620:da84079192c6

Fix Bug #1241 and #1213: Added checks each time new test page is loaded that all audioObjects have decoded. Writes to browser console WAIT and does not issue any play command if any audioObjects not ready.
author Nicholas Jillings <nickjillings@users.noreply.github.com>
date Mon, 25 May 2015 11:14:12 +0100
parents ed5b4a9b266a
children 55bf2500f278
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
b@1608 11
b@1608 12 var testXMLSetups = []; // Hold the parsed test instances
b@1608 13 var testResultsHolders =[]; // Hold the results from each test for publishing to XML
b@1608 14 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
b@1608 15 var currentTestHolder; // Hold any intermediate results during test - metrics
b@1608 16 var audioEngineContext; // The custome AudioEngine object
b@1608 17 var projectReturn; // Hold the URL for the return
b@1608 18 var preTestQuestions = document.createElement('PreTest'); // Store any pre-test question response
b@1608 19 var postTestQuestions = document.createElement('PostTest'); // Store any post-test question response
b@1608 20
b@1608 21 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
b@1608 22 AudioBufferSourceNode.prototype.owner = undefined;
b@1608 23
b@1608 24 window.onload = function() {
b@1608 25 // Function called once the browser has loaded all files.
b@1608 26 // This should perform any initial commands such as structure / loading documents
b@1608 27
b@1608 28 // Create a web audio API context
b@1608 29 // Fixed for cross-browser support
b@1608 30 var AudioContext = window.AudioContext || window.webkitAudioContext;
b@1608 31 audioContext = new AudioContext;
b@1608 32
b@1608 33 // Create the audio engine object
b@1608 34 audioEngineContext = new AudioEngine();
b@1608 35 };
b@1608 36
b@1608 37 function loadProjectSpec(url) {
b@1608 38 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
b@1608 39 // If url is null, request client to upload project XML document
b@1608 40 var r = new XMLHttpRequest();
b@1608 41 r.open('GET',url,true);
b@1608 42 r.onload = function() {
b@1608 43 loadProjectSpecCallback(r.response);
b@1608 44 };
b@1608 45 r.send();
b@1608 46 };
b@1608 47
b@1608 48 function loadProjectSpecCallback(response) {
b@1608 49 // Function called after asynchronous download of XML project specification
b@1608 50 var decode = $.parseXML(response);
b@1608 51 projectXML = $(decode);
b@1608 52
b@1608 53 // Now extract the setup tag
b@1608 54 var xmlSetup = projectXML.find('setup');
b@1608 55 // Detect the interface to use and load the relevant javascripts.
b@1608 56 var interfaceType = xmlSetup[0].attributes['interface'];
b@1608 57 var interfaceJS = document.createElement('script');
b@1608 58 interfaceJS.setAttribute("type","text/javascript");
b@1608 59 if (interfaceType.value == 'APE') {
b@1608 60 interfaceJS.setAttribute("src","ape.js");
b@1608 61
b@1608 62 // APE comes with a css file
b@1608 63 var css = document.createElement('link');
b@1608 64 css.rel = 'stylesheet';
b@1608 65 css.type = 'text/css';
b@1608 66 css.href = 'ape.css';
b@1608 67
b@1608 68 document.getElementsByTagName("head")[0].appendChild(css);
b@1608 69 }
b@1608 70 document.getElementsByTagName("head")[0].appendChild(interfaceJS);
b@1608 71 }
b@1608 72
b@1608 73 function createProjectSave(destURL) {
b@1608 74 // Save the data from interface into XML and send to destURL
b@1608 75 // If destURL is null then download XML in client
b@1608 76 // Now time to render file locally
b@1608 77 var xmlDoc = interfaceXMLSave();
b@1608 78 if (destURL == "null" || destURL == undefined) {
b@1608 79 var parent = document.createElement("div");
b@1608 80 parent.appendChild(xmlDoc);
b@1608 81 var file = [parent.innerHTML];
b@1608 82 var bb = new Blob(file,{type : 'application/xml'});
b@1608 83 var dnlk = window.URL.createObjectURL(bb);
b@1608 84 var a = document.createElement("a");
b@1608 85 a.hidden = '';
b@1608 86 a.href = dnlk;
b@1608 87 a.download = "save.xml";
b@1608 88 a.textContent = "Save File";
b@1608 89
b@1608 90 var submitDiv = document.getElementById('download-point');
b@1608 91 submitDiv.appendChild(a);
b@1608 92 }
b@1608 93 return submitDiv;
b@1608 94 }
b@1608 95
b@1608 96 function AudioEngine() {
b@1608 97
b@1608 98 // Create two output paths, the main outputGain and fooGain.
b@1608 99 // Output gain is default to 1 and any items for playback route here
b@1608 100 // Foo gain is used for analysis to ensure paths get processed, but are not heard
b@1608 101 // because web audio will optimise and any route which does not go to the destination gets ignored.
b@1608 102 this.outputGain = audioContext.createGain();
b@1608 103 this.fooGain = audioContext.createGain();
b@1608 104 this.fooGain.gain = 0;
b@1608 105
b@1608 106 // Use this to detect playback state: 0 - stopped, 1 - playing
b@1608 107 this.status = 0;
nickjillings@1620 108 this.audioObjectsReady = false;
b@1608 109
b@1608 110 // Connect both gains to output
b@1608 111 this.outputGain.connect(audioContext.destination);
b@1608 112 this.fooGain.connect(audioContext.destination);
b@1608 113
b@1608 114 // Create the timer Object
b@1608 115 this.timer = new timer();
b@1608 116 // Create session metrics
b@1608 117 this.metric = new sessionMetrics(this);
b@1608 118
b@1608 119 this.loopPlayback = false;
b@1608 120
b@1608 121 // Create store for new audioObjects
b@1608 122 this.audioObjects = [];
b@1608 123
nickjillings@1620 124 this.play = function() {
nickjillings@1620 125 // Start the timer and set the audioEngine state to playing (1)
nickjillings@1620 126 if (this.status == 0) {
nickjillings@1620 127 // Check if all audioObjects are ready
nickjillings@1620 128 if (this.audioObjectsReady == false) {
nickjillings@1620 129 this.audioObjectsReady = this.checkAllReady();
nickjillings@1620 130 }
nickjillings@1620 131 if (this.audioObjectsReady == true) {
nickjillings@1620 132 this.timer.startTest();
nickjillings@1620 133 this.status = 1;
nickjillings@1620 134 }
nickjillings@1620 135 }
nickjillings@1620 136 };
b@1608 137
nickjillings@1620 138 this.stop = function() {
nickjillings@1620 139 // Send stop and reset command to all playback buffers and set audioEngine state to stopped (1)
nickjillings@1620 140 if (this.status == 1) {
nickjillings@1620 141 for (var i=0; i<this.audioObjects.length; i++)
nickjillings@1620 142 {
nickjillings@1620 143 this.audioObjects[i].stop();
nickjillings@1620 144 }
nickjillings@1620 145 this.status = 0;
nickjillings@1620 146 }
nickjillings@1620 147 };
b@1608 148
b@1608 149
b@1608 150 this.newTrack = function(url) {
b@1608 151 // Pull data from given URL into new audio buffer
b@1608 152 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
b@1608 153
b@1608 154 // Create the audioObject with ID of the new track length;
b@1608 155 audioObjectId = this.audioObjects.length;
b@1608 156 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
b@1608 157
b@1608 158 // AudioObject will get track itself.
b@1608 159 this.audioObjects[audioObjectId].constructTrack(url);
b@1608 160 };
b@1608 161
nickjillings@1620 162 this.newTestPage = function() {
nickjillings@1620 163 this.state = 0;
nickjillings@1620 164 this.audioObjectsReady = false;
nickjillings@1620 165 this.metric.reset();
nickjillings@1620 166 this.audioObjects = [];
nickjillings@1620 167 };
nickjillings@1620 168
nickjillings@1614 169 this.checkAllPlayed = function() {
nickjillings@1614 170 arr = [];
nickjillings@1614 171 for (var id=0; id<this.audioObjects.length; id++) {
nickjillings@1614 172 if (this.audioObjects[id].played == false) {
nickjillings@1614 173 arr.push(this.audioObjects[id].id);
nickjillings@1614 174 }
nickjillings@1614 175 }
nickjillings@1614 176 return arr;
nickjillings@1614 177 };
nickjillings@1614 178
nickjillings@1620 179 this.checkAllReady = function() {
nickjillings@1620 180 var ready = true;
nickjillings@1620 181 for (var i=0; i<this.audioObjects.length; i++) {
nickjillings@1620 182 if (this.audioObjects[i].state == 0) {
nickjillings@1620 183 // Track not ready
nickjillings@1620 184 console.log('WAIT -- audioObject '+i+' not ready yet!');
nickjillings@1620 185 ready = false;
nickjillings@1620 186 };
nickjillings@1620 187 }
nickjillings@1620 188 return ready;
nickjillings@1620 189 };
nickjillings@1620 190
b@1608 191 }
b@1608 192
b@1608 193 function audioObject(id) {
b@1608 194 // The main buffer object with common control nodes to the AudioEngine
b@1608 195
b@1608 196 this.id = id;
b@1608 197 this.state = 0; // 0 - no data, 1 - ready
b@1608 198 this.url = null; // Hold the URL given for the output back to the results.
b@1608 199 this.metric = new metricTracker();
b@1608 200
nickjillings@1614 201 this.played = false;
nickjillings@1614 202
b@1608 203 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
b@1608 204 this.bufferNode = undefined;
b@1608 205 this.outputGain = audioContext.createGain();
b@1608 206
b@1608 207 // Default output gain to be zero
b@1608 208 this.outputGain.gain.value = 0.0;
b@1608 209
b@1608 210 // Connect buffer to the audio graph
b@1608 211 this.outputGain.connect(audioEngineContext.outputGain);
b@1608 212
b@1608 213 // the audiobuffer is not designed for multi-start playback
b@1608 214 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
b@1608 215 this.buffer;
b@1608 216
b@1608 217 this.play = function(startTime) {
b@1608 218 this.bufferNode = audioContext.createBufferSource();
nickjillings@1617 219 this.bufferNode.owner = this;
b@1608 220 this.bufferNode.connect(this.outputGain);
b@1608 221 this.bufferNode.buffer = this.buffer;
b@1608 222 this.bufferNode.loop = audioEngineContext.loopPlayback;
nickjillings@1617 223 if (this.bufferNode.loop == false) {
nickjillings@1617 224 this.bufferNode.onended = function() {
nickjillings@1617 225 this.owner.metric.listening(audioEngineContext.timer.getTestTime());
nickjillings@1620 226 };
nickjillings@1617 227 }
nickjillings@1617 228 this.metric.listening(audioEngineContext.timer.getTestTime());
b@1608 229 this.bufferNode.start(startTime);
nickjillings@1614 230 this.played = true;
b@1608 231 };
b@1608 232
b@1608 233 this.stop = function() {
b@1607 234 if (this.bufferNode != undefined)
b@1607 235 {
b@1607 236 this.bufferNode.stop(0);
b@1607 237 this.bufferNode = undefined;
nickjillings@1617 238 this.metric.listening(audioEngineContext.timer.getTestTime());
b@1607 239 }
b@1608 240 };
b@1608 241
b@1608 242 this.constructTrack = function(url) {
b@1608 243 var request = new XMLHttpRequest();
b@1608 244 this.url = url;
b@1608 245 request.open('GET',url,true);
b@1608 246 request.responseType = 'arraybuffer';
b@1608 247
b@1608 248 var audioObj = this;
b@1608 249
b@1608 250 // Create callback to decode the data asynchronously
b@1608 251 request.onloadend = function() {
b@1608 252 audioContext.decodeAudioData(request.response, function(decodedData) {
b@1608 253 audioObj.buffer = decodedData;
b@1608 254 audioObj.state = 1;
b@1608 255 }, function(){
b@1608 256 // Should only be called if there was an error, but sometimes gets called continuously
b@1608 257 // Check here if the error is genuine
b@1608 258 if (audioObj.state == 0 || audioObj.buffer == undefined) {
b@1608 259 // Genuine error
b@1608 260 console.log('FATAL - Error loading buffer on '+audioObj.id);
b@1608 261 }
b@1608 262 });
b@1608 263 };
b@1608 264 request.send();
b@1608 265 };
b@1608 266
b@1608 267 }
b@1608 268
b@1608 269 function timer()
b@1608 270 {
b@1608 271 /* Timer object used in audioEngine to keep track of session timings
b@1608 272 * Uses the timer of the web audio API, so sample resolution
b@1608 273 */
b@1608 274 this.testStarted = false;
b@1608 275 this.testStartTime = 0;
b@1608 276 this.testDuration = 0;
b@1608 277 this.minimumTestTime = 0; // No minimum test time
b@1608 278 this.startTest = function()
b@1608 279 {
b@1608 280 if (this.testStarted == false)
b@1608 281 {
b@1608 282 this.testStartTime = audioContext.currentTime;
b@1608 283 this.testStarted = true;
b@1608 284 this.updateTestTime();
b@1608 285 audioEngineContext.metric.initialiseTest();
b@1608 286 }
b@1608 287 };
b@1608 288 this.stopTest = function()
b@1608 289 {
b@1608 290 if (this.testStarted)
b@1608 291 {
b@1608 292 this.testDuration = this.getTestTime();
b@1608 293 this.testStarted = false;
b@1608 294 } else {
b@1608 295 console.log('ERR: Test tried to end before beginning');
b@1608 296 }
b@1608 297 };
b@1608 298 this.updateTestTime = function()
b@1608 299 {
b@1608 300 if (this.testStarted)
b@1608 301 {
b@1608 302 this.testDuration = audioContext.currentTime - this.testStartTime;
b@1608 303 }
b@1608 304 };
b@1608 305 this.getTestTime = function()
b@1608 306 {
b@1608 307 this.updateTestTime();
b@1608 308 return this.testDuration;
b@1608 309 };
b@1608 310 }
b@1608 311
b@1608 312 function sessionMetrics(engine)
b@1608 313 {
b@1608 314 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
b@1608 315 */
b@1608 316 this.engine = engine;
b@1608 317 this.lastClicked = -1;
b@1608 318 this.data = -1;
nickjillings@1620 319 this.reset = function() {
nickjillings@1620 320 this.lastClicked = -1;
nickjillings@1620 321 this.data = -1;
nickjillings@1620 322 };
b@1608 323 this.initialiseTest = function(){};
b@1608 324 }
b@1608 325
b@1608 326 function metricTracker()
b@1608 327 {
b@1608 328 /* Custom object to track and collect metric data
b@1608 329 * Used only inside the audioObjects object.
b@1608 330 */
b@1608 331
b@1608 332 this.listenedTimer = 0;
b@1608 333 this.listenStart = 0;
nickjillings@1617 334 this.listenHold = false;
b@1608 335 this.initialPosition = -1;
b@1608 336 this.movementTracker = [];
b@1608 337 this.wasListenedTo = false;
b@1608 338 this.wasMoved = false;
b@1608 339 this.hasComments = false;
b@1608 340
b@1608 341 this.initialised = function(position)
b@1608 342 {
b@1608 343 if (this.initialPosition == -1) {
b@1608 344 this.initialPosition = position;
b@1608 345 }
b@1608 346 };
b@1608 347
b@1608 348 this.moved = function(time,position)
b@1608 349 {
b@1608 350 this.wasMoved = true;
b@1608 351 this.movementTracker[this.movementTracker.length] = [time, position];
b@1608 352 };
b@1608 353
b@1608 354 this.listening = function(time)
b@1608 355 {
nickjillings@1617 356 if (this.listenHold == false)
b@1608 357 {
b@1608 358 this.wasListenedTo = true;
b@1608 359 this.listenStart = time;
nickjillings@1617 360 this.listenHold = true;
b@1608 361 } else {
b@1608 362 this.listenedTimer += (time - this.listenStart);
b@1608 363 this.listenStart = 0;
nickjillings@1617 364 this.listenHold = false;
b@1608 365 }
b@1608 366 };
b@1608 367 }
b@1608 368
b@1608 369 function randomiseOrder(input)
b@1608 370 {
b@1608 371 // This takes an array of information and randomises the order
b@1608 372 var N = input.length;
b@1608 373 var K = N;
b@1608 374 var holdArr = [];
b@1608 375 for (var n=0; n<N; n++)
b@1608 376 {
b@1608 377 // First pick a random number
b@1608 378 var r = Math.random();
b@1608 379 // Multiply and floor by the number of elements left
b@1608 380 r = Math.floor(r*input.length);
b@1608 381 // Pick out that element and delete from the array
b@1608 382 holdArr.push(input.splice(r,1)[0]);
b@1608 383 }
b@1608 384 return holdArr;
b@1608 385 }