annotate core.js @ 950:05e7edb032b9

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