annotate core.js @ 1667:f82f0a3bfc14

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