nicholas@2224
|
1 /**
|
nicholas@2224
|
2 * core.js
|
nicholas@2224
|
3 *
|
nicholas@2224
|
4 * Main script to run, calls all other core functions and manages loading/store to backend.
|
nicholas@2224
|
5 * Also contains all global variables.
|
nicholas@2224
|
6 */
|
nicholas@2224
|
7
|
nicholas@2708
|
8 /*globals window, document, XMLDocument, Element, XMLHttpRequest, DOMParser, console, Blob, $, Promise, navigator */
|
nicholas@2708
|
9 /*globals AudioBuffer, AudioBufferSourceNode */
|
nicholas@2708
|
10 /*globals Specification, calculateLoudness, WAVE, validateXML, showdown, pageXMLSave, loadTest, resizeWindow */
|
nicholas@2708
|
11
|
nicholas@2224
|
12 /* create the web audio API context and store in audioContext*/
|
nicholas@2224
|
13 var audioContext; // Hold the browser web audio API
|
nicholas@2224
|
14 var projectXML; // Hold the parsed setup XML
|
nicholas@2224
|
15 var schemaXSD; // Hold the parsed schema XSD
|
nicholas@2224
|
16 var specification;
|
nicholas@2224
|
17 var interfaceContext;
|
nicholas@2224
|
18 var storage;
|
nicholas@2224
|
19 var popup; // Hold the interfacePopup object
|
nicholas@2224
|
20 var testState;
|
nicholas@2224
|
21 var currentTrackOrder = []; // Hold the current XML tracks in their (randomised) order
|
nicholas@2224
|
22 var audioEngineContext; // The custome AudioEngine object
|
nicholas@2329
|
23 var gReturnURL;
|
giuliomoro@2337
|
24 var gSaveFilenamePrefix;
|
nicholas@2224
|
25
|
nicholas@2224
|
26
|
nicholas@2224
|
27 // Add a prototype to the bufferSourceNode to reference to the audioObject holding it
|
nicholas@2224
|
28 AudioBufferSourceNode.prototype.owner = undefined;
|
nicholas@2224
|
29 // Add a prototype to the bufferSourceNode to hold when the object was given a play command
|
nicholas@2224
|
30 AudioBufferSourceNode.prototype.playbackStartTime = undefined;
|
nicholas@2224
|
31 // Add a prototype to the bufferNode to hold the desired LINEAR gain
|
nicholas@2224
|
32 AudioBuffer.prototype.playbackGain = undefined;
|
nicholas@2224
|
33 // Add a prototype to the bufferNode to hold the computed LUFS loudness
|
nicholas@2224
|
34 AudioBuffer.prototype.lufs = undefined;
|
nicholas@2224
|
35
|
nicholas@2224
|
36 // Convert relative URLs into absolutes
|
nicholas@2224
|
37 function escapeHTML(s) {
|
nicholas@2224
|
38 return s.split('&').join('&').split('<').join('<').split('"').join('"');
|
nicholas@2224
|
39 }
|
nicholas@2498
|
40
|
nicholas@2224
|
41 function qualifyURL(url) {
|
nicholas@2498
|
42 var el = document.createElement('div');
|
nicholas@2498
|
43 el.innerHTML = '<a href="' + escapeHTML(url) + '">x</a>';
|
nicholas@2224
|
44 return el.firstChild.href;
|
nicholas@2224
|
45 }
|
nicholas@2224
|
46
|
nicholas@2224
|
47 // Firefox does not have an XMLDocument.prototype.getElementsByName
|
nicholas@2224
|
48 // and there is no searchAll style command, this custom function will
|
nicholas@2224
|
49 // search all children recusrively for the name. Used for XSD where all
|
nicholas@2224
|
50 // element nodes must have a name and therefore can pull the schema node
|
nicholas@2498
|
51 XMLDocument.prototype.getAllElementsByName = function (name) {
|
nicholas@2224
|
52 name = String(name);
|
nicholas@2224
|
53 var selected = this.documentElement.getAllElementsByName(name);
|
nicholas@2224
|
54 return selected;
|
nicholas@2708
|
55 };
|
nicholas@2224
|
56
|
nicholas@2498
|
57 Element.prototype.getAllElementsByName = function (name) {
|
nicholas@2224
|
58 name = String(name);
|
nicholas@2224
|
59 var selected = [];
|
nicholas@2224
|
60 var node = this.firstElementChild;
|
nicholas@2708
|
61 while (node !== null) {
|
nicholas@2498
|
62 if (node.getAttribute('name') == name) {
|
nicholas@2224
|
63 selected.push(node);
|
nicholas@2224
|
64 }
|
nicholas@2498
|
65 if (node.childElementCount > 0) {
|
nicholas@2224
|
66 selected = selected.concat(node.getAllElementsByName(name));
|
nicholas@2224
|
67 }
|
nicholas@2224
|
68 node = node.nextElementSibling;
|
nicholas@2224
|
69 }
|
nicholas@2224
|
70 return selected;
|
nicholas@2708
|
71 };
|
nicholas@2224
|
72
|
nicholas@2498
|
73 XMLDocument.prototype.getAllElementsByTagName = function (name) {
|
nicholas@2224
|
74 name = String(name);
|
nicholas@2224
|
75 var selected = this.documentElement.getAllElementsByTagName(name);
|
nicholas@2224
|
76 return selected;
|
nicholas@2708
|
77 };
|
nicholas@2224
|
78
|
nicholas@2498
|
79 Element.prototype.getAllElementsByTagName = function (name) {
|
nicholas@2224
|
80 name = String(name);
|
nicholas@2224
|
81 var selected = [];
|
nicholas@2224
|
82 var node = this.firstElementChild;
|
nicholas@2708
|
83 while (node !== null) {
|
nicholas@2498
|
84 if (node.nodeName == name) {
|
nicholas@2224
|
85 selected.push(node);
|
nicholas@2224
|
86 }
|
nicholas@2498
|
87 if (node.childElementCount > 0) {
|
nicholas@2224
|
88 selected = selected.concat(node.getAllElementsByTagName(name));
|
nicholas@2224
|
89 }
|
nicholas@2224
|
90 node = node.nextElementSibling;
|
nicholas@2224
|
91 }
|
nicholas@2224
|
92 return selected;
|
nicholas@2708
|
93 };
|
nicholas@2224
|
94
|
nicholas@2224
|
95 // Firefox does not have an XMLDocument.prototype.getElementsByName
|
nicholas@2224
|
96 if (typeof XMLDocument.prototype.getElementsByName != "function") {
|
nicholas@2498
|
97 XMLDocument.prototype.getElementsByName = function (name) {
|
nicholas@2224
|
98 name = String(name);
|
nicholas@2224
|
99 var node = this.documentElement.firstElementChild;
|
nicholas@2224
|
100 var selected = [];
|
nicholas@2708
|
101 while (node !== null) {
|
nicholas@2498
|
102 if (node.getAttribute('name') == name) {
|
nicholas@2224
|
103 selected.push(node);
|
nicholas@2224
|
104 }
|
nicholas@2224
|
105 node = node.nextElementSibling;
|
nicholas@2224
|
106 }
|
nicholas@2224
|
107 return selected;
|
nicholas@2708
|
108 };
|
nicholas@2224
|
109 }
|
nicholas@2224
|
110
|
nicholas@2498
|
111 var check_dependancies = function () {
|
nicholas@2401
|
112 // This will check for the data dependancies
|
nicholas@2498
|
113 if (typeof (jQuery) != "function") {
|
nicholas@2498
|
114 return false;
|
nicholas@2498
|
115 }
|
nicholas@2498
|
116 if (typeof (Specification) != "function") {
|
nicholas@2498
|
117 return false;
|
nicholas@2498
|
118 }
|
nicholas@2498
|
119 if (typeof (calculateLoudness) != "function") {
|
nicholas@2498
|
120 return false;
|
nicholas@2498
|
121 }
|
nicholas@2498
|
122 if (typeof (WAVE) != "function") {
|
nicholas@2498
|
123 return false;
|
nicholas@2498
|
124 }
|
nicholas@2498
|
125 if (typeof (validateXML) != "function") {
|
nicholas@2498
|
126 return false;
|
nicholas@2498
|
127 }
|
nicholas@2401
|
128 return true;
|
nicholas@2708
|
129 };
|
nicholas@2401
|
130
|
nicholas@2498
|
131 var onload = function () {
|
nicholas@2498
|
132 // Function called once the browser has loaded all files.
|
nicholas@2498
|
133 // This should perform any initial commands such as structure / loading documents
|
nicholas@2498
|
134
|
nicholas@2498
|
135 // Create a web audio API context
|
nicholas@2498
|
136 // Fixed for cross-browser support
|
nicholas@2498
|
137 var AudioContext = window.AudioContext || window.webkitAudioContext;
|
nicholas@2708
|
138 audioContext = new AudioContext();
|
nicholas@2498
|
139
|
nicholas@2498
|
140 // Create test state
|
nicholas@2498
|
141 testState = new stateMachine();
|
nicholas@2498
|
142
|
nicholas@2498
|
143 // Create the popup interface object
|
nicholas@2498
|
144 popup = new interfacePopup();
|
nicholas@2498
|
145
|
nicholas@2224
|
146 // Create the specification object
|
nicholas@2498
|
147 specification = new Specification();
|
nicholas@2498
|
148
|
nicholas@2498
|
149 // Create the interface object
|
nicholas@2498
|
150 interfaceContext = new Interface(specification);
|
nicholas@2498
|
151
|
nicholas@2498
|
152 // Create the storage object
|
nicholas@2498
|
153 storage = new Storage();
|
nicholas@2498
|
154 // Define window callbacks for interface
|
nicholas@2498
|
155 window.onresize = function (event) {
|
nicholas@2498
|
156 interfaceContext.resizeWindow(event);
|
nicholas@2498
|
157 };
|
nicholas@2498
|
158
|
nicholas@2708
|
159 if (window.location.search.length !== 0) {
|
nicholas@2319
|
160 var search = window.location.search.split('?')[1];
|
nicholas@2319
|
161 // Now split the requests into pairs
|
nicholas@2319
|
162 var searchQueries = search.split('&');
|
nicholas@2708
|
163 var url;
|
nicholas@2498
|
164 for (var i in searchQueries) {
|
giuliomoro@2331
|
165 // Split each key-value pair
|
nicholas@2319
|
166 searchQueries[i] = searchQueries[i].split('=');
|
giuliomoro@2331
|
167 var key = searchQueries[i][0];
|
giuliomoro@2331
|
168 var value = decodeURIComponent(searchQueries[i][1]);
|
nicholas@2498
|
169 switch (key) {
|
nicholas@2498
|
170 case "url":
|
nicholas@2498
|
171 url = value;
|
nicholas@2708
|
172 specification.url = url;
|
nicholas@2498
|
173 break;
|
nicholas@2498
|
174 case "returnURL":
|
nicholas@2498
|
175 gReturnURL = value;
|
nicholas@2498
|
176 break;
|
nicholas@2498
|
177 case "saveFilenamePrefix":
|
nicholas@2722
|
178 storage.filenamePrefix = value;
|
nicholas@2498
|
179 break;
|
nicholas@2319
|
180 }
|
nicholas@2319
|
181 }
|
nicholas@2319
|
182 loadProjectSpec(url);
|
nicholas@2498
|
183 window.onbeforeunload = function () {
|
nicholas@2319
|
184 return "Please only leave this page once you have completed the tests. Are you sure you have completed all testing?";
|
nicholas@2319
|
185 };
|
nicholas@2319
|
186 }
|
nicholas@2360
|
187 interfaceContext.lightbox.resize();
|
nicholas@2224
|
188 };
|
nicholas@2224
|
189
|
nicholas@2224
|
190 function loadProjectSpec(url) {
|
nicholas@2498
|
191 // Load the project document from the given URL, decode the XML and instruct audioEngine to get audio data
|
nicholas@2498
|
192 // If url is null, request client to upload project XML document
|
nicholas@2498
|
193 var xmlhttp = new XMLHttpRequest();
|
nicholas@2498
|
194 xmlhttp.open("GET", 'xml/test-schema.xsd', true);
|
nicholas@2498
|
195 xmlhttp.onload = function () {
|
nicholas@2708
|
196 specification.processSchema(xmlhttp.response);
|
nicholas@2498
|
197 var r = new XMLHttpRequest();
|
nicholas@2498
|
198 r.open('GET', url, true);
|
nicholas@2498
|
199 r.onload = function () {
|
nicholas@2498
|
200 loadProjectSpecCallback(r.response);
|
nicholas@2498
|
201 };
|
nicholas@2498
|
202 r.onerror = function () {
|
nicholas@2224
|
203 document.getElementsByTagName('body')[0].innerHTML = null;
|
nicholas@2224
|
204 var msg = document.createElement("h3");
|
nicholas@2224
|
205 msg.textContent = "FATAL ERROR";
|
nicholas@2224
|
206 var span = document.createElement("p");
|
nicholas@2224
|
207 span.textContent = "There was an error when loading your XML file. Please check your path in the URL. After the path to this page, there should be '?url=path/to/your/file.xml'. Check the spelling of your filename as well. If you are still having issues, check the log of the python server or your webserver distribution for 404 codes for your file.";
|
nicholas@2224
|
208 document.getElementsByTagName('body')[0].appendChild(msg);
|
nicholas@2224
|
209 document.getElementsByTagName('body')[0].appendChild(span);
|
nicholas@2708
|
210 };
|
nicholas@2498
|
211 r.send();
|
nicholas@2498
|
212 };
|
nicholas@2498
|
213 xmlhttp.send();
|
nicholas@2708
|
214 }
|
nicholas@2224
|
215
|
nicholas@2224
|
216 function loadProjectSpecCallback(response) {
|
nicholas@2498
|
217 // Function called after asynchronous download of XML project specification
|
nicholas@2498
|
218 //var decode = $.parseXML(response);
|
nicholas@2498
|
219 //projectXML = $(decode);
|
nicholas@2498
|
220
|
nicholas@2224
|
221 // Check if XML is new or a resumption
|
nicholas@2224
|
222 var parse = new DOMParser();
|
nicholas@2498
|
223 var responseDocument = parse.parseFromString(response, 'text/xml');
|
nicholas@2224
|
224 var errorNode = responseDocument.getElementsByTagName('parsererror');
|
nicholas@2708
|
225 var msg, span;
|
nicholas@2498
|
226 if (errorNode.length >= 1) {
|
nicholas@2708
|
227 msg = document.createElement("h3");
|
nicholas@2498
|
228 msg.textContent = "FATAL ERROR";
|
nicholas@2708
|
229 span = document.createElement("span");
|
nicholas@2498
|
230 span.textContent = "The XML parser returned the following errors when decoding your XML file";
|
nicholas@2498
|
231 document.getElementsByTagName('body')[0].innerHTML = null;
|
nicholas@2498
|
232 document.getElementsByTagName('body')[0].appendChild(msg);
|
nicholas@2498
|
233 document.getElementsByTagName('body')[0].appendChild(span);
|
nicholas@2498
|
234 document.getElementsByTagName('body')[0].appendChild(errorNode[0]);
|
nicholas@2498
|
235 return;
|
nicholas@2498
|
236 }
|
nicholas@2708
|
237 if (responseDocument === undefined || responseDocument.firstChild === undefined) {
|
nicholas@2708
|
238 msg = document.createElement("h3");
|
nicholas@2498
|
239 msg.textContent = "FATAL ERROR";
|
nicholas@2708
|
240 span = document.createElement("span");
|
nicholas@2498
|
241 span.textContent = "The project XML was not decoded properly, try refreshing your browser and clearing caches. If the problem persists, contact the test creator.";
|
nicholas@2498
|
242 document.getElementsByTagName('body')[0].innerHTML = null;
|
nicholas@2498
|
243 document.getElementsByTagName('body')[0].appendChild(msg);
|
nicholas@2498
|
244 document.getElementsByTagName('body')[0].appendChild(span);
|
nicholas@2498
|
245 return;
|
nicholas@2224
|
246 }
|
nicholas@2247
|
247 if (responseDocument.firstChild.nodeName == "waet") {
|
nicholas@2224
|
248 // document is a specification
|
nicholas@2498
|
249
|
nicholas@2224
|
250 // Perform XML schema validation
|
nicholas@2224
|
251 var Module = {
|
nicholas@2224
|
252 xml: response,
|
nicholas@2708
|
253 schema: specification.getSchemaString(),
|
nicholas@2498
|
254 arguments: ["--noout", "--schema", 'test-schema.xsd', 'document.xml']
|
nicholas@2224
|
255 };
|
nicholas@2498
|
256 projectXML = responseDocument;
|
nicholas@2224
|
257 var xmllint = validateXML(Module);
|
nicholas@2224
|
258 console.log(xmllint);
|
nicholas@2498
|
259 if (xmllint != 'document.xml validates\n') {
|
nicholas@2224
|
260 document.getElementsByTagName('body')[0].innerHTML = null;
|
nicholas@2708
|
261 msg = document.createElement("h3");
|
nicholas@2224
|
262 msg.textContent = "FATAL ERROR";
|
nicholas@2708
|
263 span = document.createElement("h4");
|
nicholas@2224
|
264 span.textContent = "The XML validator returned the following errors when decoding your XML file";
|
nicholas@2224
|
265 document.getElementsByTagName('body')[0].appendChild(msg);
|
nicholas@2224
|
266 document.getElementsByTagName('body')[0].appendChild(span);
|
nicholas@2224
|
267 xmllint = xmllint.split('\n');
|
nicholas@2498
|
268 for (var i in xmllint) {
|
nicholas@2224
|
269 document.getElementsByTagName('body')[0].appendChild(document.createElement('br'));
|
nicholas@2708
|
270 span = document.createElement("span");
|
nicholas@2224
|
271 span.textContent = xmllint[i];
|
nicholas@2224
|
272 document.getElementsByTagName('body')[0].appendChild(span);
|
nicholas@2224
|
273 }
|
nicholas@2224
|
274 return;
|
nicholas@2224
|
275 }
|
nicholas@2224
|
276 // Build the specification
|
nicholas@2498
|
277 specification.decode(projectXML);
|
nicholas@2224
|
278 // Generate the session-key
|
nicholas@2224
|
279 storage.initialise();
|
nicholas@2498
|
280
|
nicholas@2247
|
281 } else if (responseDocument.firstChild.nodeName == "waetresult") {
|
nicholas@2224
|
282 // document is a result
|
nicholas@2498
|
283 projectXML = document.implementation.createDocument(null, "waet");
|
nicholas@2294
|
284 projectXML.firstChild.appendChild(responseDocument.getElementsByTagName('waet')[0].getElementsByTagName("setup")[0].cloneNode(true));
|
nicholas@2708
|
285 var child = responseDocument.firstChild.firstChild,
|
nicholas@2708
|
286 copy;
|
nicholas@2708
|
287 while (child !== null) {
|
nicholas@2224
|
288 if (child.nodeName == "survey") {
|
nicholas@2224
|
289 // One of the global survey elements
|
nicholas@2224
|
290 if (child.getAttribute("state") == "complete") {
|
nicholas@2224
|
291 // We need to remove this survey from <setup>
|
nicholas@2224
|
292 var location = child.getAttribute("location");
|
nicholas@2224
|
293 var globalSurveys = projectXML.getElementsByTagName("setup")[0].getElementsByTagName("survey")[0];
|
nicholas@2708
|
294 while (globalSurveys !== null) {
|
nicholas@2224
|
295 if (location == "pre" || location == "before") {
|
nicholas@2224
|
296 if (globalSurveys.getAttribute("location") == "pre" || globalSurveys.getAttribute("location") == "before") {
|
nicholas@2224
|
297 projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
|
nicholas@2224
|
298 break;
|
nicholas@2224
|
299 }
|
nicholas@2224
|
300 } else {
|
nicholas@2224
|
301 if (globalSurveys.getAttribute("location") == "post" || globalSurveys.getAttribute("location") == "after") {
|
nicholas@2224
|
302 projectXML.getElementsByTagName("setup")[0].removeChild(globalSurveys);
|
nicholas@2224
|
303 break;
|
nicholas@2224
|
304 }
|
nicholas@2224
|
305 }
|
nicholas@2224
|
306 globalSurveys = globalSurveys.nextElementSibling;
|
nicholas@2224
|
307 }
|
nicholas@2224
|
308 } else {
|
nicholas@2224
|
309 // We need to complete this, so it must be regenerated by store
|
nicholas@2708
|
310 copy = child;
|
nicholas@2224
|
311 child = child.previousElementSibling;
|
nicholas@2294
|
312 responseDocument.firstChild.removeChild(copy);
|
nicholas@2224
|
313 }
|
nicholas@2224
|
314 } else if (child.nodeName == "page") {
|
nicholas@2224
|
315 if (child.getAttribute("state") == "empty") {
|
nicholas@2224
|
316 // We need to complete this page
|
nicholas@2294
|
317 projectXML.firstChild.appendChild(responseDocument.getElementById(child.getAttribute("ref")).cloneNode(true));
|
nicholas@2708
|
318 copy = child;
|
nicholas@2224
|
319 child = child.previousElementSibling;
|
nicholas@2294
|
320 responseDocument.firstChild.removeChild(copy);
|
nicholas@2224
|
321 }
|
nicholas@2224
|
322 }
|
nicholas@2224
|
323 child = child.nextElementSibling;
|
nicholas@2224
|
324 }
|
nicholas@2224
|
325 // Build the specification
|
nicholas@2498
|
326 specification.decode(projectXML);
|
nicholas@2224
|
327 // Use the original
|
nicholas@2224
|
328 storage.initialise(responseDocument);
|
nicholas@2224
|
329 }
|
nicholas@2498
|
330 /// CHECK FOR SAMPLE RATE COMPATIBILITY
|
nicholas@2708
|
331 if (isFinite(specification.sampleRate)) {
|
nicholas@2498
|
332 if (Number(specification.sampleRate) != audioContext.sampleRate) {
|
nicholas@2498
|
333 var errStr = 'Sample rates do not match! Requested ' + Number(specification.sampleRate) + ', got ' + audioContext.sampleRate + '. Please set the sample rate to match before completing this test.';
|
nicholas@2498
|
334 interfaceContext.lightbox.post("Error", errStr);
|
nicholas@2498
|
335 return;
|
nicholas@2498
|
336 }
|
nicholas@2498
|
337 }
|
nicholas@2498
|
338
|
nicholas@2624
|
339 var getInterfaces = new XMLHttpRequest();
|
nicholas@2624
|
340 getInterfaces.open("GET", "interfaces/interfaces.json");
|
nicholas@2624
|
341 getInterfaces.onerror = function (e) {
|
nicholas@2624
|
342 throw (e);
|
nicholas@2708
|
343 };
|
nicholas@2624
|
344 getInterfaces.onload = function () {
|
nicholas@2624
|
345 if (getInterfaces.status !== 200) {
|
nicholas@2624
|
346 throw (new Error(getInterfaces.status));
|
nicholas@2624
|
347 }
|
nicholas@2624
|
348 // Get the current interface
|
nicholas@2624
|
349 var name = specification.interface,
|
nicholas@2624
|
350 head = document.getElementsByTagName("head")[0],
|
nicholas@2624
|
351 data = JSON.parse(getInterfaces.responseText),
|
nicholas@2624
|
352 interfaceObject = data.interfaces.find(function (e) {
|
nicholas@2624
|
353 return e.name == name;
|
nicholas@2624
|
354 });
|
nicholas@2624
|
355 if (!interfaceObject) {
|
nicholas@2624
|
356 throw ("Cannot load desired interface");
|
nicholas@2624
|
357 }
|
nicholas@2624
|
358 interfaceObject.scripts.forEach(function (v) {
|
nicholas@2624
|
359 var script = document.createElement("script");
|
nicholas@2624
|
360 script.setAttribute("type", "text/javascript");
|
nicholas@2624
|
361 script.setAttribute("src", v);
|
nicholas@2624
|
362 head.appendChild(script);
|
nicholas@2624
|
363 });
|
nicholas@2624
|
364 interfaceObject.css.forEach(function (v) {
|
nicholas@2624
|
365 var css = document.createElement("link");
|
nicholas@2624
|
366 css.setAttribute("rel", "stylesheet");
|
nicholas@2624
|
367 css.setAttribute("type", "text/css");
|
nicholas@2624
|
368 css.setAttribute("href", v);
|
nicholas@2624
|
369 head.appendChild(css);
|
nicholas@2624
|
370 });
|
nicholas@2708
|
371 };
|
nicholas@2624
|
372 getInterfaces.send();
|
nicholas@2498
|
373
|
nicholas@2708
|
374 if (gReturnURL !== undefined) {
|
nicholas@2498
|
375 console.log("returnURL Overide from " + specification.returnURL + " to " + gReturnURL);
|
nicholas@2329
|
376 specification.returnURL = gReturnURL;
|
nicholas@2329
|
377 }
|
nicholas@2708
|
378 if (gSaveFilenamePrefix !== undefined) {
|
giuliomoro@2337
|
379 specification.saveFilenamePrefix = gSaveFilenamePrefix;
|
giuliomoro@2337
|
380 }
|
nicholas@2498
|
381
|
nicholas@2498
|
382 // Create the audio engine object
|
nicholas@2498
|
383 audioEngineContext = new AudioEngine(specification);
|
nicholas@2224
|
384 }
|
nicholas@2224
|
385
|
nicholas@2224
|
386 function createProjectSave(destURL) {
|
nicholas@2224
|
387 // Clear the window.onbeforeunload
|
nicholas@2224
|
388 window.onbeforeunload = null;
|
nicholas@2498
|
389 // Save the data from interface into XML and send to destURL
|
nicholas@2498
|
390 // If destURL is null then download XML in client
|
nicholas@2498
|
391 // Now time to render file locally
|
nicholas@2733
|
392 var xmlDoc = storage.finish();
|
nicholas@2498
|
393 var parent = document.createElement("div");
|
nicholas@2498
|
394 parent.appendChild(xmlDoc);
|
nicholas@2498
|
395 var file = [parent.innerHTML];
|
nicholas@2498
|
396 if (destURL == "local") {
|
nicholas@2498
|
397 var bb = new Blob(file, {
|
nicholas@2498
|
398 type: 'application/xml'
|
nicholas@2498
|
399 });
|
nicholas@2498
|
400 var dnlk = window.URL.createObjectURL(bb);
|
nicholas@2498
|
401 var a = document.createElement("a");
|
nicholas@2498
|
402 a.hidden = '';
|
nicholas@2498
|
403 a.href = dnlk;
|
nicholas@2498
|
404 a.download = "save.xml";
|
nicholas@2498
|
405 a.textContent = "Save File";
|
nicholas@2498
|
406
|
nicholas@2498
|
407 popup.showPopup();
|
nicholas@2498
|
408 popup.popupContent.innerHTML = "<span>Please save the file below to give to your test supervisor</span><br>";
|
nicholas@2498
|
409 popup.popupContent.appendChild(a);
|
nicholas@2498
|
410 } else {
|
nicholas@2498
|
411 var projectReturn = "";
|
nicholas@2498
|
412 if (typeof specification.projectReturn == "string") {
|
nicholas@2498
|
413 if (specification.projectReturn.substr(0, 4) == "http") {
|
nicholas@2498
|
414 projectReturn = specification.projectReturn;
|
nicholas@2498
|
415 }
|
nicholas@2498
|
416 }
|
nicholas@2723
|
417 storage.SessionKey.finish().then(function (resolved) {
|
nicholas@2723
|
418 if (typeof specification.returnURL == "string" && specification.returnURL.length > 0) {
|
nicholas@2723
|
419 window.location = specification.returnURL;
|
nicholas@2723
|
420 } else {
|
nicholas@2723
|
421 popup.popupContent.textContent = specification.exitText;
|
nicholas@2723
|
422 }
|
nicholas@2723
|
423 }, function (message) {
|
nicholas@2723
|
424 console.log("Save: Error! " + message.textContent);
|
nicholas@2498
|
425 createProjectSave("local");
|
nicholas@2723
|
426 });
|
nicholas@2498
|
427 popup.showPopup();
|
nicholas@2498
|
428 popup.popupContent.innerHTML = null;
|
nicholas@2498
|
429 popup.popupContent.textContent = "Submitting. Please Wait";
|
nicholas@2498
|
430 if (typeof (popup.hideNextButton) === "function") {
|
nicholas@2498
|
431 popup.hideNextButton();
|
nicholas@2498
|
432 }
|
nicholas@2498
|
433 if (typeof (popup.hidePreviousButton) === "function") {
|
nicholas@2498
|
434 popup.hidePreviousButton();
|
nicholas@2498
|
435 }
|
nicholas@2498
|
436 }
|
nicholas@2224
|
437 }
|
nicholas@2224
|
438
|
nicholas@2498
|
439 function errorSessionDump(msg) {
|
nicholas@2498
|
440 // Create the partial interface XML save
|
nicholas@2498
|
441 // Include error node with message on why the dump occured
|
nicholas@2498
|
442 popup.showPopup();
|
nicholas@2498
|
443 popup.popupContent.innerHTML = null;
|
nicholas@2498
|
444 var err = document.createElement('error');
|
nicholas@2498
|
445 var parent = document.createElement("div");
|
nicholas@2498
|
446 if (typeof msg === "object") {
|
nicholas@2498
|
447 err.appendChild(msg);
|
nicholas@2498
|
448 popup.popupContent.appendChild(msg);
|
nicholas@2498
|
449
|
nicholas@2498
|
450 } else {
|
nicholas@2498
|
451 err.textContent = msg;
|
nicholas@2498
|
452 popup.popupContent.innerHTML = "ERROR : " + msg;
|
nicholas@2498
|
453 }
|
nicholas@2498
|
454 var xmlDoc = interfaceXMLSave();
|
nicholas@2498
|
455 xmlDoc.appendChild(err);
|
nicholas@2498
|
456 parent.appendChild(xmlDoc);
|
nicholas@2498
|
457 var file = [parent.innerHTML];
|
nicholas@2498
|
458 var bb = new Blob(file, {
|
nicholas@2498
|
459 type: 'application/xml'
|
nicholas@2498
|
460 });
|
nicholas@2498
|
461 var dnlk = window.URL.createObjectURL(bb);
|
nicholas@2498
|
462 var a = document.createElement("a");
|
nicholas@2498
|
463 a.hidden = '';
|
nicholas@2498
|
464 a.href = dnlk;
|
nicholas@2498
|
465 a.download = "save.xml";
|
nicholas@2498
|
466 a.textContent = "Save File";
|
nicholas@2498
|
467
|
nicholas@2498
|
468
|
nicholas@2498
|
469
|
nicholas@2498
|
470 popup.popupContent.appendChild(a);
|
nicholas@2224
|
471 }
|
nicholas@2224
|
472
|
nicholas@2224
|
473 // Only other global function which must be defined in the interface class. Determines how to create the XML document.
|
nicholas@2498
|
474 function interfaceXMLSave() {
|
nicholas@2498
|
475 // Create the XML string to be exported with results
|
nicholas@2498
|
476 return storage.finish();
|
nicholas@2224
|
477 }
|
nicholas@2224
|
478
|
nicholas@2498
|
479 function linearToDecibel(gain) {
|
nicholas@2498
|
480 return 20.0 * Math.log10(gain);
|
nicholas@2224
|
481 }
|
nicholas@2224
|
482
|
nicholas@2498
|
483 function decibelToLinear(gain) {
|
nicholas@2498
|
484 return Math.pow(10, gain / 20.0);
|
nicholas@2224
|
485 }
|
nicholas@2224
|
486
|
nicholas@2498
|
487 function secondsToSamples(time, fs) {
|
nicholas@2498
|
488 return Math.round(time * fs);
|
nicholas@2224
|
489 }
|
nicholas@2224
|
490
|
nicholas@2498
|
491 function samplesToSeconds(samples, fs) {
|
nicholas@2224
|
492 return samples / fs;
|
nicholas@2224
|
493 }
|
nicholas@2224
|
494
|
nicholas@2224
|
495 function randomString(length) {
|
nicholas@2708
|
496 var str = "";
|
nicholas@2498
|
497 for (var i = 0; i < length; i += 2) {
|
nicholas@2498
|
498 var num = Math.floor(Math.random() * 1295);
|
nicholas@2376
|
499 str += num.toString(36);
|
nicholas@2376
|
500 }
|
nicholas@2376
|
501 return str;
|
nicholas@2376
|
502 //return Math.round((Math.pow(36, length + 1) - Math.random() * Math.pow(36, length))).toString(36).slice(1);
|
nicholas@2224
|
503 }
|
nicholas@2224
|
504
|
nicholas@2498
|
505 function randomiseOrder(input) {
|
nicholas@2498
|
506 // This takes an array of information and randomises the order
|
nicholas@2498
|
507 var N = input.length;
|
nicholas@2498
|
508
|
nicholas@2498
|
509 var inputSequence = []; // For safety purposes: keep track of randomisation
|
nicholas@2498
|
510 for (var counter = 0; counter < N; ++counter)
|
nicholas@2708
|
511 inputSequence.push(counter); // Fill array
|
nicholas@2498
|
512 var inputSequenceClone = inputSequence.slice(0);
|
nicholas@2498
|
513
|
nicholas@2498
|
514 var holdArr = [];
|
nicholas@2498
|
515 var outputSequence = [];
|
nicholas@2498
|
516 for (var n = 0; n < N; n++) {
|
nicholas@2498
|
517 // First pick a random number
|
nicholas@2498
|
518 var r = Math.random();
|
nicholas@2498
|
519 // Multiply and floor by the number of elements left
|
nicholas@2498
|
520 r = Math.floor(r * input.length);
|
nicholas@2498
|
521 // Pick out that element and delete from the array
|
nicholas@2498
|
522 holdArr.push(input.splice(r, 1)[0]);
|
nicholas@2498
|
523 // Do the same with sequence
|
nicholas@2498
|
524 outputSequence.push(inputSequence.splice(r, 1)[0]);
|
nicholas@2498
|
525 }
|
nicholas@2498
|
526 console.log(inputSequenceClone.toString()); // print original array to console
|
nicholas@2498
|
527 console.log(outputSequence.toString()); // print randomised array to console
|
nicholas@2498
|
528 return holdArr;
|
nicholas@2224
|
529 }
|
nicholas@2224
|
530
|
nicholas@2498
|
531 function randomSubArray(array, num) {
|
nicholas@2224
|
532 if (num > array.length) {
|
nicholas@2224
|
533 num = array.length;
|
nicholas@2224
|
534 }
|
nicholas@2224
|
535 var ret = [];
|
nicholas@2224
|
536 while (num > 0) {
|
nicholas@2224
|
537 var index = Math.floor(Math.random() * array.length);
|
nicholas@2498
|
538 ret.push(array.splice(index, 1)[0]);
|
nicholas@2224
|
539 num--;
|
nicholas@2224
|
540 }
|
nicholas@2224
|
541 return ret;
|
nicholas@2224
|
542 }
|
nicholas@2224
|
543
|
nicholas@2224
|
544 function interfacePopup() {
|
nicholas@2498
|
545 // Creates an object to manage the popup
|
nicholas@2498
|
546 this.popup = null;
|
nicholas@2498
|
547 this.popupContent = null;
|
nicholas@2498
|
548 this.popupTitle = null;
|
nicholas@2498
|
549 this.popupResponse = null;
|
nicholas@2498
|
550 this.buttonProceed = null;
|
nicholas@2498
|
551 this.buttonPrevious = null;
|
nicholas@2498
|
552 this.popupOptions = null;
|
nicholas@2498
|
553 this.currentIndex = null;
|
nicholas@2498
|
554 this.node = null;
|
nicholas@2498
|
555 this.store = null;
|
nicholas@2775
|
556 var lastNodeStart;
|
nicholas@2498
|
557 $(window).keypress(function (e) {
|
n@2915
|
558 if (e.keyCode == 13 && popup.popup.style.visibility == 'visible' && interfaceContext.lightbox.isVisible() === false) {
|
nicholas@2498
|
559 console.log(e);
|
nicholas@2498
|
560 popup.buttonProceed.onclick();
|
nicholas@2498
|
561 e.preventDefault();
|
nicholas@2498
|
562 }
|
nicholas@2498
|
563 });
|
nicholas@2708
|
564 // Generators & Processors //
|
nicholas@2708
|
565
|
nicholas@2708
|
566 function processConditional(node, value) {
|
nicholas@2708
|
567 function jumpToId(jumpID) {
|
nicholas@2708
|
568 var index = this.popupOptions.findIndex(function (item, index, element) {
|
nicholas@2708
|
569 if (item.specification.id == jumpID) {
|
nicholas@2708
|
570 return true;
|
nicholas@2708
|
571 } else {
|
nicholas@2708
|
572 return false;
|
nicholas@2708
|
573 }
|
nicholas@2708
|
574 }, this);
|
nicholas@2708
|
575 this.currentIndex = index - 1;
|
nicholas@2708
|
576 }
|
nicholas@2708
|
577 var conditionFunction;
|
nicholas@2708
|
578 if (node.specification.type === "question") {
|
nicholas@2708
|
579 conditionFunction = processQuestionConditional;
|
nicholas@2708
|
580 } else if (node.specification.type === "checkbox") {
|
nicholas@2708
|
581 conditionFunction = processCheckboxConditional;
|
nicholas@2708
|
582 } else if (node.specification.type === "radio") {
|
nicholas@2708
|
583 conditionFunction = processRadioConditional;
|
nicholas@2708
|
584 } else if (node.specification.type === "number") {
|
nicholas@2708
|
585 conditionFunction = processNumberConditional;
|
nicholas@2708
|
586 } else if (node.specification.type === "slider") {
|
nicholas@2708
|
587 conditionFunction = processSliderConditional;
|
nicholas@2708
|
588 } else {
|
nicholas@2708
|
589 return;
|
nicholas@2708
|
590 }
|
nicholas@2708
|
591 for (var i = 0; i < node.specification.conditions.length; i++) {
|
nicholas@2708
|
592 var condition = node.specification.conditions[i];
|
nicholas@2708
|
593 var pass = conditionFunction(condition, value);
|
nicholas@2708
|
594 var jumpID;
|
nicholas@2708
|
595 if (pass) {
|
nicholas@2708
|
596 jumpID = condition.jumpToOnPass;
|
nicholas@2708
|
597 } else {
|
nicholas@2708
|
598 jumpID = condition.jumpToOnFail;
|
nicholas@2708
|
599 }
|
nicholas@2735
|
600 if (jumpID !== null) {
|
nicholas@2708
|
601 jumpToId.call(this, jumpID);
|
nicholas@2708
|
602 break;
|
nicholas@2708
|
603 }
|
nicholas@2708
|
604 }
|
nicholas@2708
|
605 }
|
nicholas@2708
|
606
|
nicholas@2708
|
607 function postQuestion(node) {
|
nicholas@2708
|
608 var textArea = document.createElement('textarea');
|
nicholas@2708
|
609 switch (node.specification.boxsize) {
|
nicholas@2708
|
610 case 'small':
|
nicholas@2708
|
611 textArea.cols = "20";
|
nicholas@2708
|
612 textArea.rows = "1";
|
nicholas@2708
|
613 break;
|
nicholas@2708
|
614 case 'normal':
|
nicholas@2708
|
615 textArea.cols = "30";
|
nicholas@2708
|
616 textArea.rows = "2";
|
nicholas@2708
|
617 break;
|
nicholas@2708
|
618 case 'large':
|
nicholas@2708
|
619 textArea.cols = "40";
|
nicholas@2708
|
620 textArea.rows = "5";
|
nicholas@2708
|
621 break;
|
nicholas@2708
|
622 case 'huge':
|
nicholas@2708
|
623 textArea.cols = "50";
|
nicholas@2708
|
624 textArea.rows = "10";
|
nicholas@2708
|
625 break;
|
nicholas@2708
|
626 }
|
nicholas@2708
|
627 if (node.response === undefined) {
|
nicholas@2708
|
628 node.response = "";
|
nicholas@2708
|
629 } else {
|
nicholas@2708
|
630 textArea.value = node.response;
|
nicholas@2708
|
631 }
|
nicholas@2708
|
632 this.popupResponse.appendChild(textArea);
|
nicholas@2708
|
633 textArea.focus();
|
nicholas@2708
|
634 this.popupResponse.style.textAlign = "center";
|
nicholas@2708
|
635 this.popupResponse.style.left = "0%";
|
nicholas@2708
|
636 }
|
nicholas@2708
|
637
|
nicholas@2708
|
638 function processQuestionConditional(condition, value) {
|
nicholas@2708
|
639 switch (condition.check) {
|
nicholas@2708
|
640 case "equals":
|
nicholas@2708
|
641 // Deliberately loose check
|
nicholas@2708
|
642 if (value == condition.value) {
|
nicholas@2708
|
643 return true;
|
nicholas@2708
|
644 }
|
nicholas@2708
|
645 break;
|
nicholas@2708
|
646 case "greaterThan":
|
nicholas@2708
|
647 case "lessThan":
|
nicholas@2708
|
648 console.log("Survey Element of type 'question' cannot interpret greaterThan/lessThan conditions. IGNORING");
|
nicholas@2708
|
649 break;
|
nicholas@2708
|
650 case "contains":
|
nicholas@2708
|
651 if (value.includes(condition.value)) {
|
nicholas@2708
|
652 return true;
|
nicholas@2708
|
653 }
|
nicholas@2708
|
654 break;
|
nicholas@2708
|
655 }
|
nicholas@2708
|
656 return false;
|
nicholas@2708
|
657 }
|
nicholas@2708
|
658
|
nicholas@2708
|
659 function processQuestion(node) {
|
nicholas@2708
|
660 var textArea = this.popupResponse.getElementsByTagName("textarea")[0];
|
nicholas@2708
|
661 if (node.specification.mandatory === true && textArea.value.length === 0) {
|
nicholas@2708
|
662 interfaceContext.lightbox.post("Error", "This question is mandatory");
|
nicholas@2708
|
663 return false;
|
nicholas@2708
|
664 }
|
nicholas@2708
|
665 // Save the text content
|
nicholas@2708
|
666 console.log("Question: " + node.specification.statement);
|
nicholas@2708
|
667 console.log("Question Response: " + textArea.value);
|
nicholas@2708
|
668 node.response = textArea.value;
|
nicholas@2708
|
669 processConditional.call(this, node, textArea.value);
|
nicholas@2708
|
670 return true;
|
nicholas@2708
|
671 }
|
nicholas@2708
|
672
|
nicholas@2708
|
673 function postCheckbox(node) {
|
n@2918
|
674 if (node.response === undefined) {
|
n@2918
|
675 node.response = Array(node.specification.options.length);
|
nicholas@2708
|
676 }
|
nicholas@2708
|
677 var table = document.createElement("table");
|
nicholas@2708
|
678 table.className = "popup-option-list";
|
nicholas@2708
|
679 table.border = "0";
|
n@2918
|
680 node.response = [];
|
n@2924
|
681 var nodelist = [];
|
nicholas@2708
|
682 node.specification.options.forEach(function (option, index) {
|
nicholas@2708
|
683 var tr = document.createElement("tr");
|
n@2924
|
684 nodelist.push(tr);
|
nicholas@2708
|
685 var td = document.createElement("td");
|
nicholas@2708
|
686 tr.appendChild(td);
|
nicholas@2708
|
687 var input = document.createElement('input');
|
nicholas@2708
|
688 input.id = option.name;
|
nicholas@2708
|
689 input.type = 'checkbox';
|
nicholas@2708
|
690 td.appendChild(input);
|
nicholas@2708
|
691
|
nicholas@2708
|
692 td = document.createElement("td");
|
nicholas@2708
|
693 tr.appendChild(td);
|
nicholas@2708
|
694 var span = document.createElement('span');
|
nicholas@2708
|
695 span.textContent = option.text;
|
nicholas@2708
|
696 td.appendChild(span);
|
nicholas@2708
|
697 tr = document.createElement('div');
|
nicholas@2708
|
698 tr.setAttribute('name', 'option');
|
nicholas@2708
|
699 tr.className = "popup-option-checbox";
|
nicholas@2708
|
700 if (node.response[index] !== undefined) {
|
nicholas@2708
|
701 if (node.response[index].checked === true) {
|
nicholas@2708
|
702 input.checked = "true";
|
nicholas@2708
|
703 }
|
nicholas@2708
|
704 }
|
nicholas@2708
|
705 index++;
|
nicholas@2708
|
706 });
|
n@2924
|
707 if (node.specification.randomise) {
|
n@2924
|
708 nodelist = randomiseOrder(nodelist);
|
n@2924
|
709 }
|
n@2924
|
710 nodelist.forEach(function (e) {
|
n@2924
|
711 table.appendChild(e);
|
n@2924
|
712 });
|
nicholas@2708
|
713 this.popupResponse.appendChild(table);
|
nicholas@2708
|
714 }
|
nicholas@2708
|
715
|
nicholas@2708
|
716 function processCheckbox(node) {
|
nicholas@2708
|
717 console.log("Checkbox: " + node.specification.statement);
|
nicholas@2708
|
718 var inputs = this.popupResponse.getElementsByTagName('input');
|
nicholas@2708
|
719 node.response = [];
|
nicholas@2708
|
720 var numChecked = 0,
|
nicholas@2708
|
721 i;
|
nicholas@2708
|
722 for (i = 0; i < node.specification.options.length; i++) {
|
nicholas@2708
|
723 if (inputs[i].checked) {
|
nicholas@2708
|
724 numChecked++;
|
nicholas@2708
|
725 }
|
nicholas@2708
|
726 }
|
nicholas@2708
|
727 if (node.specification.min !== undefined) {
|
nicholas@2708
|
728 if (node.specification.max === undefined) {
|
nicholas@2708
|
729 if (numChecked < node.specification.min) {
|
nicholas@2708
|
730 var msg = "You must select at least " + node.specification.min + " option";
|
nicholas@2708
|
731 if (node.specification.min > 1) {
|
nicholas@2708
|
732 msg += "s";
|
nicholas@2708
|
733 }
|
nicholas@2708
|
734 interfaceContext.lightbox.post("Error", msg);
|
nicholas@2708
|
735 return;
|
nicholas@2708
|
736 }
|
nicholas@2708
|
737 } else {
|
nicholas@2708
|
738 if (numChecked < node.specification.min || numChecked > node.specification.max) {
|
nicholas@2708
|
739 if (node.specification.min == node.specification.max) {
|
nicholas@2708
|
740 interfaceContext.lightbox.post("Error", "You must only select " + node.specification.min);
|
nicholas@2708
|
741 } else {
|
nicholas@2708
|
742 interfaceContext.lightbox.post("Error", "You must select between " + node.specification.min + " and " + node.specification.max);
|
nicholas@2708
|
743 }
|
nicholas@2708
|
744 return false;
|
nicholas@2708
|
745 }
|
nicholas@2708
|
746 }
|
nicholas@2708
|
747 }
|
nicholas@2708
|
748 for (i = 0; i < node.specification.options.length; i++) {
|
nicholas@2708
|
749 node.response.push({
|
nicholas@2708
|
750 name: node.specification.options[i].name,
|
nicholas@2708
|
751 text: node.specification.options[i].text,
|
nicholas@2708
|
752 checked: inputs[i].checked
|
nicholas@2708
|
753 });
|
nicholas@2708
|
754 console.log(node.specification.options[i].name + ": " + inputs[i].checked);
|
nicholas@2708
|
755 }
|
nicholas@2708
|
756 processConditional.call(this, node, node.response);
|
nicholas@2708
|
757 return true;
|
nicholas@2708
|
758 }
|
nicholas@2708
|
759
|
nicholas@2708
|
760 function processCheckboxConditional(condition, response) {
|
nicholas@2708
|
761 switch (condition.check) {
|
nicholas@2708
|
762 case "contains":
|
nicholas@2708
|
763 for (var i = 0; i < response.length; i++) {
|
nicholas@2708
|
764 var value = response[i];
|
nicholas@2708
|
765 if (value.name === condition.value && value.checked) {
|
nicholas@2708
|
766 return true;
|
nicholas@2708
|
767 }
|
nicholas@2708
|
768 }
|
nicholas@2708
|
769 break;
|
nicholas@2708
|
770 case "equals":
|
nicholas@2708
|
771 case "greaterThan":
|
nicholas@2708
|
772 case "lessThan":
|
nicholas@2708
|
773 console.log("Survey Element of type 'checkbox' cannot interpret equals/greaterThan/lessThan conditions. IGNORING");
|
nicholas@2708
|
774 break;
|
nicholas@2708
|
775 default:
|
nicholas@2708
|
776 console.log("Unknown condition. IGNORING");
|
nicholas@2708
|
777 break;
|
nicholas@2708
|
778 }
|
nicholas@2708
|
779 return false;
|
nicholas@2708
|
780 }
|
nicholas@2708
|
781
|
nicholas@2708
|
782 function postRadio(node) {
|
nicholas@2708
|
783 if (node.response === null) {
|
nicholas@2708
|
784 node.response = {
|
nicholas@2708
|
785 name: "",
|
nicholas@2708
|
786 text: ""
|
nicholas@2708
|
787 };
|
nicholas@2708
|
788 }
|
nicholas@2708
|
789 var table = document.createElement("table");
|
nicholas@2708
|
790 table.className = "popup-option-list";
|
nicholas@2708
|
791 table.border = "0";
|
nicholas@2708
|
792 if (node.response === null || node.response.length === 0) {
|
nicholas@2708
|
793 node.response = [];
|
nicholas@2708
|
794 }
|
nicholas@2708
|
795 node.specification.options.forEach(function (option, index) {
|
nicholas@2708
|
796 var tr = document.createElement("tr");
|
nicholas@2708
|
797 table.appendChild(tr);
|
nicholas@2708
|
798 var td = document.createElement("td");
|
nicholas@2708
|
799 tr.appendChild(td);
|
nicholas@2708
|
800 var input = document.createElement('input');
|
nicholas@2708
|
801 input.id = option.name;
|
nicholas@2708
|
802 input.type = 'radio';
|
nicholas@2708
|
803 input.name = node.specification.id;
|
nicholas@2708
|
804 td.appendChild(input);
|
nicholas@2708
|
805
|
nicholas@2708
|
806 td = document.createElement("td");
|
nicholas@2708
|
807 tr.appendChild(td);
|
nicholas@2708
|
808 var span = document.createElement('span');
|
nicholas@2708
|
809 span.textContent = option.text;
|
nicholas@2708
|
810 td.appendChild(span);
|
nicholas@2708
|
811 tr = document.createElement('div');
|
nicholas@2708
|
812 tr.setAttribute('name', 'option');
|
nicholas@2708
|
813 tr.className = "popup-option-checbox";
|
nicholas@2708
|
814 table.appendChild(tr);
|
nicholas@2708
|
815 });
|
nicholas@2708
|
816 this.popupResponse.appendChild(table);
|
nicholas@2708
|
817 }
|
nicholas@2708
|
818
|
nicholas@2708
|
819 function processRadio(node) {
|
nicholas@2708
|
820 var optHold = this.popupResponse;
|
nicholas@2708
|
821 console.log("Radio: " + node.specification.statement);
|
nicholas@2708
|
822 node.response = null;
|
nicholas@2708
|
823 var i = 0;
|
nicholas@2708
|
824 var inputs = optHold.getElementsByTagName('input');
|
nicholas@2708
|
825 while (node.response === null) {
|
nicholas@2708
|
826 if (i == inputs.length) {
|
nicholas@2708
|
827 if (node.specification.mandatory === true) {
|
nicholas@2708
|
828 interfaceContext.lightbox.post("Error", "Please select one option");
|
nicholas@2708
|
829 return false;
|
nicholas@2708
|
830 }
|
nicholas@2708
|
831 break;
|
nicholas@2708
|
832 }
|
nicholas@2708
|
833 if (inputs[i].checked === true) {
|
nicholas@2708
|
834 node.response = node.specification.options[i];
|
nicholas@2708
|
835 console.log("Selected: " + node.specification.options[i].name);
|
nicholas@2708
|
836 }
|
nicholas@2708
|
837 i++;
|
nicholas@2708
|
838 }
|
nicholas@2735
|
839 processConditional.call(this, node, node.response.name);
|
nicholas@2708
|
840 return true;
|
nicholas@2708
|
841 }
|
nicholas@2708
|
842
|
nicholas@2708
|
843 function processRadioConditional(condition, response) {
|
nicholas@2708
|
844 switch (condition.check) {
|
nicholas@2708
|
845 case "equals":
|
nicholas@2708
|
846 if (response === condition.value) {
|
nicholas@2708
|
847 return true;
|
nicholas@2708
|
848 }
|
nicholas@2708
|
849 break;
|
nicholas@2708
|
850 case "contains":
|
nicholas@2708
|
851 case "greaterThan":
|
nicholas@2708
|
852 case "lessThan":
|
nicholas@2708
|
853 console.log("Survey Element of type 'radio' cannot interpret contains/greaterThan/lessThan conditions. IGNORING");
|
nicholas@2708
|
854 break;
|
nicholas@2708
|
855 default:
|
nicholas@2708
|
856 console.log("Unknown condition. IGNORING");
|
nicholas@2708
|
857 break;
|
nicholas@2708
|
858 }
|
nicholas@2708
|
859 return false;
|
nicholas@2708
|
860 }
|
nicholas@2708
|
861
|
nicholas@2708
|
862 function postNumber(node) {
|
nicholas@2708
|
863 var input = document.createElement('input');
|
nicholas@2708
|
864 input.type = 'textarea';
|
nicholas@2708
|
865 if (node.specification.min !== null) {
|
nicholas@2708
|
866 input.min = node.specification.min;
|
nicholas@2708
|
867 }
|
nicholas@2708
|
868 if (node.specification.max !== null) {
|
nicholas@2708
|
869 input.max = node.specification.max;
|
nicholas@2708
|
870 }
|
nicholas@2708
|
871 if (node.specification.step !== null) {
|
nicholas@2708
|
872 input.step = node.specification.step;
|
nicholas@2708
|
873 }
|
nicholas@2708
|
874 if (node.response !== undefined) {
|
nicholas@2708
|
875 input.value = node.response;
|
nicholas@2708
|
876 }
|
nicholas@2708
|
877 this.popupResponse.appendChild(input);
|
nicholas@2708
|
878 this.popupResponse.style.textAlign = "center";
|
nicholas@2708
|
879 this.popupResponse.style.left = "0%";
|
nicholas@2708
|
880 }
|
nicholas@2708
|
881
|
nicholas@2708
|
882 function processNumber(node) {
|
nicholas@2708
|
883 var input = this.popupContent.getElementsByTagName('input')[0];
|
nicholas@2730
|
884 if (node.specification.mandatory === true && input.value.length === 0) {
|
nicholas@2708
|
885 interfaceContext.lightbox.post("Error", 'This question is mandatory. Please enter a number');
|
nicholas@2708
|
886 return false;
|
nicholas@2708
|
887 }
|
nicholas@2708
|
888 var enteredNumber = Number(input.value);
|
nicholas@2708
|
889 if (isNaN(enteredNumber)) {
|
nicholas@2708
|
890 interfaceContext.lightbox.post("Error", 'Please enter a valid number');
|
nicholas@2708
|
891 return false;
|
nicholas@2708
|
892 }
|
nicholas@2730
|
893 if (enteredNumber < node.specification.min && node.specification.min !== null) {
|
nicholas@2730
|
894 interfaceContext.lightbox.post("Error", 'Number is below the minimum value of ' + node.specification.min);
|
nicholas@2708
|
895 return false;
|
nicholas@2708
|
896 }
|
nicholas@2730
|
897 if (enteredNumber > node.specification.max && node.specification.max !== null) {
|
nicholas@2730
|
898 interfaceContext.lightbox.post("Error", 'Number is above the maximum value of ' + node.specification.max);
|
nicholas@2708
|
899 return false;
|
nicholas@2708
|
900 }
|
nicholas@2708
|
901 node.response = input.value;
|
nicholas@2708
|
902 processConditional.call(this, node, node.response);
|
nicholas@2708
|
903 return true;
|
nicholas@2708
|
904 }
|
nicholas@2708
|
905
|
nicholas@2708
|
906 function processNumberConditional(condtion, value) {
|
nicholas@2708
|
907 var condition = condition;
|
nicholas@2708
|
908 switch (condition.check) {
|
nicholas@2708
|
909 case "greaterThan":
|
nicholas@2708
|
910 if (value > Number(condition.value)) {
|
nicholas@2708
|
911 return true;
|
nicholas@2708
|
912 }
|
nicholas@2708
|
913 break;
|
nicholas@2708
|
914 case "lessThan":
|
nicholas@2708
|
915 if (value < Number(condition.value)) {
|
nicholas@2708
|
916 return true;
|
nicholas@2708
|
917 }
|
nicholas@2708
|
918 break;
|
nicholas@2708
|
919 case "equals":
|
nicholas@2708
|
920 if (value == condition.value) {
|
nicholas@2708
|
921 return true;
|
nicholas@2708
|
922 }
|
nicholas@2708
|
923 break;
|
nicholas@2708
|
924 case "contains":
|
nicholas@2708
|
925 console.log("Survey Element of type 'number' cannot interpret \"contains\" conditions. IGNORING");
|
nicholas@2708
|
926 break;
|
nicholas@2708
|
927 default:
|
nicholas@2708
|
928 console.log("Unknown condition. IGNORING");
|
nicholas@2708
|
929 break;
|
nicholas@2708
|
930 }
|
nicholas@2708
|
931 return false;
|
nicholas@2708
|
932 }
|
nicholas@2708
|
933
|
nicholas@2708
|
934 function postVideo(node) {
|
nicholas@2708
|
935 var video = document.createElement("video");
|
nicholas@2708
|
936 video.src = node.specification.url;
|
nicholas@2708
|
937 this.popupResponse.appendChild(video);
|
nicholas@2708
|
938 }
|
nicholas@2708
|
939
|
nicholas@2708
|
940 function postYoutube(node) {
|
nicholas@2708
|
941 var iframe = document.createElement("iframe");
|
nicholas@2708
|
942 iframe.className = "youtube";
|
nicholas@2708
|
943 iframe.src = node.specification.url;
|
nicholas@2708
|
944 this.popupResponse.appendChild(iframe);
|
nicholas@2708
|
945 }
|
nicholas@2708
|
946
|
nicholas@2708
|
947 function postSlider(node) {
|
nicholas@2708
|
948 var hold = document.createElement('div');
|
nicholas@2708
|
949 var input = document.createElement('input');
|
nicholas@2708
|
950 input.type = 'range';
|
nicholas@2708
|
951 input.style.width = "90%";
|
nicholas@2708
|
952 if (node.specification.min !== null) {
|
nicholas@2708
|
953 input.min = node.specification.min;
|
nicholas@2708
|
954 }
|
nicholas@2708
|
955 if (node.specification.max !== null) {
|
nicholas@2708
|
956 input.max = node.specification.max;
|
nicholas@2708
|
957 }
|
nicholas@2708
|
958 if (node.response !== undefined) {
|
nicholas@2708
|
959 input.value = node.response;
|
nicholas@2708
|
960 }
|
nicholas@2708
|
961 hold.className = "survey-slider-text-holder";
|
nicholas@2708
|
962 var minText = document.createElement('span');
|
nicholas@2708
|
963 var maxText = document.createElement('span');
|
nicholas@2708
|
964 minText.textContent = node.specification.leftText;
|
nicholas@2708
|
965 maxText.textContent = node.specification.rightText;
|
nicholas@2708
|
966 hold.appendChild(minText);
|
nicholas@2708
|
967 hold.appendChild(maxText);
|
nicholas@2708
|
968 this.popupResponse.appendChild(input);
|
nicholas@2708
|
969 this.popupResponse.appendChild(hold);
|
nicholas@2708
|
970 this.popupResponse.style.textAlign = "center";
|
nicholas@2708
|
971 }
|
nicholas@2708
|
972
|
nicholas@2708
|
973 function processSlider(node) {
|
nicholas@2708
|
974 var input = this.popupContent.getElementsByTagName('input')[0];
|
nicholas@2708
|
975 node.response = input.value;
|
nicholas@2708
|
976 processConditional.call(this, node, node.response);
|
nicholas@2708
|
977 return true;
|
nicholas@2708
|
978 }
|
nicholas@2708
|
979
|
nicholas@2708
|
980 function processSliderConditional(condition, value) {
|
nicholas@2708
|
981 switch (condition.check) {
|
nicholas@2708
|
982 case "contains":
|
nicholas@2708
|
983 console.log("Survey Element of type 'number' cannot interpret contains conditions. IGNORING");
|
nicholas@2708
|
984 break;
|
nicholas@2708
|
985 case "greaterThan":
|
nicholas@2708
|
986 if (value > Number(condition.value)) {
|
nicholas@2708
|
987 return true;
|
nicholas@2708
|
988 }
|
nicholas@2708
|
989 break;
|
nicholas@2708
|
990 case "lessThan":
|
nicholas@2708
|
991 if (value < Number(condition.value)) {
|
nicholas@2708
|
992 return true;
|
nicholas@2708
|
993 }
|
nicholas@2708
|
994 break;
|
nicholas@2708
|
995 case "equals":
|
nicholas@2708
|
996 if (value == condition.value) {
|
nicholas@2708
|
997 return true;
|
nicholas@2708
|
998 }
|
nicholas@2708
|
999 break;
|
nicholas@2708
|
1000 default:
|
nicholas@2708
|
1001 console.log("Unknown condition. IGNORING");
|
nicholas@2708
|
1002 break;
|
nicholas@2708
|
1003 }
|
nicholas@2708
|
1004 return false;
|
nicholas@2708
|
1005 }
|
nicholas@2498
|
1006
|
nicholas@2498
|
1007 this.createPopup = function () {
|
nicholas@2498
|
1008 // Create popup window interface
|
nicholas@2498
|
1009 var insertPoint = document.getElementById("topLevelBody");
|
nicholas@2498
|
1010
|
nicholas@2498
|
1011 this.popup = document.getElementById('popupHolder');
|
nicholas@2498
|
1012 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
|
nicholas@2498
|
1013 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
|
nicholas@2498
|
1014
|
nicholas@2498
|
1015 this.popupContent = document.getElementById('popupContent');
|
nicholas@2498
|
1016
|
nicholas@2645
|
1017 this.popupTitle = document.getElementById('popupTitleHolder');
|
nicholas@2498
|
1018
|
nicholas@2498
|
1019 this.popupResponse = document.getElementById('popupResponse');
|
nicholas@2498
|
1020
|
nicholas@2498
|
1021 this.buttonProceed = document.getElementById('popup-proceed');
|
nicholas@2498
|
1022 this.buttonProceed.onclick = function () {
|
nicholas@2498
|
1023 popup.proceedClicked();
|
nicholas@2498
|
1024 };
|
nicholas@2498
|
1025
|
nicholas@2498
|
1026 this.buttonPrevious = document.getElementById('popup-previous');
|
nicholas@2498
|
1027 this.buttonPrevious.onclick = function () {
|
nicholas@2498
|
1028 popup.previousClick();
|
nicholas@2498
|
1029 };
|
nicholas@2498
|
1030
|
nicholas@2224
|
1031 this.hidePopup();
|
nicholas@2498
|
1032 this.popup.style.visibility = 'hidden';
|
nicholas@2498
|
1033 };
|
nicholas@2498
|
1034
|
nicholas@2498
|
1035 this.showPopup = function () {
|
nicholas@2708
|
1036 if (this.popup === null) {
|
nicholas@2498
|
1037 this.createPopup();
|
nicholas@2498
|
1038 }
|
nicholas@2498
|
1039 this.popup.style.visibility = 'visible';
|
nicholas@2498
|
1040 var blank = document.getElementsByClassName('testHalt')[0];
|
nicholas@2498
|
1041 blank.style.visibility = 'visible';
|
nicholas@2498
|
1042 this.popupResponse.style.left = "0%";
|
nicholas@2498
|
1043 };
|
nicholas@2498
|
1044
|
nicholas@2498
|
1045 this.hidePopup = function () {
|
nicholas@2224
|
1046 if (this.popup) {
|
nicholas@2224
|
1047 this.popup.style.visibility = 'hidden';
|
nicholas@2224
|
1048 var blank = document.getElementsByClassName('testHalt')[0];
|
nicholas@2224
|
1049 blank.style.visibility = 'hidden';
|
nicholas@2224
|
1050 this.buttonPrevious.style.visibility = 'inherit';
|
nicholas@2224
|
1051 }
|
nicholas@2498
|
1052 };
|
nicholas@2498
|
1053
|
nicholas@2498
|
1054 this.postNode = function () {
|
nicholas@2498
|
1055 // This will take the node from the popupOptions and display it
|
nicholas@2645
|
1056 var node = this.popupOptions[this.currentIndex],
|
nicholas@2646
|
1057 converter = new showdown.Converter(),
|
nicholas@2646
|
1058 p = new DOMParser();
|
nicholas@2774
|
1059 lastNodeStart = new Date();
|
nicholas@2498
|
1060 this.popupResponse.innerHTML = "";
|
nicholas@2648
|
1061 this.popupTitle.innerHTML = "";
|
nicholas@2647
|
1062 this.popupTitle.appendChild(p.parseFromString(converter.makeHtml(node.specification.statement), "text/html").getElementsByTagName("body")[0].firstElementChild);
|
nicholas@2498
|
1063 if (node.specification.type == 'question') {
|
nicholas@2708
|
1064 postQuestion.call(this, node);
|
nicholas@2498
|
1065 } else if (node.specification.type == 'checkbox') {
|
nicholas@2708
|
1066 postCheckbox.call(this, node);
|
nicholas@2498
|
1067 } else if (node.specification.type == 'radio') {
|
nicholas@2708
|
1068 postRadio.call(this, node);
|
nicholas@2498
|
1069 } else if (node.specification.type == 'number') {
|
nicholas@2708
|
1070 postNumber.call(this, node);
|
nicholas@2498
|
1071 } else if (node.specification.type == "video") {
|
nicholas@2708
|
1072 postVideo.call(this, node);
|
nicholas@2491
|
1073 } else if (node.specification.type == "youtube") {
|
nicholas@2708
|
1074 postYoutube.call(this, node);
|
n@2583
|
1075 } else if (node.specification.type == "slider") {
|
nicholas@2708
|
1076 postSlider.call(this, node);
|
nicholas@2491
|
1077 }
|
nicholas@2498
|
1078 if (this.currentIndex + 1 == this.popupOptions.length) {
|
nicholas@2498
|
1079 if (this.node.location == "pre") {
|
nicholas@2498
|
1080 this.buttonProceed.textContent = 'Start';
|
nicholas@2498
|
1081 } else {
|
nicholas@2498
|
1082 this.buttonProceed.textContent = 'Submit';
|
nicholas@2498
|
1083 }
|
nicholas@2498
|
1084 } else {
|
nicholas@2498
|
1085 this.buttonProceed.textContent = 'Next';
|
nicholas@2498
|
1086 }
|
nicholas@2498
|
1087 if (this.currentIndex > 0)
|
nicholas@2498
|
1088 this.buttonPrevious.style.visibility = 'visible';
|
nicholas@2498
|
1089 else
|
nicholas@2498
|
1090 this.buttonPrevious.style.visibility = 'hidden';
|
nicholas@2498
|
1091 };
|
nicholas@2498
|
1092
|
nicholas@2498
|
1093 this.initState = function (node, store) {
|
nicholas@2498
|
1094 //Call this with your preTest and postTest nodes when needed to
|
nicholas@2498
|
1095 // initialise the popup procedure.
|
nicholas@2498
|
1096 if (node.options.length > 0) {
|
nicholas@2498
|
1097 this.popupOptions = [];
|
nicholas@2498
|
1098 this.node = node;
|
nicholas@2498
|
1099 this.store = store;
|
nicholas@2708
|
1100 node.options.forEach(function (opt) {
|
nicholas@2498
|
1101 this.popupOptions.push({
|
nicholas@2498
|
1102 specification: opt,
|
nicholas@2498
|
1103 response: null
|
nicholas@2498
|
1104 });
|
nicholas@2708
|
1105 }, this);
|
nicholas@2498
|
1106 this.currentIndex = 0;
|
nicholas@2498
|
1107 this.showPopup();
|
nicholas@2498
|
1108 this.postNode();
|
nicholas@2498
|
1109 } else {
|
nicholas@2498
|
1110 advanceState();
|
nicholas@2498
|
1111 }
|
nicholas@2498
|
1112 };
|
nicholas@2498
|
1113
|
nicholas@2498
|
1114 this.proceedClicked = function () {
|
nicholas@2498
|
1115 // Each time the popup button is clicked!
|
nicholas@2708
|
1116 if (testState.stateIndex === 0 && specification.calibration) {
|
nicholas@2224
|
1117 interfaceContext.calibrationModuleObject.collect();
|
nicholas@2224
|
1118 advanceState();
|
nicholas@2224
|
1119 return;
|
nicholas@2224
|
1120 }
|
nicholas@2708
|
1121 var node = this.popupOptions[this.currentIndex],
|
nicholas@2774
|
1122 pass = true,
|
nicholas@2778
|
1123 timeDelta = (new Date() - lastNodeStart) / 1000.0;
|
nicholas@2774
|
1124 if (timeDelta < node.specification.minWait) {
|
nicholas@2778
|
1125 interfaceContext.lightbox.post("Error", "Not enough time has elapsed, please wait " + (node.specification.minWait - timeDelta).toFixed(0) + " seconds");
|
nicholas@2774
|
1126 return;
|
nicholas@2774
|
1127 }
|
nicholas@2775
|
1128 node.elapsedTime = timeDelta;
|
nicholas@2498
|
1129 if (node.specification.type == 'question') {
|
nicholas@2498
|
1130 // Must extract the question data
|
nicholas@2708
|
1131 pass = processQuestion.call(this, node);
|
nicholas@2498
|
1132 } else if (node.specification.type == 'checkbox') {
|
nicholas@2498
|
1133 // Must extract checkbox data
|
nicholas@2708
|
1134 pass = processCheckbox.call(this, node);
|
nicholas@2708
|
1135 } else if (node.specification.type == "radio") {
|
nicholas@2464
|
1136 // Perform the conditional
|
nicholas@2708
|
1137 pass = processRadio.call(this, node);
|
nicholas@2708
|
1138 } else if (node.specification.type == "number") {
|
nicholas@2464
|
1139 // Perform the conditional
|
nicholas@2708
|
1140 pass = processNumber.call(this, node);
|
n@2583
|
1141 } else if (node.specification.type == 'slider') {
|
nicholas@2708
|
1142 pass = processSlider.call(this, node);
|
nicholas@2708
|
1143 }
|
nicholas@2708
|
1144 if (pass === false) {
|
nicholas@2708
|
1145 return;
|
nicholas@2498
|
1146 }
|
nicholas@2498
|
1147 this.currentIndex++;
|
nicholas@2498
|
1148 if (this.currentIndex < this.popupOptions.length) {
|
nicholas@2498
|
1149 this.postNode();
|
nicholas@2498
|
1150 } else {
|
nicholas@2498
|
1151 // Reached the end of the popupOptions
|
nicholas@2645
|
1152 this.popupTitle.innerHTML = "";
|
nicholas@2498
|
1153 this.popupResponse.innerHTML = "";
|
nicholas@2498
|
1154 this.hidePopup();
|
nicholas@2708
|
1155 this.popupOptions.forEach(function (node) {
|
nicholas@2498
|
1156 this.store.postResult(node);
|
nicholas@2708
|
1157 }, this);
|
nicholas@2224
|
1158 this.store.complete();
|
nicholas@2498
|
1159 advanceState();
|
nicholas@2498
|
1160 }
|
nicholas@2498
|
1161 };
|
nicholas@2498
|
1162
|
nicholas@2498
|
1163 this.previousClick = function () {
|
nicholas@2498
|
1164 // Triggered when the 'Back' button is clicked in the survey
|
nicholas@2498
|
1165 if (this.currentIndex > 0) {
|
nicholas@2498
|
1166 this.currentIndex--;
|
nicholas@2498
|
1167 this.postNode();
|
nicholas@2498
|
1168 }
|
nicholas@2498
|
1169 };
|
nicholas@2498
|
1170
|
nicholas@2498
|
1171 this.resize = function (event) {
|
nicholas@2498
|
1172 // Called on window resize;
|
nicholas@2708
|
1173 if (this.popup !== null) {
|
nicholas@2498
|
1174 this.popup.style.left = (window.innerWidth / 2) - 250 + 'px';
|
nicholas@2498
|
1175 this.popup.style.top = (window.innerHeight / 2) - 125 + 'px';
|
nicholas@2498
|
1176 var blank = document.getElementsByClassName('testHalt')[0];
|
nicholas@2498
|
1177 blank.style.width = window.innerWidth;
|
nicholas@2498
|
1178 blank.style.height = window.innerHeight;
|
nicholas@2498
|
1179 }
|
nicholas@2498
|
1180 };
|
nicholas@2498
|
1181 this.hideNextButton = function () {
|
nicholas@2224
|
1182 this.buttonProceed.style.visibility = "hidden";
|
nicholas@2708
|
1183 };
|
nicholas@2498
|
1184 this.hidePreviousButton = function () {
|
nicholas@2224
|
1185 this.buttonPrevious.style.visibility = "hidden";
|
nicholas@2708
|
1186 };
|
nicholas@2498
|
1187 this.showNextButton = function () {
|
nicholas@2224
|
1188 this.buttonProceed.style.visibility = "visible";
|
nicholas@2708
|
1189 };
|
nicholas@2498
|
1190 this.showPreviousButton = function () {
|
nicholas@2224
|
1191 this.buttonPrevious.style.visibility = "visible";
|
nicholas@2708
|
1192 };
|
nicholas@2224
|
1193 }
|
nicholas@2224
|
1194
|
nicholas@2498
|
1195 function advanceState() {
|
nicholas@2498
|
1196 // Just for complete clarity
|
nicholas@2498
|
1197 testState.advanceState();
|
nicholas@2224
|
1198 }
|
nicholas@2224
|
1199
|
nicholas@2498
|
1200 function stateMachine() {
|
nicholas@2498
|
1201 // Object prototype for tracking and managing the test state
|
nicholas@2722
|
1202
|
n@2716
|
1203 function pickSubPool(pool, numElements) {
|
n@2716
|
1204 // Assumes each element of pool has function "alwaysInclude"
|
n@2716
|
1205
|
n@2716
|
1206 // First extract those excluded from picking process
|
n@2716
|
1207 var picked = [];
|
nicholas@2833
|
1208 pool.forEach(function (e, i) {
|
n@2716
|
1209 if (e.alwaysInclude) {
|
nicholas@2833
|
1210 picked.push(pool.splice(i, 1)[0]);
|
n@2716
|
1211 }
|
n@2716
|
1212 });
|
n@2716
|
1213
|
n@2716
|
1214 return picked.concat(randomSubArray(pool, numElements - picked.length));
|
n@2716
|
1215 }
|
nicholas@2722
|
1216
|
nicholas@2498
|
1217 this.stateMap = [];
|
nicholas@2498
|
1218 this.preTestSurvey = null;
|
nicholas@2498
|
1219 this.postTestSurvey = null;
|
nicholas@2498
|
1220 this.stateIndex = null;
|
nicholas@2498
|
1221 this.currentStateMap = null;
|
nicholas@2498
|
1222 this.currentStatePosition = null;
|
nicholas@2224
|
1223 this.currentStore = null;
|
nicholas@2498
|
1224 this.initialise = function () {
|
nicholas@2498
|
1225
|
n@2909
|
1226 function randomiseElements(page) {
|
n@2909
|
1227 // Get the elements which are fixed / labelled
|
n@2909
|
1228 var fixed = [],
|
n@2909
|
1229 or = [],
|
n@2909
|
1230 remainder = [];
|
n@2909
|
1231 page.audioElements.forEach(function (a) {
|
n@2909
|
1232 if (a.label.length > 0 || a.postion !== undefined) {
|
n@2909
|
1233 fixed.push(a);
|
n@2909
|
1234 } else if (a.type === "outside-reference") {
|
n@2909
|
1235 or.push(a);
|
n@2909
|
1236 } else {
|
n@2909
|
1237 remainder.push(a);
|
n@2909
|
1238 }
|
n@2909
|
1239 })
|
n@2909
|
1240 if (page.poolSize > 0 || page.randomiseOrder) {
|
n@2909
|
1241 page.randomiseOrder = true;
|
n@2909
|
1242 if (page.poolSize === 0) {
|
n@2909
|
1243 page.poolSize = page.audioElements.length;
|
n@2909
|
1244 }
|
n@2909
|
1245 page.poolSize -= fixed.length;
|
n@2909
|
1246 remainder = pickSubPool(remainder, page.poolSize);
|
n@2909
|
1247 }
|
n@2909
|
1248 // Randomise the remainders
|
n@2909
|
1249 if (page.randomiseOrder) {
|
n@2909
|
1250 remainder = randomiseOrder(remainder);
|
n@2909
|
1251 }
|
n@2909
|
1252 fixed = fixed.concat(remainder);
|
n@2909
|
1253 page.audioElements = fixed.concat(or);
|
n@2909
|
1254 page.audioElements.forEach(function (a, i) {
|
n@2909
|
1255 a.position = i;
|
n@2909
|
1256 });
|
n@2909
|
1257 }
|
n@2909
|
1258
|
nicholas@2498
|
1259 // Get the data from Specification
|
nicholas@2498
|
1260 var pagePool = [];
|
nicholas@2722
|
1261 specification.pages.forEach(function (page) {
|
n@2716
|
1262 if (page.position !== null || page.alwaysInclude) {
|
n@2716
|
1263 page.alwaysInclude = true;
|
n@2716
|
1264 }
|
n@2716
|
1265 pagePool.push(page);
|
n@2717
|
1266 });
|
n@2716
|
1267 if (specification.numPages > 0) {
|
n@2716
|
1268 specification.randomiseOrder = true;
|
n@2716
|
1269 pagePool = pickSubPool(pagePool, specification.numPages);
|
n@2716
|
1270 }
|
n@2716
|
1271
|
n@2716
|
1272 // Now get the order of pages
|
n@2716
|
1273 var fixed = [];
|
nicholas@2722
|
1274 pagePool.forEach(function (page) {
|
nicholas@2748
|
1275 if (page.position !== undefined) {
|
n@2716
|
1276 fixed.push(page);
|
n@2716
|
1277 var i = pagePool.indexOf(page);
|
n@2716
|
1278 pagePool.splice(i, 1);
|
nicholas@2224
|
1279 }
|
n@2717
|
1280 });
|
nicholas@2498
|
1281
|
n@2716
|
1282 if (specification.randomiseOrder) {
|
n@2716
|
1283 pagePool = randomiseOrder(pagePool);
|
nicholas@2224
|
1284 }
|
nicholas@2498
|
1285
|
n@2716
|
1286 // Place in the correct order
|
nicholas@2722
|
1287 fixed.forEach(function (page) {
|
n@2716
|
1288 pagePool.splice(page.position, 0, page);
|
n@2717
|
1289 });
|
n@2716
|
1290
|
n@2716
|
1291 // Now process the pages
|
n@2716
|
1292 pagePool.forEach(function (page, i) {
|
n@2716
|
1293 page.presentedId = i;
|
n@2716
|
1294 this.stateMap.push(page);
|
n@2716
|
1295 var elements = page.audioElements;
|
n@2909
|
1296 randomiseElements(page);
|
n@2716
|
1297 storage.createTestPageStore(page);
|
n@2716
|
1298 audioEngineContext.loadPageData(page);
|
n@2716
|
1299 }, this);
|
nicholas@2674
|
1300
|
nicholas@2708
|
1301 if (specification.preTest !== null) {
|
nicholas@2498
|
1302 this.preTestSurvey = specification.preTest;
|
nicholas@2498
|
1303 }
|
nicholas@2708
|
1304 if (specification.postTest !== null) {
|
nicholas@2498
|
1305 this.postTestSurvey = specification.postTest;
|
nicholas@2498
|
1306 }
|
nicholas@2498
|
1307
|
nicholas@2498
|
1308 if (this.stateMap.length > 0) {
|
nicholas@2708
|
1309 if (this.stateIndex !== null) {
|
nicholas@2498
|
1310 console.log('NOTE - State already initialise');
|
nicholas@2498
|
1311 }
|
nicholas@2498
|
1312 this.stateIndex = -2;
|
nicholas@2224
|
1313 console.log('Starting test...');
|
nicholas@2498
|
1314 } else {
|
nicholas@2498
|
1315 console.log('FATAL - StateMap not correctly constructed. EMPTY_STATE_MAP');
|
nicholas@2498
|
1316 }
|
nicholas@2498
|
1317 };
|
nicholas@2498
|
1318 this.advanceState = function () {
|
nicholas@2708
|
1319 if (this.stateIndex === null) {
|
nicholas@2498
|
1320 this.initialise();
|
nicholas@2498
|
1321 }
|
nicholas@2357
|
1322 if (this.stateIndex > -2) {
|
nicholas@2357
|
1323 storage.update();
|
nicholas@2357
|
1324 }
|
nicholas@2498
|
1325 if (this.stateIndex == -2) {
|
nicholas@2224
|
1326 this.stateIndex++;
|
nicholas@2708
|
1327 if (this.preTestSurvey !== undefined) {
|
nicholas@2498
|
1328 popup.initState(this.preTestSurvey, storage.globalPreTest);
|
nicholas@2498
|
1329 } else {
|
nicholas@2498
|
1330 this.advanceState();
|
nicholas@2498
|
1331 }
|
nicholas@2498
|
1332 } else if (this.stateIndex == -1) {
|
nicholas@2224
|
1333 this.stateIndex++;
|
nicholas@2224
|
1334 if (specification.calibration) {
|
nicholas@2224
|
1335 popup.showPopup();
|
nicholas@2224
|
1336 popup.popupTitle.textContent = "Calibration. Set the levels so all tones are of equal amplitude. Move your mouse over the sliders to hear the tones. The red slider is the reference tone";
|
nicholas@2224
|
1337 interfaceContext.calibrationModuleObject = new interfaceContext.calibrationModule();
|
nicholas@2224
|
1338 interfaceContext.calibrationModuleObject.build(popup.popupResponse);
|
nicholas@2224
|
1339 popup.hidePreviousButton();
|
nicholas@2224
|
1340 } else {
|
nicholas@2224
|
1341 this.advanceState();
|
nicholas@2224
|
1342 }
|
nicholas@2498
|
1343 } else if (this.stateIndex == this.stateMap.length) {
|
nicholas@2498
|
1344 // All test pages complete, post test
|
nicholas@2498
|
1345 console.log('Ending test ...');
|
nicholas@2498
|
1346 this.stateIndex++;
|
nicholas@2708
|
1347 if (this.postTestSurvey === undefined) {
|
nicholas@2498
|
1348 this.advanceState();
|
nicholas@2498
|
1349 } else {
|
nicholas@2498
|
1350 popup.initState(this.postTestSurvey, storage.globalPostTest);
|
nicholas@2498
|
1351 }
|
nicholas@2498
|
1352 } else if (this.stateIndex > this.stateMap.length) {
|
nicholas@2498
|
1353 createProjectSave(specification.projectReturn);
|
nicholas@2498
|
1354 } else {
|
nicholas@2224
|
1355 popup.hidePopup();
|
nicholas@2708
|
1356 if (this.currentStateMap === null) {
|
nicholas@2498
|
1357 this.currentStateMap = this.stateMap[this.stateIndex];
|
nicholas@2498
|
1358
|
nicholas@2224
|
1359 this.currentStore = storage.testPages[this.stateIndex];
|
nicholas@2708
|
1360 if (this.currentStateMap.preTest !== undefined) {
|
nicholas@2498
|
1361 this.currentStatePosition = 'pre';
|
nicholas@2498
|
1362 popup.initState(this.currentStateMap.preTest, storage.testPages[this.stateIndex].preTest);
|
nicholas@2498
|
1363 } else {
|
nicholas@2498
|
1364 this.currentStatePosition = 'test';
|
nicholas@2498
|
1365 }
|
nicholas@2498
|
1366 interfaceContext.newPage(this.currentStateMap, storage.testPages[this.stateIndex]);
|
nicholas@2498
|
1367 return;
|
nicholas@2498
|
1368 }
|
nicholas@2498
|
1369 switch (this.currentStatePosition) {
|
nicholas@2498
|
1370 case 'pre':
|
nicholas@2498
|
1371 this.currentStatePosition = 'test';
|
nicholas@2498
|
1372 break;
|
nicholas@2498
|
1373 case 'test':
|
nicholas@2498
|
1374 this.currentStatePosition = 'post';
|
nicholas@2498
|
1375 // Save the data
|
nicholas@2498
|
1376 this.testPageCompleted();
|
nicholas@2708
|
1377 if (this.currentStateMap.postTest === undefined) {
|
nicholas@2498
|
1378 this.advanceState();
|
nicholas@2498
|
1379 return;
|
nicholas@2498
|
1380 } else {
|
nicholas@2498
|
1381 popup.initState(this.currentStateMap.postTest, storage.testPages[this.stateIndex].postTest);
|
nicholas@2498
|
1382 }
|
nicholas@2498
|
1383 break;
|
nicholas@2498
|
1384 case 'post':
|
nicholas@2498
|
1385 this.stateIndex++;
|
nicholas@2498
|
1386 this.currentStateMap = null;
|
nicholas@2498
|
1387 this.advanceState();
|
nicholas@2498
|
1388 break;
|
nicholas@2708
|
1389 }
|
nicholas@2498
|
1390 }
|
nicholas@2498
|
1391 };
|
nicholas@2498
|
1392
|
nicholas@2498
|
1393 this.testPageCompleted = function () {
|
nicholas@2498
|
1394 // Function called each time a test page has been completed
|
nicholas@2498
|
1395 var storePoint = storage.testPages[this.stateIndex];
|
nicholas@2498
|
1396 // First get the test metric
|
nicholas@2498
|
1397
|
nicholas@2498
|
1398 var metric = storePoint.XMLDOM.getElementsByTagName('metric')[0];
|
nicholas@2498
|
1399 if (audioEngineContext.metric.enableTestTimer) {
|
nicholas@2498
|
1400 var testTime = storePoint.parent.document.createElement('metricresult');
|
nicholas@2498
|
1401 testTime.id = 'testTime';
|
nicholas@2498
|
1402 testTime.textContent = audioEngineContext.timer.testDuration;
|
nicholas@2498
|
1403 metric.appendChild(testTime);
|
nicholas@2498
|
1404 }
|
nicholas@2498
|
1405
|
nicholas@2498
|
1406 var audioObjects = audioEngineContext.audioObjects;
|
nicholas@2708
|
1407 audioEngineContext.audioObjects.forEach(function (ao) {
|
nicholas@2498
|
1408 ao.exportXMLDOM();
|
nicholas@2708
|
1409 });
|
nicholas@2708
|
1410 interfaceContext.commentQuestions.forEach(function (element) {
|
nicholas@2498
|
1411 element.exportXMLDOM(storePoint);
|
nicholas@2708
|
1412 });
|
nicholas@2498
|
1413 pageXMLSave(storePoint.XMLDOM, this.currentStateMap);
|
nicholas@2224
|
1414 storePoint.complete();
|
nicholas@2498
|
1415 };
|
nicholas@2498
|
1416
|
nicholas@2498
|
1417 this.getCurrentTestPage = function () {
|
nicholas@2498
|
1418 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
|
nicholas@2310
|
1419 return this.currentStateMap;
|
nicholas@2310
|
1420 } else {
|
nicholas@2310
|
1421 return null;
|
nicholas@2310
|
1422 }
|
nicholas@2708
|
1423 };
|
nicholas@2498
|
1424 this.getCurrentTestPageStore = function () {
|
nicholas@2498
|
1425 if (this.stateIndex >= 0 && this.stateIndex < this.stateMap.length) {
|
nicholas@2312
|
1426 return this.currentStore;
|
nicholas@2312
|
1427 } else {
|
nicholas@2312
|
1428 return null;
|
nicholas@2312
|
1429 }
|
nicholas@2708
|
1430 };
|
nicholas@2224
|
1431 }
|
nicholas@2224
|
1432
|
nicholas@2224
|
1433 function AudioEngine(specification) {
|
nicholas@2498
|
1434
|
nicholas@2498
|
1435 // Create two output paths, the main outputGain and fooGain.
|
nicholas@2498
|
1436 // Output gain is default to 1 and any items for playback route here
|
nicholas@2498
|
1437 // Foo gain is used for analysis to ensure paths get processed, but are not heard
|
nicholas@2498
|
1438 // because web audio will optimise and any route which does not go to the destination gets ignored.
|
nicholas@2498
|
1439 this.outputGain = audioContext.createGain();
|
nicholas@2498
|
1440 this.fooGain = audioContext.createGain();
|
nicholas@2508
|
1441 this.fooGain.gain.value = 0;
|
nicholas@2498
|
1442
|
nicholas@2498
|
1443 // Use this to detect playback state: 0 - stopped, 1 - playing
|
nicholas@2498
|
1444 this.status = 0;
|
nicholas@2498
|
1445
|
nicholas@2498
|
1446 // Connect both gains to output
|
nicholas@2498
|
1447 this.outputGain.connect(audioContext.destination);
|
nicholas@2498
|
1448 this.fooGain.connect(audioContext.destination);
|
nicholas@2498
|
1449
|
nicholas@2498
|
1450 // Create the timer Object
|
nicholas@2498
|
1451 this.timer = new timer();
|
nicholas@2498
|
1452 // Create session metrics
|
nicholas@2498
|
1453 this.metric = new sessionMetrics(this, specification);
|
nicholas@2498
|
1454
|
nicholas@2498
|
1455 this.loopPlayback = false;
|
nicholas@2351
|
1456 this.synchPlayback = false;
|
nicholas@2351
|
1457 this.pageSpecification = null;
|
nicholas@2498
|
1458
|
nicholas@2498
|
1459 this.pageStore = null;
|
nicholas@2498
|
1460
|
nicholas@2508
|
1461 // Chrome 53+ Error solution
|
nicholas@2508
|
1462 // Empty buffer for keep-alive
|
nicholas@2508
|
1463 var nullBuffer = audioContext.createBuffer(1, audioContext.sampleRate, audioContext.sampleRate);
|
nicholas@2508
|
1464 this.nullBufferSource = audioContext.createBufferSource();
|
nicholas@2508
|
1465 this.nullBufferSource.buffer = nullBuffer;
|
nicholas@2508
|
1466 this.nullBufferSource.loop = true;
|
nicholas@2508
|
1467 this.nullBufferSource.start(0);
|
nicholas@2508
|
1468
|
nicholas@2498
|
1469 // Create store for new audioObjects
|
nicholas@2498
|
1470 this.audioObjects = [];
|
nicholas@2498
|
1471
|
nicholas@2498
|
1472 this.buffers = [];
|
nicholas@2498
|
1473 this.bufferObj = function () {
|
nicholas@2617
|
1474 var urls = [];
|
nicholas@2498
|
1475 this.buffer = null;
|
nicholas@2498
|
1476 this.users = [];
|
nicholas@2224
|
1477 this.progress = 0;
|
nicholas@2224
|
1478 this.status = 0;
|
nicholas@2498
|
1479 this.ready = function () {
|
nicholas@2498
|
1480 if (this.status >= 2) {
|
nicholas@2224
|
1481 this.status = 3;
|
nicholas@2224
|
1482 }
|
nicholas@2498
|
1483 for (var i = 0; i < this.users.length; i++) {
|
nicholas@2498
|
1484 this.users[i].state = 1;
|
nicholas@2708
|
1485 if (this.users[i].interfaceDOM !== null) {
|
nicholas@2498
|
1486 this.users[i].bufferLoaded(this);
|
nicholas@2498
|
1487 }
|
nicholas@2498
|
1488 }
|
nicholas@2498
|
1489 };
|
nicholas@2617
|
1490 this.setUrls = function (obj) {
|
nicholas@2617
|
1491 // Obj must be an array of pairs:
|
nicholas@2617
|
1492 // [{sampleRate, url}]
|
nicholas@2617
|
1493 var localFs = audioContext.sampleRate,
|
nicholas@2617
|
1494 list = [],
|
nicholas@2617
|
1495 i;
|
nicholas@2617
|
1496 for (i = 0; i < obj.length; i++) {
|
nicholas@2617
|
1497 if (obj[i].sampleRate == localFs) {
|
nicholas@2617
|
1498 list.push(obj.splice(i, 1)[0]);
|
nicholas@2617
|
1499 }
|
nicholas@2617
|
1500 }
|
nicholas@2617
|
1501 list = list.concat(obj);
|
nicholas@2617
|
1502 urls = list;
|
nicholas@2617
|
1503 };
|
nicholas@2617
|
1504 this.hasUrl = function (checkUrl) {
|
nicholas@2617
|
1505 var l = urls.length,
|
nicholas@2617
|
1506 i;
|
nicholas@2617
|
1507 for (i = 0; i < l; i++) {
|
nicholas@2617
|
1508 if (urls[i].url == checkUrl) {
|
nicholas@2617
|
1509 return true;
|
nicholas@2617
|
1510 }
|
nicholas@2617
|
1511 }
|
nicholas@2617
|
1512 return false;
|
nicholas@2708
|
1513 };
|
nicholas@2617
|
1514 this.getMedia = function () {
|
nicholas@2615
|
1515 var self = this;
|
nicholas@2616
|
1516 var currentUrlIndex = 0;
|
nicholas@2498
|
1517
|
nicholas@2615
|
1518 function get(fqurl) {
|
nicholas@2615
|
1519 return new Promise(function (resolve, reject) {
|
nicholas@2615
|
1520 var req = new XMLHttpRequest();
|
nicholas@2615
|
1521 req.open('GET', fqurl, true);
|
nicholas@2615
|
1522 req.responseType = 'arraybuffer';
|
nicholas@2615
|
1523 req.onload = function () {
|
nicholas@2615
|
1524 if (req.status == 200) {
|
nicholas@2615
|
1525 resolve(req.response);
|
nicholas@2615
|
1526 }
|
nicholas@2615
|
1527 };
|
nicholas@2615
|
1528 req.onerror = function () {
|
nicholas@2615
|
1529 reject(new Error(req.statusText));
|
nicholas@2615
|
1530 };
|
nicholas@2615
|
1531
|
nicholas@2615
|
1532 req.addEventListener("progress", progressCallback.bind(self));
|
nicholas@2615
|
1533 req.send();
|
nicholas@2615
|
1534 });
|
nicholas@2615
|
1535 }
|
nicholas@2615
|
1536
|
nicholas@2615
|
1537 function getNextURL() {
|
nicholas@2615
|
1538 currentUrlIndex++;
|
nicholas@2615
|
1539 var self = this;
|
nicholas@2617
|
1540 if (currentUrlIndex >= urls.length) {
|
nicholas@2615
|
1541 processError();
|
nicholas@2615
|
1542 } else {
|
nicholas@2617
|
1543 return get(urls[currentUrlIndex].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
|
nicholas@2615
|
1544 }
|
nicholas@2615
|
1545 }
|
nicholas@2498
|
1546
|
nicholas@2498
|
1547 // Create callback to decode the data asynchronously
|
nicholas@2615
|
1548 function processAudio(response) {
|
nicholas@2615
|
1549 var self = this;
|
nicholas@2615
|
1550 return audioContext.decodeAudioData(response, function (decodedData) {
|
nicholas@2615
|
1551 self.buffer = decodedData;
|
nicholas@2615
|
1552 self.status = 2;
|
nicholas@2615
|
1553 calculateLoudness(self, "I");
|
nicholas@2615
|
1554 return true;
|
nicholas@2498
|
1555 }, function (e) {
|
nicholas@2403
|
1556 var waveObj = new WAVE();
|
nicholas@2708
|
1557 if (waveObj.open(response) === 0) {
|
nicholas@2615
|
1558 self.buffer = audioContext.createBuffer(waveObj.num_channels, waveObj.num_samples, waveObj.sample_rate);
|
nicholas@2498
|
1559 for (var c = 0; c < waveObj.num_channels; c++) {
|
nicholas@2615
|
1560 var buffer_ptr = self.buffer.getChannelData(c);
|
nicholas@2498
|
1561 for (var n = 0; n < waveObj.num_samples; n++) {
|
nicholas@2403
|
1562 buffer_ptr[n] = waveObj.decoded_data[c][n];
|
nicholas@2224
|
1563 }
|
nicholas@2224
|
1564 }
|
nicholas@2403
|
1565 }
|
nicholas@2708
|
1566 if (self.buffer !== undefined) {
|
nicholas@2615
|
1567 self.status = 2;
|
nicholas@2615
|
1568 calculateLoudness(self, "I");
|
nicholas@2615
|
1569 return true;
|
nicholas@2403
|
1570 }
|
nicholas@2708
|
1571 waveObj = undefined;
|
nicholas@2615
|
1572 return false;
|
nicholas@2403
|
1573 });
|
nicholas@2615
|
1574 }
|
nicholas@2498
|
1575
|
nicholas@2224
|
1576 // Create callback for any error in loading
|
nicholas@2615
|
1577 function processError() {
|
nicholas@2615
|
1578 this.status = -1;
|
nicholas@2615
|
1579 for (var i = 0; i < this.users.length; i++) {
|
nicholas@2615
|
1580 this.users[i].state = -1;
|
nicholas@2708
|
1581 if (this.users[i].interfaceDOM !== null) {
|
nicholas@2615
|
1582 this.users[i].bufferLoaded(this);
|
nicholas@2224
|
1583 }
|
nicholas@2224
|
1584 }
|
nicholas@2617
|
1585 interfaceContext.lightbox.post("Error", "Could not load resource " + urls[currentUrlIndex].url);
|
nicholas@2224
|
1586 }
|
nicholas@2498
|
1587
|
nicholas@2615
|
1588 function progressCallback(event) {
|
nicholas@2498
|
1589 if (event.lengthComputable) {
|
nicholas@2615
|
1590 this.progress = event.loaded / event.total;
|
nicholas@2615
|
1591 for (var i = 0; i < this.users.length; i++) {
|
nicholas@2708
|
1592 if (this.users[i].interfaceDOM !== null) {
|
nicholas@2615
|
1593 if (typeof this.users[i].interfaceDOM.updateLoading === "function") {
|
nicholas@2615
|
1594 this.users[i].interfaceDOM.updateLoading(this.progress * 100);
|
nicholas@2498
|
1595 }
|
nicholas@2498
|
1596 }
|
nicholas@2498
|
1597 }
|
nicholas@2498
|
1598 }
|
nicholas@2708
|
1599 }
|
nicholas@2615
|
1600
|
nicholas@2615
|
1601 this.progress = 0;
|
nicholas@2224
|
1602 this.status = 1;
|
nicholas@2617
|
1603 currentUrlIndex = 0;
|
nicholas@2617
|
1604 get(urls[0].url).then(processAudio.bind(self)).catch(getNextURL.bind(self));
|
nicholas@2498
|
1605 };
|
nicholas@2498
|
1606
|
nicholas@2498
|
1607 this.registerAudioObject = function (audioObject) {
|
nicholas@2224
|
1608 // Called by an audioObject to register to the buffer for use
|
nicholas@2224
|
1609 // First check if already in the register pool
|
nicholas@2708
|
1610 this.users.forEach(function (object) {
|
nicholas@2708
|
1611 if (audioObject.id == object.id) {
|
nicholas@2498
|
1612 return 0;
|
nicholas@2498
|
1613 }
|
nicholas@2708
|
1614 });
|
nicholas@2224
|
1615 this.users.push(audioObject);
|
nicholas@2498
|
1616 if (this.status == 3 || this.status == -1) {
|
nicholas@2224
|
1617 // The buffer is already ready, trigger bufferLoaded
|
nicholas@2224
|
1618 audioObject.bufferLoaded(this);
|
nicholas@2224
|
1619 }
|
nicholas@2224
|
1620 };
|
nicholas@2498
|
1621
|
nicholas@2498
|
1622 this.copyBuffer = function (preSilenceTime, postSilenceTime) {
|
nicholas@2224
|
1623 // Copies the entire bufferObj.
|
nicholas@2708
|
1624 if (preSilenceTime === undefined) {
|
nicholas@2498
|
1625 preSilenceTime = 0;
|
nicholas@2498
|
1626 }
|
nicholas@2708
|
1627 if (postSilenceTime === undefined) {
|
nicholas@2498
|
1628 postSilenceTime = 0;
|
nicholas@2498
|
1629 }
|
nicholas@2498
|
1630 var preSilenceSamples = secondsToSamples(preSilenceTime, this.buffer.sampleRate);
|
nicholas@2498
|
1631 var postSilenceSamples = secondsToSamples(postSilenceTime, this.buffer.sampleRate);
|
nicholas@2498
|
1632 var newLength = this.buffer.length + preSilenceSamples + postSilenceSamples;
|
nicholas@2460
|
1633 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
|
nicholas@2708
|
1634 var c;
|
nicholas@2224
|
1635 // Now we can use some efficient background copy schemes if we are just padding the end
|
nicholas@2708
|
1636 if (preSilenceSamples === 0 && typeof copybuffer.copyToChannel === "function") {
|
nicholas@2708
|
1637 for (c = 0; c < this.buffer.numberOfChannels; c++) {
|
nicholas@2498
|
1638 copybuffer.copyToChannel(this.buffer.getChannelData(c), c);
|
nicholas@2224
|
1639 }
|
nicholas@2224
|
1640 } else {
|
nicholas@2708
|
1641 for (c = 0; c < this.buffer.numberOfChannels; c++) {
|
nicholas@2224
|
1642 var src = this.buffer.getChannelData(c);
|
nicholas@2460
|
1643 var dst = copybuffer.getChannelData(c);
|
nicholas@2498
|
1644 for (var n = 0; n < src.length; n++)
|
nicholas@2498
|
1645 dst[n + preSilenceSamples] = src[n];
|
nicholas@2224
|
1646 }
|
nicholas@2224
|
1647 }
|
nicholas@2224
|
1648 // Copy in the rest of the buffer information
|
nicholas@2460
|
1649 copybuffer.lufs = this.buffer.lufs;
|
nicholas@2460
|
1650 copybuffer.playbackGain = this.buffer.playbackGain;
|
nicholas@2460
|
1651 return copybuffer;
|
nicholas@2708
|
1652 };
|
nicholas@2498
|
1653
|
nicholas@2498
|
1654 this.cropBuffer = function (startTime, stopTime) {
|
nicholas@2460
|
1655 // Copy and return the cropped buffer
|
nicholas@2498
|
1656 var start_sample = Math.floor(startTime * this.buffer.sampleRate);
|
nicholas@2498
|
1657 var stop_sample = Math.floor(stopTime * this.buffer.sampleRate);
|
nicholas@2460
|
1658 var newLength = stop_sample - start_sample;
|
nicholas@2460
|
1659 var copybuffer = audioContext.createBuffer(this.buffer.numberOfChannels, newLength, this.buffer.sampleRate);
|
nicholas@2460
|
1660 // Now we can use some efficient background copy schemes if we are just padding the end
|
nicholas@2498
|
1661 for (var c = 0; c < this.buffer.numberOfChannels; c++) {
|
nicholas@2460
|
1662 var buffer = this.buffer.getChannelData(c);
|
nicholas@2498
|
1663 var sub_frame = buffer.subarray(start_sample, stop_sample);
|
nicholas@2460
|
1664 if (typeof copybuffer.copyToChannel == "function") {
|
nicholas@2498
|
1665 copybuffer.copyToChannel(sub_frame, c);
|
nicholas@2460
|
1666 } else {
|
nicholas@2460
|
1667 var dst = copybuffer.getChannelData(c);
|
nicholas@2498
|
1668 for (var n = 0; n < newLength; n++)
|
nicholas@2505
|
1669 dst[n] = buffer[n + start_sample];
|
nicholas@2460
|
1670 }
|
nicholas@2460
|
1671 }
|
nicholas@2460
|
1672 return copybuffer;
|
nicholas@2708
|
1673 };
|
nicholas@2498
|
1674 };
|
nicholas@2498
|
1675
|
nicholas@2498
|
1676 this.loadPageData = function (page) {
|
nicholas@2224
|
1677 // Load the URL from pages
|
nicholas@2708
|
1678 function loadAudioElementData(element) {
|
nicholas@2224
|
1679 var URL = page.hostURL + element.url;
|
nicholas@2708
|
1680 var buffer = this.buffers.find(function (buffObj) {
|
nicholas@2708
|
1681 return buffObj.hasUrl(URL);
|
nicholas@2708
|
1682 });
|
nicholas@2708
|
1683 if (buffer === undefined) {
|
nicholas@2224
|
1684 buffer = new this.bufferObj();
|
nicholas@2617
|
1685 var urls = [{
|
nicholas@2617
|
1686 url: URL,
|
nicholas@2617
|
1687 sampleRate: element.sampleRate
|
nicholas@2617
|
1688 }];
|
nicholas@2615
|
1689 element.alternatives.forEach(function (e) {
|
nicholas@2617
|
1690 urls.push({
|
nicholas@2617
|
1691 url: e.url,
|
nicholas@2617
|
1692 sampleRate: e.sampleRate
|
nicholas@2617
|
1693 });
|
nicholas@2615
|
1694 });
|
nicholas@2617
|
1695 buffer.setUrls(urls);
|
nicholas@2617
|
1696 buffer.getMedia();
|
nicholas@2224
|
1697 this.buffers.push(buffer);
|
nicholas@2224
|
1698 }
|
nicholas@2224
|
1699 }
|
nicholas@2708
|
1700 page.audioElements.forEach(loadAudioElementData, this);
|
nicholas@2224
|
1701 };
|
nicholas@2498
|
1702
|
nicholas@2708
|
1703 function playNormal(id) {
|
nicholas@2708
|
1704 var playTime = audioContext.currentTime + 0.1;
|
nicholas@2708
|
1705 var stopTime = playTime + specification.crossFade;
|
nicholas@2708
|
1706 this.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
1707 if (ao.id === id) {
|
nicholas@2708
|
1708 ao.play(playTime);
|
nicholas@2708
|
1709 } else {
|
nicholas@2708
|
1710 ao.stop(stopTime);
|
nicholas@2708
|
1711 }
|
nicholas@2708
|
1712 });
|
nicholas@2708
|
1713 }
|
nicholas@2708
|
1714
|
nicholas@2708
|
1715 function playLoopSync(id) {
|
nicholas@2708
|
1716 var playTime = audioContext.currentTime + 0.1;
|
nicholas@2708
|
1717 var stopTime = playTime + specification.crossFade;
|
nicholas@2708
|
1718 this.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
1719 ao.play(playTime);
|
nicholas@2708
|
1720 if (ao.id === id) {
|
nicholas@2708
|
1721 ao.loopStart(playTime);
|
nicholas@2708
|
1722 } else {
|
nicholas@2708
|
1723 ao.loopStop(stopTime);
|
nicholas@2708
|
1724 }
|
nicholas@2708
|
1725 });
|
nicholas@2708
|
1726 }
|
nicholas@2708
|
1727
|
nicholas@2498
|
1728 this.play = function (id) {
|
nicholas@2498
|
1729 // Start the timer and set the audioEngine state to playing (1)
|
nicholas@2708
|
1730 if (typeof id !== "number" || id < 0 || id > this.audioObjects.length) {
|
nicholas@2708
|
1731 throw ('FATAL - Passed id was undefined - AudioEngineContext.play(id)');
|
nicholas@2498
|
1732 }
|
nicholas@2823
|
1733 var maxPlays = this.audioObjects[id].specification.maxNumberPlays || this.audioObjects[id].specification.parent.maxNumberPlays || specification.maxNumberPlays;
|
nicholas@2823
|
1734 if (maxPlays !== undefined && this.audioObjects[id].numberOfPlays >= maxPlays) {
|
nicholas@2823
|
1735 interfaceContext.lightbox.post("Error", "Cannot play this fragment more than " + maxPlays + " times");
|
nicholas@2823
|
1736 return;
|
nicholas@2823
|
1737 }
|
nicholas@2708
|
1738 if (this.status === 1) {
|
nicholas@2498
|
1739 this.timer.startTest();
|
nicholas@2708
|
1740 interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]);
|
nicholas@2498
|
1741 if (this.synchPlayback && this.loopPlayback) {
|
nicholas@2351
|
1742 // Traditional looped playback
|
nicholas@2708
|
1743 playLoopSync.call(this, id);
|
nicholas@2708
|
1744 } else {
|
nicholas@2708
|
1745 if (this.bufferReady(id) === false) {
|
nicholas@2708
|
1746 console.log("Cannot play. Buffer not ready");
|
nicholas@2708
|
1747 return;
|
nicholas@2498
|
1748 }
|
nicholas@2708
|
1749 playNormal.call(this, id);
|
nicholas@2498
|
1750 }
|
nicholas@2498
|
1751 interfaceContext.playhead.start();
|
nicholas@2498
|
1752 }
|
nicholas@2498
|
1753 };
|
nicholas@2224
|
1754
|
nicholas@2498
|
1755 this.stop = function () {
|
nicholas@2498
|
1756 // Send stop and reset command to all playback buffers
|
nicholas@2498
|
1757 if (this.status == 1) {
|
nicholas@2498
|
1758 var setTime = audioContext.currentTime + 0.1;
|
nicholas@2708
|
1759 this.audioObjects.forEach(function (a) {
|
nicholas@2708
|
1760 a.stop(setTime);
|
nicholas@2708
|
1761 });
|
nicholas@2498
|
1762 interfaceContext.playhead.stop();
|
nicholas@2498
|
1763 }
|
nicholas@2498
|
1764 };
|
nicholas@2498
|
1765
|
nicholas@2498
|
1766 this.newTrack = function (element) {
|
nicholas@2498
|
1767 // Pull data from given URL into new audio buffer
|
nicholas@2498
|
1768 // URLs must either be from the same source OR be setup to 'Access-Control-Allow-Origin'
|
nicholas@2498
|
1769
|
nicholas@2498
|
1770 // Create the audioObject with ID of the new track length;
|
nicholas@2708
|
1771 var audioObjectId = this.audioObjects.length;
|
nicholas@2498
|
1772 this.audioObjects[audioObjectId] = new audioObject(audioObjectId);
|
nicholas@2498
|
1773
|
nicholas@2498
|
1774 // Check if audioObject buffer is currently stored by full URL
|
nicholas@2498
|
1775 var URL = testState.currentStateMap.hostURL + element.url;
|
nicholas@2708
|
1776 var buffer = this.buffers.find(function (buffObj) {
|
nicholas@2708
|
1777 return buffObj.hasUrl(URL);
|
nicholas@2708
|
1778 });
|
nicholas@2708
|
1779 if (buffer === undefined) {
|
nicholas@2498
|
1780 console.log("[WARN]: Buffer was not loaded in pre-test! " + URL);
|
nicholas@2498
|
1781 buffer = new this.bufferObj();
|
nicholas@2224
|
1782 this.buffers.push(buffer);
|
nicholas@2498
|
1783 buffer.getMedia(URL);
|
nicholas@2498
|
1784 }
|
nicholas@2498
|
1785 this.audioObjects[audioObjectId].specification = element;
|
nicholas@2498
|
1786 this.audioObjects[audioObjectId].url = URL;
|
nicholas@2498
|
1787 // Obtain store node
|
nicholas@2498
|
1788 var aeNodes = this.pageStore.XMLDOM.getElementsByTagName('audioelement');
|
nicholas@2498
|
1789 for (var i = 0; i < aeNodes.length; i++) {
|
nicholas@2498
|
1790 if (aeNodes[i].getAttribute("ref") == element.id) {
|
nicholas@2498
|
1791 this.audioObjects[audioObjectId].storeDOM = aeNodes[i];
|
nicholas@2498
|
1792 break;
|
nicholas@2498
|
1793 }
|
nicholas@2498
|
1794 }
|
nicholas@2224
|
1795 buffer.registerAudioObject(this.audioObjects[audioObjectId]);
|
nicholas@2498
|
1796 return this.audioObjects[audioObjectId];
|
nicholas@2498
|
1797 };
|
nicholas@2498
|
1798
|
nicholas@2498
|
1799 this.newTestPage = function (audioHolderObject, store) {
|
nicholas@2498
|
1800 this.pageStore = store;
|
nicholas@2351
|
1801 this.pageSpecification = audioHolderObject;
|
nicholas@2498
|
1802 this.status = 0;
|
nicholas@2498
|
1803 this.audioObjectsReady = false;
|
nicholas@2498
|
1804 this.metric.reset();
|
nicholas@2708
|
1805 this.buffers.forEach(function (buffer) {
|
nicholas@2708
|
1806 buffer.users = [];
|
nicholas@2708
|
1807 });
|
nicholas@2498
|
1808 this.audioObjects = [];
|
nicholas@2224
|
1809 this.timer = new timer();
|
nicholas@2224
|
1810 this.loopPlayback = audioHolderObject.loop;
|
nicholas@2351
|
1811 this.synchPlayback = audioHolderObject.synchronous;
|
nicholas@2498
|
1812 };
|
nicholas@2498
|
1813
|
nicholas@2498
|
1814 this.checkAllPlayed = function () {
|
nicholas@2708
|
1815 var arr = [];
|
nicholas@2498
|
1816 for (var id = 0; id < this.audioObjects.length; id++) {
|
nicholas@2708
|
1817 if (this.audioObjects[id].metric.wasListenedTo === false) {
|
nicholas@2498
|
1818 arr.push(this.audioObjects[id].id);
|
nicholas@2498
|
1819 }
|
nicholas@2498
|
1820 }
|
nicholas@2498
|
1821 return arr;
|
nicholas@2498
|
1822 };
|
nicholas@2498
|
1823
|
nicholas@2498
|
1824 this.checkAllReady = function () {
|
nicholas@2498
|
1825 var ready = true;
|
nicholas@2498
|
1826 for (var i = 0; i < this.audioObjects.length; i++) {
|
nicholas@2708
|
1827 if (this.audioObjects[i].state === 0) {
|
nicholas@2498
|
1828 // Track not ready
|
nicholas@2498
|
1829 console.log('WAIT -- audioObject ' + i + ' not ready yet!');
|
nicholas@2498
|
1830 ready = false;
|
nicholas@2708
|
1831 }
|
nicholas@2498
|
1832 }
|
nicholas@2498
|
1833 return ready;
|
nicholas@2498
|
1834 };
|
nicholas@2498
|
1835
|
nicholas@2498
|
1836 this.setSynchronousLoop = function () {
|
nicholas@2570
|
1837 // Pads the signals so they are all exactly the same duration
|
nicholas@2570
|
1838 // Get the duration of the longest signal.
|
nicholas@2570
|
1839 var duration = 0;
|
nicholas@2498
|
1840 var maxId;
|
nicholas@2498
|
1841 for (var i = 0; i < this.audioObjects.length; i++) {
|
nicholas@2570
|
1842 if (duration < this.audioObjects[i].buffer.buffer.duration) {
|
nicholas@2570
|
1843 duration = this.audioObjects[i].buffer.buffer.duration;
|
nicholas@2498
|
1844 maxId = i;
|
nicholas@2498
|
1845 }
|
nicholas@2498
|
1846 }
|
nicholas@2498
|
1847 // Extract the audio and zero-pad
|
nicholas@2708
|
1848 this.audioObjects.forEach(function (ao) {
|
nicholas@2570
|
1849 if (ao.buffer.buffer.duration !== duration) {
|
nicholas@2570
|
1850 ao.buffer.buffer = ao.buffer.copyBuffer(0, duration - ao.buffer.buffer.duration);
|
nicholas@2500
|
1851 }
|
nicholas@2708
|
1852 });
|
nicholas@2498
|
1853 };
|
nicholas@2498
|
1854
|
nicholas@2498
|
1855 this.bufferReady = function (id) {
|
nicholas@2498
|
1856 if (this.checkAllReady()) {
|
nicholas@2498
|
1857 if (this.synchPlayback) {
|
nicholas@2498
|
1858 this.setSynchronousLoop();
|
nicholas@2498
|
1859 }
|
nicholas@2460
|
1860 this.status = 1;
|
nicholas@2460
|
1861 return true;
|
nicholas@2460
|
1862 }
|
nicholas@2460
|
1863 return false;
|
nicholas@2224
|
1864 };
|
nicholas@2498
|
1865
|
nicholas@2224
|
1866 }
|
nicholas@2224
|
1867
|
nicholas@2224
|
1868 function audioObject(id) {
|
nicholas@2498
|
1869 // The main buffer object with common control nodes to the AudioEngine
|
nicholas@2498
|
1870
|
nicholas@2823
|
1871 var playCounter = 0;
|
nicholas@2823
|
1872
|
nicholas@2708
|
1873 this.specification = undefined;
|
nicholas@2498
|
1874 this.id = id;
|
nicholas@2498
|
1875 this.state = 0; // 0 - no data, 1 - ready
|
nicholas@2498
|
1876 this.url = null; // Hold the URL given for the output back to the results.
|
nicholas@2498
|
1877 this.metric = new metricTracker(this);
|
nicholas@2498
|
1878 this.storeDOM = null;
|
nicholas@2498
|
1879
|
nicholas@2498
|
1880 // Bindings for GUI
|
nicholas@2498
|
1881 this.interfaceDOM = null;
|
nicholas@2498
|
1882 this.commentDOM = null;
|
nicholas@2498
|
1883
|
nicholas@2498
|
1884 // Create a buffer and external gain control to allow internal patching of effects and volume leveling.
|
nicholas@2498
|
1885 this.bufferNode = undefined;
|
nicholas@2498
|
1886 this.outputGain = audioContext.createGain();
|
nicholas@2498
|
1887
|
nicholas@2498
|
1888 this.onplayGain = 1.0;
|
nicholas@2498
|
1889
|
nicholas@2498
|
1890 // Connect buffer to the audio graph
|
nicholas@2498
|
1891 this.outputGain.connect(audioEngineContext.outputGain);
|
nicholas@2508
|
1892 audioEngineContext.nullBufferSource.connect(this.outputGain);
|
nicholas@2498
|
1893
|
nicholas@2498
|
1894 // the audiobuffer is not designed for multi-start playback
|
nicholas@2498
|
1895 // When stopeed, the buffer node is deleted and recreated with the stored buffer.
|
nicholas@2708
|
1896 this.buffer = undefined;
|
nicholas@2498
|
1897
|
nicholas@2498
|
1898 this.bufferLoaded = function (callee) {
|
nicholas@2498
|
1899 // Called by the associated buffer when it has finished loading, will then 'bind' the buffer to the
|
nicholas@2498
|
1900 // audioObject and trigger the interfaceDOM.enable() function for user feedback
|
nicholas@2224
|
1901 if (callee.status == -1) {
|
nicholas@2224
|
1902 // ERROR
|
nicholas@2224
|
1903 this.state = -1;
|
nicholas@2708
|
1904 if (this.interfaceDOM !== null) {
|
nicholas@2498
|
1905 this.interfaceDOM.error();
|
nicholas@2498
|
1906 }
|
nicholas@2224
|
1907 this.buffer = callee;
|
nicholas@2224
|
1908 return;
|
nicholas@2224
|
1909 }
|
nicholas@2224
|
1910 this.buffer = callee;
|
nicholas@2224
|
1911 var preSilenceTime = this.specification.preSilence || this.specification.parent.preSilence || specification.preSilence || 0.0;
|
nicholas@2224
|
1912 var postSilenceTime = this.specification.postSilence || this.specification.parent.postSilence || specification.postSilence || 0.0;
|
nicholas@2460
|
1913 var startTime = this.specification.startTime;
|
nicholas@2460
|
1914 var stopTime = this.specification.stopTime;
|
nicholas@2460
|
1915 var copybuffer = new callee.constructor();
|
nicholas@2500
|
1916
|
nicholas@2500
|
1917 copybuffer.buffer = callee.cropBuffer(startTime || 0, stopTime || callee.buffer.duration);
|
nicholas@2708
|
1918 if (preSilenceTime !== 0 || postSilenceTime !== 0) {
|
nicholas@2500
|
1919 copybuffer.buffer = copybuffer.copyBuffer(preSilenceTime, postSilenceTime);
|
nicholas@2460
|
1920 }
|
nicholas@2500
|
1921
|
nicholas@2660
|
1922 copybuffer.buffer.lufs = callee.buffer.lufs;
|
nicholas@2500
|
1923 this.buffer = copybuffer;
|
nicholas@2498
|
1924
|
nicholas@2661
|
1925 var targetLUFS = this.specification.loudness || this.specification.parent.loudness || specification.loudness;
|
nicholas@2498
|
1926 if (typeof targetLUFS === "number" && isFinite(targetLUFS)) {
|
nicholas@2498
|
1927 this.buffer.buffer.playbackGain = decibelToLinear(targetLUFS - this.buffer.buffer.lufs);
|
nicholas@2498
|
1928 } else {
|
nicholas@2498
|
1929 this.buffer.buffer.playbackGain = 1.0;
|
nicholas@2498
|
1930 }
|
nicholas@2708
|
1931 if (this.interfaceDOM !== null) {
|
nicholas@2498
|
1932 this.interfaceDOM.enable();
|
nicholas@2498
|
1933 }
|
nicholas@2498
|
1934 this.onplayGain = decibelToLinear(this.specification.gain) * (this.buffer.buffer.playbackGain || 1.0);
|
nicholas@2498
|
1935 this.storeDOM.setAttribute('playGain', linearToDecibel(this.onplayGain));
|
nicholas@2460
|
1936 this.state = 1;
|
nicholas@2460
|
1937 audioEngineContext.bufferReady(id);
|
nicholas@2498
|
1938 };
|
nicholas@2498
|
1939
|
nicholas@2498
|
1940 this.bindInterface = function (interfaceObject) {
|
nicholas@2498
|
1941 this.interfaceDOM = interfaceObject;
|
nicholas@2498
|
1942 this.metric.initialise(interfaceObject.getValue());
|
nicholas@2498
|
1943 if (this.state == 1) {
|
nicholas@2498
|
1944 this.interfaceDOM.enable();
|
nicholas@2498
|
1945 } else if (this.state == -1) {
|
nicholas@2224
|
1946 // ERROR
|
nicholas@2224
|
1947 this.interfaceDOM.error();
|
nicholas@2224
|
1948 return;
|
nicholas@2224
|
1949 }
|
nicholas@2498
|
1950 this.storeDOM.setAttribute('presentedId', interfaceObject.getPresentedId());
|
nicholas@2498
|
1951 };
|
nicholas@2498
|
1952
|
nicholas@2498
|
1953 this.loopStart = function (setTime) {
|
nicholas@2498
|
1954 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, setTime);
|
nicholas@2498
|
1955 this.metric.startListening(audioEngineContext.timer.getTestTime());
|
nicholas@2224
|
1956 this.interfaceDOM.startPlayback();
|
nicholas@2498
|
1957 };
|
nicholas@2498
|
1958
|
nicholas@2498
|
1959 this.loopStop = function (setTime) {
|
nicholas@2708
|
1960 if (this.outputGain.gain.value !== 0.0) {
|
nicholas@2498
|
1961 this.outputGain.gain.linearRampToValueAtTime(0.0, setTime);
|
nicholas@2498
|
1962 this.metric.stopListening(audioEngineContext.timer.getTestTime());
|
nicholas@2498
|
1963 }
|
nicholas@2224
|
1964 this.interfaceDOM.stopPlayback();
|
nicholas@2498
|
1965 };
|
nicholas@2498
|
1966
|
nicholas@2498
|
1967 this.play = function (startTime) {
|
nicholas@2708
|
1968 if (this.bufferNode === undefined && this.buffer.buffer !== undefined) {
|
nicholas@2823
|
1969 playCounter++;
|
nicholas@2498
|
1970 this.bufferNode = audioContext.createBufferSource();
|
nicholas@2498
|
1971 this.bufferNode.owner = this;
|
nicholas@2498
|
1972 this.bufferNode.connect(this.outputGain);
|
nicholas@2498
|
1973 this.bufferNode.buffer = this.buffer.buffer;
|
nicholas@2498
|
1974 this.bufferNode.loop = audioEngineContext.loopPlayback;
|
nicholas@2498
|
1975 this.bufferNode.onended = function (event) {
|
nicholas@2498
|
1976 // Safari does not like using 'this' to reference the calling object!
|
nicholas@2498
|
1977 //event.currentTarget.owner.metric.stopListening(audioEngineContext.timer.getTestTime(),event.currentTarget.owner.getCurrentPosition());
|
nicholas@2708
|
1978 if (event.currentTarget !== null) {
|
n@2924
|
1979 event.currentTarget.owner.stop(audioContext.currentTime + 0.1);
|
nicholas@2224
|
1980 }
|
nicholas@2498
|
1981 };
|
nicholas@2508
|
1982 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
|
nicholas@2498
|
1983 if (!audioEngineContext.loopPlayback || !audioEngineContext.synchPlayback) {
|
nicholas@2498
|
1984 this.metric.startListening(audioEngineContext.timer.getTestTime());
|
n@2924
|
1985 if (this.outputGain.gain.value !== this.onplayGain) {
|
n@2924
|
1986 this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, startTime + Number(specification.crossFade));
|
n@2924
|
1987 }
|
nicholas@2224
|
1988 this.interfaceDOM.startPlayback();
|
nicholas@2498
|
1989 } else {
|
nicholas@2529
|
1990 this.outputGain.gain.linearRampToValueAtTime(0.0, startTime);
|
nicholas@2224
|
1991 }
|
nicholas@2499
|
1992 if (audioEngineContext.loopPlayback) {
|
nicholas@2499
|
1993 this.bufferNode.loopStart = this.specification.startTime || 0;
|
nicholas@2499
|
1994 this.bufferNode.loopEnd = this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration;
|
nicholas@2499
|
1995 this.bufferNode.start(startTime);
|
nicholas@2499
|
1996 } else {
|
nicholas@2499
|
1997 this.bufferNode.start(startTime, this.specification.startTime || 0, this.specification.stopTime - this.specification.startTime || this.buffer.buffer.duration);
|
nicholas@2499
|
1998 }
|
nicholas@2224
|
1999 this.bufferNode.playbackStartTime = audioEngineContext.timer.getTestTime();
|
nicholas@2498
|
2000 }
|
nicholas@2498
|
2001 };
|
nicholas@2498
|
2002
|
nicholas@2498
|
2003 this.stop = function (stopTime) {
|
nicholas@2224
|
2004 this.outputGain.gain.cancelScheduledValues(audioContext.currentTime);
|
nicholas@2708
|
2005 if (this.bufferNode !== undefined) {
|
nicholas@2498
|
2006 this.metric.stopListening(audioEngineContext.timer.getTestTime(), this.getCurrentPosition());
|
nicholas@2498
|
2007 this.bufferNode.stop(stopTime);
|
nicholas@2498
|
2008 this.bufferNode = undefined;
|
nicholas@2498
|
2009 }
|
nicholas@2529
|
2010 this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime);
|
nicholas@2224
|
2011 this.interfaceDOM.stopPlayback();
|
nicholas@2498
|
2012 };
|
nicholas@2498
|
2013
|
nicholas@2498
|
2014 this.getCurrentPosition = function () {
|
nicholas@2498
|
2015 var time = audioEngineContext.timer.getTestTime();
|
nicholas@2708
|
2016 if (this.bufferNode !== undefined) {
|
nicholas@2498
|
2017 var position = (time - this.bufferNode.playbackStartTime) % this.buffer.buffer.duration;
|
nicholas@2498
|
2018 if (isNaN(position)) {
|
nicholas@2498
|
2019 return 0;
|
nicholas@2498
|
2020 }
|
nicholas@2224
|
2021 return position;
|
nicholas@2498
|
2022 } else {
|
nicholas@2498
|
2023 return 0;
|
nicholas@2498
|
2024 }
|
nicholas@2498
|
2025 };
|
nicholas@2498
|
2026
|
nicholas@2498
|
2027 this.exportXMLDOM = function () {
|
nicholas@2498
|
2028 var file = storage.document.createElement('file');
|
nicholas@2498
|
2029 file.setAttribute('sampleRate', this.buffer.buffer.sampleRate);
|
nicholas@2498
|
2030 file.setAttribute('channels', this.buffer.buffer.numberOfChannels);
|
nicholas@2498
|
2031 file.setAttribute('sampleCount', this.buffer.buffer.length);
|
nicholas@2498
|
2032 file.setAttribute('duration', this.buffer.buffer.duration);
|
nicholas@2498
|
2033 this.storeDOM.appendChild(file);
|
nicholas@2498
|
2034 if (this.specification.type != 'outside-reference') {
|
nicholas@2498
|
2035 var interfaceXML = this.interfaceDOM.exportXMLDOM(this);
|
nicholas@2708
|
2036 if (interfaceXML !== null) {
|
nicholas@2708
|
2037 if (interfaceXML.length === undefined) {
|
nicholas@2498
|
2038 this.storeDOM.appendChild(interfaceXML);
|
nicholas@2498
|
2039 } else {
|
nicholas@2498
|
2040 for (var i = 0; i < interfaceXML.length; i++) {
|
nicholas@2498
|
2041 this.storeDOM.appendChild(interfaceXML[i]);
|
nicholas@2498
|
2042 }
|
nicholas@2498
|
2043 }
|
nicholas@2498
|
2044 }
|
nicholas@2708
|
2045 if (this.commentDOM !== null) {
|
nicholas@2498
|
2046 this.storeDOM.appendChild(this.commentDOM.exportXMLDOM(this));
|
nicholas@2498
|
2047 }
|
nicholas@2498
|
2048 }
|
nicholas@2708
|
2049 this.metric.exportXMLDOM(this.storeDOM.getElementsByTagName('metric')[0]);
|
nicholas@2498
|
2050 };
|
nicholas@2823
|
2051
|
nicholas@2823
|
2052 Object.defineProperties(this, {
|
nicholas@2823
|
2053 "numberOfPlays": {
|
nicholas@2823
|
2054 'get': function () {
|
nicholas@2823
|
2055 return playCounter;
|
nicholas@2823
|
2056 },
|
nicholas@2823
|
2057 'set': function () {
|
nicholas@2823
|
2058 return playCounter;
|
nicholas@2823
|
2059 }
|
nicholas@2823
|
2060 }
|
nicholas@2823
|
2061 });
|
nicholas@2224
|
2062 }
|
nicholas@2224
|
2063
|
nicholas@2498
|
2064 function timer() {
|
nicholas@2498
|
2065 /* Timer object used in audioEngine to keep track of session timings
|
nicholas@2498
|
2066 * Uses the timer of the web audio API, so sample resolution
|
nicholas@2498
|
2067 */
|
nicholas@2498
|
2068 this.testStarted = false;
|
nicholas@2498
|
2069 this.testStartTime = 0;
|
nicholas@2498
|
2070 this.testDuration = 0;
|
nicholas@2498
|
2071 this.minimumTestTime = 0; // No minimum test time
|
nicholas@2498
|
2072 this.startTest = function () {
|
nicholas@2708
|
2073 if (this.testStarted === false) {
|
nicholas@2498
|
2074 this.testStartTime = audioContext.currentTime;
|
nicholas@2498
|
2075 this.testStarted = true;
|
nicholas@2498
|
2076 this.updateTestTime();
|
nicholas@2498
|
2077 audioEngineContext.metric.initialiseTest();
|
nicholas@2498
|
2078 }
|
nicholas@2498
|
2079 };
|
nicholas@2498
|
2080 this.stopTest = function () {
|
nicholas@2498
|
2081 if (this.testStarted) {
|
nicholas@2498
|
2082 this.testDuration = this.getTestTime();
|
nicholas@2498
|
2083 this.testStarted = false;
|
nicholas@2498
|
2084 } else {
|
nicholas@2498
|
2085 console.log('ERR: Test tried to end before beginning');
|
nicholas@2498
|
2086 }
|
nicholas@2498
|
2087 };
|
nicholas@2498
|
2088 this.updateTestTime = function () {
|
nicholas@2498
|
2089 if (this.testStarted) {
|
nicholas@2498
|
2090 this.testDuration = audioContext.currentTime - this.testStartTime;
|
nicholas@2498
|
2091 }
|
nicholas@2498
|
2092 };
|
nicholas@2498
|
2093 this.getTestTime = function () {
|
nicholas@2498
|
2094 this.updateTestTime();
|
nicholas@2498
|
2095 return this.testDuration;
|
nicholas@2498
|
2096 };
|
nicholas@2224
|
2097 }
|
nicholas@2224
|
2098
|
nicholas@2498
|
2099 function sessionMetrics(engine, specification) {
|
nicholas@2498
|
2100 /* Used by audioEngine to link to audioObjects to minimise the timer call timers;
|
nicholas@2498
|
2101 */
|
nicholas@2498
|
2102 this.engine = engine;
|
nicholas@2498
|
2103 this.lastClicked = -1;
|
nicholas@2498
|
2104 this.data = -1;
|
nicholas@2498
|
2105 this.reset = function () {
|
nicholas@2498
|
2106 this.lastClicked = -1;
|
nicholas@2498
|
2107 this.data = -1;
|
nicholas@2498
|
2108 };
|
nicholas@2498
|
2109
|
nicholas@2498
|
2110 this.enableElementInitialPosition = false;
|
nicholas@2498
|
2111 this.enableElementListenTracker = false;
|
nicholas@2498
|
2112 this.enableElementTimer = false;
|
nicholas@2498
|
2113 this.enableElementTracker = false;
|
nicholas@2498
|
2114 this.enableFlagListenedTo = false;
|
nicholas@2498
|
2115 this.enableFlagMoved = false;
|
nicholas@2498
|
2116 this.enableTestTimer = false;
|
nicholas@2498
|
2117 // Obtain the metrics enabled
|
nicholas@2498
|
2118 for (var i = 0; i < specification.metrics.enabled.length; i++) {
|
nicholas@2498
|
2119 var node = specification.metrics.enabled[i];
|
nicholas@2498
|
2120 switch (node) {
|
nicholas@2498
|
2121 case 'testTimer':
|
nicholas@2498
|
2122 this.enableTestTimer = true;
|
nicholas@2498
|
2123 break;
|
nicholas@2498
|
2124 case 'elementTimer':
|
nicholas@2498
|
2125 this.enableElementTimer = true;
|
nicholas@2498
|
2126 break;
|
nicholas@2498
|
2127 case 'elementTracker':
|
nicholas@2498
|
2128 this.enableElementTracker = true;
|
nicholas@2498
|
2129 break;
|
nicholas@2498
|
2130 case 'elementListenTracker':
|
nicholas@2498
|
2131 this.enableElementListenTracker = true;
|
nicholas@2498
|
2132 break;
|
nicholas@2498
|
2133 case 'elementInitialPosition':
|
nicholas@2498
|
2134 this.enableElementInitialPosition = true;
|
nicholas@2498
|
2135 break;
|
nicholas@2498
|
2136 case 'elementFlagListenedTo':
|
nicholas@2498
|
2137 this.enableFlagListenedTo = true;
|
nicholas@2498
|
2138 break;
|
nicholas@2498
|
2139 case 'elementFlagMoved':
|
nicholas@2498
|
2140 this.enableFlagMoved = true;
|
nicholas@2498
|
2141 break;
|
nicholas@2498
|
2142 case 'elementFlagComments':
|
nicholas@2498
|
2143 this.enableFlagComments = true;
|
nicholas@2498
|
2144 break;
|
nicholas@2498
|
2145 }
|
nicholas@2498
|
2146 }
|
nicholas@2498
|
2147 this.initialiseTest = function () {};
|
nicholas@2224
|
2148 }
|
nicholas@2224
|
2149
|
nicholas@2498
|
2150 function metricTracker(caller) {
|
nicholas@2498
|
2151 /* Custom object to track and collect metric data
|
nicholas@2498
|
2152 * Used only inside the audioObjects object.
|
nicholas@2498
|
2153 */
|
nicholas@2498
|
2154
|
nicholas@2498
|
2155 this.listenedTimer = 0;
|
nicholas@2498
|
2156 this.listenStart = 0;
|
nicholas@2498
|
2157 this.listenHold = false;
|
nicholas@2498
|
2158 this.initialPosition = -1;
|
nicholas@2498
|
2159 this.movementTracker = [];
|
nicholas@2498
|
2160 this.listenTracker = [];
|
nicholas@2498
|
2161 this.wasListenedTo = false;
|
nicholas@2498
|
2162 this.wasMoved = false;
|
nicholas@2498
|
2163 this.hasComments = false;
|
nicholas@2498
|
2164 this.parent = caller;
|
nicholas@2498
|
2165
|
nicholas@2498
|
2166 this.initialise = function (position) {
|
nicholas@2498
|
2167 if (this.initialPosition == -1) {
|
nicholas@2498
|
2168 this.initialPosition = position;
|
nicholas@2498
|
2169 this.moved(0, position);
|
nicholas@2498
|
2170 }
|
nicholas@2498
|
2171 };
|
nicholas@2498
|
2172
|
nicholas@2498
|
2173 this.moved = function (time, position) {
|
nicholas@2498
|
2174 if (time > 0) {
|
nicholas@2498
|
2175 this.wasMoved = true;
|
nicholas@2498
|
2176 }
|
nicholas@2498
|
2177 this.movementTracker[this.movementTracker.length] = [time, position];
|
nicholas@2498
|
2178 };
|
nicholas@2498
|
2179
|
nicholas@2498
|
2180 this.startListening = function (time) {
|
nicholas@2708
|
2181 if (this.listenHold === false) {
|
nicholas@2498
|
2182 this.wasListenedTo = true;
|
nicholas@2498
|
2183 this.listenStart = time;
|
nicholas@2498
|
2184 this.listenHold = true;
|
nicholas@2498
|
2185
|
nicholas@2498
|
2186 var evnt = document.createElement('event');
|
nicholas@2498
|
2187 var testTime = document.createElement('testTime');
|
nicholas@2498
|
2188 testTime.setAttribute('start', time);
|
nicholas@2498
|
2189 var bufferTime = document.createElement('bufferTime');
|
nicholas@2498
|
2190 bufferTime.setAttribute('start', this.parent.getCurrentPosition());
|
nicholas@2498
|
2191 evnt.appendChild(testTime);
|
nicholas@2498
|
2192 evnt.appendChild(bufferTime);
|
nicholas@2498
|
2193 this.listenTracker.push(evnt);
|
nicholas@2498
|
2194
|
nicholas@2498
|
2195 console.log('slider ' + this.parent.id + ' played (' + time + ')'); // DEBUG/SAFETY: show played slider id
|
nicholas@2498
|
2196 }
|
nicholas@2498
|
2197 };
|
nicholas@2498
|
2198
|
nicholas@2498
|
2199 this.stopListening = function (time, bufferStopTime) {
|
nicholas@2708
|
2200 if (this.listenHold === true) {
|
nicholas@2498
|
2201 var diff = time - this.listenStart;
|
nicholas@2498
|
2202 this.listenedTimer += (diff);
|
nicholas@2498
|
2203 this.listenStart = 0;
|
nicholas@2498
|
2204 this.listenHold = false;
|
nicholas@2498
|
2205
|
nicholas@2498
|
2206 var evnt = this.listenTracker[this.listenTracker.length - 1];
|
nicholas@2498
|
2207 var testTime = evnt.getElementsByTagName('testTime')[0];
|
nicholas@2498
|
2208 var bufferTime = evnt.getElementsByTagName('bufferTime')[0];
|
nicholas@2498
|
2209 testTime.setAttribute('stop', time);
|
nicholas@2708
|
2210 if (bufferStopTime === undefined) {
|
nicholas@2498
|
2211 bufferTime.setAttribute('stop', this.parent.getCurrentPosition());
|
nicholas@2498
|
2212 } else {
|
nicholas@2498
|
2213 bufferTime.setAttribute('stop', bufferStopTime);
|
nicholas@2498
|
2214 }
|
nicholas@2498
|
2215 console.log('slider ' + this.parent.id + ' played for (' + diff + ')'); // DEBUG/SAFETY: show played slider id
|
nicholas@2498
|
2216 }
|
nicholas@2498
|
2217 };
|
nicholas@2498
|
2218
|
nicholas@2708
|
2219 function exportElementTimer(parentElement) {
|
nicholas@2708
|
2220 var mElementTimer = storage.document.createElement('metricresult');
|
nicholas@2708
|
2221 mElementTimer.setAttribute('name', 'enableElementTimer');
|
nicholas@2708
|
2222 mElementTimer.textContent = this.listenedTimer;
|
nicholas@2708
|
2223 parentElement.appendChild(mElementTimer);
|
nicholas@2708
|
2224 return mElementTimer;
|
nicholas@2708
|
2225 }
|
nicholas@2708
|
2226
|
nicholas@2708
|
2227 function exportElementTrack(parentElement) {
|
nicholas@2708
|
2228 var elementTrackerFull = storage.document.createElement('metricresult');
|
nicholas@2708
|
2229 elementTrackerFull.setAttribute('name', 'elementTrackerFull');
|
nicholas@2708
|
2230 for (var k = 0; k < this.movementTracker.length; k++) {
|
nicholas@2708
|
2231 var timePos = storage.document.createElement('movement');
|
nicholas@2708
|
2232 timePos.setAttribute("time", this.movementTracker[k][0]);
|
nicholas@2708
|
2233 timePos.setAttribute("value", this.movementTracker[k][1]);
|
nicholas@2708
|
2234 elementTrackerFull.appendChild(timePos);
|
nicholas@2708
|
2235 }
|
nicholas@2708
|
2236 parentElement.appendChild(elementTrackerFull);
|
nicholas@2708
|
2237 return elementTrackerFull;
|
nicholas@2708
|
2238 }
|
nicholas@2708
|
2239
|
nicholas@2708
|
2240 function exportElementListenTracker(parentElement) {
|
nicholas@2708
|
2241 var elementListenTracker = storage.document.createElement('metricresult');
|
nicholas@2708
|
2242 elementListenTracker.setAttribute('name', 'elementListenTracker');
|
nicholas@2708
|
2243 for (var k = 0; k < this.listenTracker.length; k++) {
|
nicholas@2708
|
2244 elementListenTracker.appendChild(this.listenTracker[k]);
|
nicholas@2708
|
2245 }
|
nicholas@2708
|
2246 parentElement.appendChild(elementListenTracker);
|
nicholas@2708
|
2247 return elementListenTracker;
|
nicholas@2708
|
2248 }
|
nicholas@2708
|
2249
|
nicholas@2708
|
2250 function exportElementInitialPosition(parentElement) {
|
nicholas@2708
|
2251 var elementInitial = storage.document.createElement('metricresult');
|
nicholas@2708
|
2252 elementInitial.setAttribute('name', 'elementInitialPosition');
|
nicholas@2708
|
2253 elementInitial.textContent = this.initialPosition;
|
nicholas@2708
|
2254 parentElement.appendChild(elementInitial);
|
nicholas@2708
|
2255 return elementInitial;
|
nicholas@2708
|
2256 }
|
nicholas@2708
|
2257
|
nicholas@2708
|
2258 function exportFlagListenedTo(parentElement) {
|
nicholas@2708
|
2259 var flagListenedTo = storage.document.createElement('metricresult');
|
nicholas@2708
|
2260 flagListenedTo.setAttribute('name', 'elementFlagListenedTo');
|
nicholas@2708
|
2261 flagListenedTo.textContent = this.wasListenedTo;
|
nicholas@2708
|
2262 parentElement.appendChild(flagListenedTo);
|
nicholas@2708
|
2263 return flagListenedTo;
|
nicholas@2708
|
2264 }
|
nicholas@2708
|
2265
|
nicholas@2708
|
2266 function exportFlagMoved(parentElement) {
|
nicholas@2708
|
2267 var flagMoved = storage.document.createElement('metricresult');
|
nicholas@2708
|
2268 flagMoved.setAttribute('name', 'elementFlagMoved');
|
nicholas@2708
|
2269 flagMoved.textContent = this.wasMoved;
|
nicholas@2708
|
2270 parentElement.appendChild(flagMoved);
|
nicholas@2708
|
2271 return flagMoved;
|
nicholas@2708
|
2272 }
|
nicholas@2708
|
2273
|
nicholas@2708
|
2274 function exportFlagComments(parentElement) {
|
nicholas@2708
|
2275 var flagComments = storage.document.createElement('metricresult');
|
nicholas@2708
|
2276 flagComments.setAttribute('name', 'elementFlagComments');
|
nicholas@2708
|
2277 if (this.parent.commentDOM === null) {
|
nicholas@2708
|
2278 flagComments.textContent = 'false';
|
nicholas@2708
|
2279 } else if (this.parent.commentDOM.textContent.length === 0) {
|
nicholas@2708
|
2280 flagComments.textContent = 'false';
|
nicholas@2708
|
2281 } else {
|
nicholas@2708
|
2282 flagComments.textContet = 'true';
|
nicholas@2708
|
2283 }
|
nicholas@2708
|
2284 parentElement.appendChild(flagComments);
|
nicholas@2708
|
2285 return flagComments;
|
nicholas@2708
|
2286 }
|
nicholas@2708
|
2287
|
nicholas@2708
|
2288 this.exportXMLDOM = function (parentElement) {
|
nicholas@2708
|
2289 var elems = [];
|
nicholas@2498
|
2290 if (audioEngineContext.metric.enableElementTimer) {
|
nicholas@2708
|
2291 elems.push(exportElementTimer.call(this, parentElement));
|
nicholas@2498
|
2292 }
|
nicholas@2498
|
2293 if (audioEngineContext.metric.enableElementTracker) {
|
nicholas@2708
|
2294 elems.push(exportElementTrack.call(this, parentElement));
|
nicholas@2498
|
2295 }
|
nicholas@2498
|
2296 if (audioEngineContext.metric.enableElementListenTracker) {
|
nicholas@2708
|
2297 elems.push(exportElementListenTracker.call(this, parentElement));
|
nicholas@2498
|
2298 }
|
nicholas@2498
|
2299 if (audioEngineContext.metric.enableElementInitialPosition) {
|
nicholas@2708
|
2300 elems.push(exportElementInitialPosition.call(this, parentElement));
|
nicholas@2498
|
2301 }
|
nicholas@2498
|
2302 if (audioEngineContext.metric.enableFlagListenedTo) {
|
nicholas@2708
|
2303 elems.push(exportFlagListenedTo.call(this, parentElement));
|
nicholas@2498
|
2304 }
|
nicholas@2498
|
2305 if (audioEngineContext.metric.enableFlagMoved) {
|
nicholas@2708
|
2306 elems.push(exportFlagMoved.call(this, parentElement));
|
nicholas@2498
|
2307 }
|
nicholas@2498
|
2308 if (audioEngineContext.metric.enableFlagComments) {
|
nicholas@2708
|
2309 elems.push(exportFlagComments.call(this, parentElement));
|
nicholas@2498
|
2310 }
|
nicholas@2708
|
2311 return elems;
|
nicholas@2498
|
2312 };
|
nicholas@2224
|
2313 }
|
nicholas@2498
|
2314
|
nicholas@2224
|
2315 function Interface(specificationObject) {
|
nicholas@2498
|
2316 // This handles the bindings between the interface and the audioEngineContext;
|
nicholas@2498
|
2317 this.specification = specificationObject;
|
nicholas@2498
|
2318 this.insertPoint = document.getElementById("topLevelBody");
|
nicholas@2498
|
2319
|
nicholas@2498
|
2320 this.newPage = function (audioHolderObject, store) {
|
nicholas@2498
|
2321 audioEngineContext.newTestPage(audioHolderObject, store);
|
nicholas@2498
|
2322 interfaceContext.commentBoxes.deleteCommentBoxes();
|
nicholas@2498
|
2323 interfaceContext.deleteCommentQuestions();
|
nicholas@2498
|
2324 loadTest(audioHolderObject, store);
|
nicholas@2498
|
2325 };
|
nicholas@2498
|
2326
|
nicholas@2498
|
2327 // Bounded by interface!!
|
nicholas@2498
|
2328 // Interface object MUST have an exportXMLDOM method which returns the various DOM levels
|
nicholas@2498
|
2329 // For example, APE returns the slider position normalised in a <value> tag.
|
nicholas@2498
|
2330 this.interfaceObjects = [];
|
nicholas@2498
|
2331 this.interfaceObject = function () {};
|
nicholas@2498
|
2332
|
nicholas@2498
|
2333 this.resizeWindow = function (event) {
|
nicholas@2498
|
2334 popup.resize(event);
|
nicholas@2352
|
2335 this.volume.resize();
|
nicholas@2360
|
2336 this.lightbox.resize();
|
n@2718
|
2337 this.commentBoxes.boxes.forEach(function (elem) {
|
nicholas@2708
|
2338 elem.resize();
|
nicholas@2708
|
2339 });
|
nicholas@2708
|
2340 this.commentQuestions.forEach(function (elem) {
|
nicholas@2708
|
2341 elem.resize();
|
nicholas@2708
|
2342 });
|
nicholas@2498
|
2343 try {
|
nicholas@2498
|
2344 resizeWindow(event);
|
nicholas@2498
|
2345 } catch (err) {
|
nicholas@2498
|
2346 console.log("Warning - Interface does not have Resize option");
|
nicholas@2498
|
2347 console.log(err);
|
nicholas@2498
|
2348 }
|
nicholas@2498
|
2349 };
|
nicholas@2498
|
2350
|
nicholas@2498
|
2351 this.returnNavigator = function () {
|
nicholas@2498
|
2352 var node = storage.document.createElement("navigator");
|
nicholas@2498
|
2353 var platform = storage.document.createElement("platform");
|
nicholas@2498
|
2354 platform.textContent = navigator.platform;
|
nicholas@2498
|
2355 var vendor = storage.document.createElement("vendor");
|
nicholas@2498
|
2356 vendor.textContent = navigator.vendor;
|
nicholas@2498
|
2357 var userAgent = storage.document.createElement("uagent");
|
nicholas@2498
|
2358 userAgent.textContent = navigator.userAgent;
|
nicholas@2224
|
2359 var screen = storage.document.createElement("window");
|
nicholas@2498
|
2360 screen.setAttribute('innerWidth', window.innerWidth);
|
nicholas@2498
|
2361 screen.setAttribute('innerHeight', window.innerHeight);
|
nicholas@2498
|
2362 node.appendChild(platform);
|
nicholas@2498
|
2363 node.appendChild(vendor);
|
nicholas@2498
|
2364 node.appendChild(userAgent);
|
nicholas@2224
|
2365 node.appendChild(screen);
|
nicholas@2498
|
2366 return node;
|
nicholas@2498
|
2367 };
|
nicholas@2498
|
2368
|
nicholas@2498
|
2369 this.returnDateNode = function () {
|
nicholas@2224
|
2370 // Create an XML Node for the Date and Time a test was conducted
|
nicholas@2224
|
2371 // Structure is
|
nicholas@2224
|
2372 // <datetime>
|
nicholas@2224
|
2373 // <date year="##" month="##" day="##">DD/MM/YY</date>
|
nicholas@2224
|
2374 // <time hour="##" minute="##" sec="##">HH:MM:SS</time>
|
nicholas@2224
|
2375 // </datetime>
|
nicholas@2224
|
2376 var dateTime = new Date();
|
nicholas@2224
|
2377 var hold = storage.document.createElement("datetime");
|
nicholas@2224
|
2378 var date = storage.document.createElement("date");
|
nicholas@2224
|
2379 var time = storage.document.createElement("time");
|
nicholas@2498
|
2380 date.setAttribute('year', dateTime.getFullYear());
|
nicholas@2498
|
2381 date.setAttribute('month', dateTime.getMonth() + 1);
|
nicholas@2498
|
2382 date.setAttribute('day', dateTime.getDate());
|
nicholas@2498
|
2383 time.setAttribute('hour', dateTime.getHours());
|
nicholas@2498
|
2384 time.setAttribute('minute', dateTime.getMinutes());
|
nicholas@2498
|
2385 time.setAttribute('secs', dateTime.getSeconds());
|
nicholas@2498
|
2386
|
nicholas@2224
|
2387 hold.appendChild(date);
|
nicholas@2224
|
2388 hold.appendChild(time);
|
nicholas@2224
|
2389 return hold;
|
nicholas@2224
|
2390
|
nicholas@2708
|
2391 };
|
nicholas@2498
|
2392
|
nicholas@2360
|
2393 this.lightbox = {
|
nicholas@2360
|
2394 parent: this,
|
nicholas@2360
|
2395 root: document.createElement("div"),
|
nicholas@2360
|
2396 content: document.createElement("div"),
|
nicholas@2360
|
2397 accept: document.createElement("button"),
|
nicholas@2360
|
2398 blanker: document.createElement("div"),
|
nicholas@2498
|
2399 post: function (type, message) {
|
nicholas@2498
|
2400 switch (type) {
|
nicholas@2360
|
2401 case "Error":
|
nicholas@2360
|
2402 this.content.className = "lightbox-error";
|
nicholas@2360
|
2403 break;
|
nicholas@2360
|
2404 case "Warning":
|
nicholas@2360
|
2405 this.content.className = "lightbox-warning";
|
nicholas@2360
|
2406 break;
|
nicholas@2360
|
2407 default:
|
nicholas@2360
|
2408 this.content.className = "lightbox-message";
|
nicholas@2360
|
2409 break;
|
nicholas@2360
|
2410 }
|
nicholas@2360
|
2411 var msg = document.createElement("p");
|
nicholas@2360
|
2412 msg.textContent = message;
|
nicholas@2360
|
2413 this.content.appendChild(msg);
|
nicholas@2360
|
2414 this.show();
|
nicholas@2360
|
2415 },
|
nicholas@2498
|
2416 show: function () {
|
nicholas@2360
|
2417 this.root.style.visibility = "visible";
|
nicholas@2360
|
2418 this.blanker.style.visibility = "visible";
|
n@2914
|
2419 this.accept.focus();
|
nicholas@2360
|
2420 },
|
nicholas@2498
|
2421 clear: function () {
|
nicholas@2360
|
2422 this.root.style.visibility = "";
|
nicholas@2360
|
2423 this.blanker.style.visibility = "";
|
nicholas@2360
|
2424 this.content.textContent = "";
|
nicholas@2360
|
2425 },
|
nicholas@2498
|
2426 handleEvent: function (event) {
|
nicholas@2360
|
2427 if (event.currentTarget == this.accept) {
|
nicholas@2360
|
2428 this.clear();
|
nicholas@2360
|
2429 }
|
nicholas@2360
|
2430 },
|
nicholas@2498
|
2431 resize: function (event) {
|
nicholas@2498
|
2432 this.root.style.left = (window.innerWidth / 2) - 250 + 'px';
|
n@2915
|
2433 },
|
n@2915
|
2434 isVisible: function () {
|
n@2915
|
2435 return this.root.style.visibility == "visible";
|
nicholas@2360
|
2436 }
|
nicholas@2708
|
2437 };
|
nicholas@2498
|
2438
|
nicholas@2360
|
2439 this.lightbox.root.appendChild(this.lightbox.content);
|
nicholas@2360
|
2440 this.lightbox.root.appendChild(this.lightbox.accept);
|
nicholas@2360
|
2441 this.lightbox.root.className = "popupHolder";
|
nicholas@2360
|
2442 this.lightbox.root.id = "lightbox-root";
|
nicholas@2360
|
2443 this.lightbox.accept.className = "popupButton";
|
nicholas@2360
|
2444 this.lightbox.accept.style.bottom = "10px";
|
nicholas@2360
|
2445 this.lightbox.accept.textContent = "OK";
|
nicholas@2360
|
2446 this.lightbox.accept.style.left = "237.5px";
|
nicholas@2498
|
2447 this.lightbox.accept.addEventListener("click", this.lightbox);
|
nicholas@2360
|
2448 this.lightbox.blanker.className = "testHalt";
|
nicholas@2360
|
2449 this.lightbox.blanker.id = "lightbox-blanker";
|
nicholas@2360
|
2450 document.getElementsByTagName("body")[0].appendChild(this.lightbox.root);
|
nicholas@2360
|
2451 document.getElementsByTagName("body")[0].appendChild(this.lightbox.blanker);
|
nicholas@2498
|
2452
|
nicholas@2712
|
2453 this.commentBoxes = (function () {
|
nicholas@2712
|
2454 var commentBoxes = {};
|
nicholas@2712
|
2455 commentBoxes.boxes = [];
|
nicholas@2712
|
2456 commentBoxes.injectPoint = null;
|
nicholas@2712
|
2457 commentBoxes.elementCommentBox = function (audioObject) {
|
nicholas@2224
|
2458 var element = audioObject.specification;
|
nicholas@2224
|
2459 this.audioObject = audioObject;
|
nicholas@2224
|
2460 this.id = audioObject.id;
|
nicholas@2224
|
2461 var audioHolderObject = audioObject.specification.parent;
|
nicholas@2224
|
2462 // Create document objects to hold the comment boxes
|
nicholas@2224
|
2463 this.trackComment = document.createElement('div');
|
nicholas@2224
|
2464 this.trackComment.className = 'comment-div';
|
nicholas@2498
|
2465 this.trackComment.id = 'comment-div-' + audioObject.id;
|
nicholas@2224
|
2466 // Create a string next to each comment asking for a comment
|
nicholas@2224
|
2467 this.trackString = document.createElement('span');
|
nicholas@2498
|
2468 this.trackString.innerHTML = audioHolderObject.commentBoxPrefix + ' ' + audioObject.interfaceDOM.getPresentedId();
|
nicholas@2224
|
2469 // Create the HTML5 comment box 'textarea'
|
nicholas@2224
|
2470 this.trackCommentBox = document.createElement('textarea');
|
nicholas@2224
|
2471 this.trackCommentBox.rows = '4';
|
nicholas@2224
|
2472 this.trackCommentBox.cols = '100';
|
nicholas@2498
|
2473 this.trackCommentBox.name = 'trackComment' + audioObject.id;
|
nicholas@2224
|
2474 this.trackCommentBox.className = 'trackComment';
|
nicholas@2224
|
2475 var br = document.createElement('br');
|
nicholas@2224
|
2476 // Add to the holder.
|
nicholas@2224
|
2477 this.trackComment.appendChild(this.trackString);
|
nicholas@2224
|
2478 this.trackComment.appendChild(br);
|
nicholas@2224
|
2479 this.trackComment.appendChild(this.trackCommentBox);
|
nicholas@2224
|
2480
|
nicholas@2498
|
2481 this.exportXMLDOM = function () {
|
nicholas@2224
|
2482 var root = document.createElement('comment');
|
nicholas@2224
|
2483 var question = document.createElement('question');
|
nicholas@2224
|
2484 question.textContent = this.trackString.textContent;
|
nicholas@2224
|
2485 var response = document.createElement('response');
|
nicholas@2224
|
2486 response.textContent = this.trackCommentBox.value;
|
nicholas@2498
|
2487 console.log("Comment frag-" + this.id + ": " + response.textContent);
|
nicholas@2224
|
2488 root.appendChild(question);
|
nicholas@2224
|
2489 root.appendChild(response);
|
nicholas@2224
|
2490 return root;
|
nicholas@2224
|
2491 };
|
nicholas@2498
|
2492 this.resize = function () {
|
nicholas@2498
|
2493 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2494 if (boxwidth >= 600) {
|
nicholas@2224
|
2495 boxwidth = 600;
|
nicholas@2498
|
2496 } else if (boxwidth < 400) {
|
nicholas@2224
|
2497 boxwidth = 400;
|
nicholas@2224
|
2498 }
|
nicholas@2498
|
2499 this.trackComment.style.width = boxwidth + "px";
|
nicholas@2498
|
2500 this.trackCommentBox.style.width = boxwidth - 6 + "px";
|
nicholas@2224
|
2501 };
|
nicholas@2224
|
2502 this.resize();
|
nicholas@2725
|
2503 this.highlight = function (state) {
|
nicholas@2725
|
2504 if (state === true) {
|
nicholas@2725
|
2505 $(this.trackComment).addClass("comment-box-playing");
|
nicholas@2725
|
2506 } else {
|
nicholas@2725
|
2507 $(this.trackComment).removeClass("comment-box-playing");
|
nicholas@2725
|
2508 }
|
nicholas@2725
|
2509 };
|
nicholas@2224
|
2510 };
|
nicholas@2712
|
2511 commentBoxes.createCommentBox = function (audioObject) {
|
nicholas@2224
|
2512 var node = new this.elementCommentBox(audioObject);
|
nicholas@2224
|
2513 this.boxes.push(node);
|
nicholas@2224
|
2514 audioObject.commentDOM = node;
|
nicholas@2224
|
2515 return node;
|
nicholas@2224
|
2516 };
|
nicholas@2712
|
2517 commentBoxes.sortCommentBoxes = function () {
|
nicholas@2498
|
2518 this.boxes.sort(function (a, b) {
|
nicholas@2498
|
2519 return a.id - b.id;
|
nicholas@2498
|
2520 });
|
nicholas@2224
|
2521 };
|
nicholas@2224
|
2522
|
nicholas@2712
|
2523 commentBoxes.showCommentBoxes = function (inject, sort) {
|
nicholas@2224
|
2524 this.injectPoint = inject;
|
nicholas@2498
|
2525 if (sort) {
|
nicholas@2498
|
2526 this.sortCommentBoxes();
|
nicholas@2498
|
2527 }
|
nicholas@2708
|
2528 this.boxes.forEach(function (box) {
|
nicholas@2224
|
2529 inject.appendChild(box.trackComment);
|
nicholas@2708
|
2530 });
|
nicholas@2224
|
2531 };
|
nicholas@2224
|
2532
|
nicholas@2712
|
2533 commentBoxes.deleteCommentBoxes = function () {
|
nicholas@2708
|
2534 if (this.injectPoint !== null) {
|
nicholas@2708
|
2535 this.boxes.forEach(function (box) {
|
nicholas@2224
|
2536 this.injectPoint.removeChild(box.trackComment);
|
nicholas@2708
|
2537 }, this);
|
nicholas@2224
|
2538 this.injectPoint = null;
|
nicholas@2224
|
2539 }
|
nicholas@2224
|
2540 this.boxes = [];
|
nicholas@2224
|
2541 };
|
nicholas@2725
|
2542 commentBoxes.highlightById = function (id) {
|
nicholas@2725
|
2543 if (id === undefined || typeof id !== "number" || id >= this.boxes.length) {
|
nicholas@2725
|
2544 console.log("Error - Invalid id");
|
nicholas@2725
|
2545 id = -1;
|
nicholas@2725
|
2546 }
|
nicholas@2725
|
2547 this.boxes.forEach(function (a) {
|
nicholas@2725
|
2548 if (a.id === id) {
|
nicholas@2725
|
2549 a.highlight(true);
|
nicholas@2725
|
2550 } else {
|
nicholas@2725
|
2551 a.highlight(false);
|
nicholas@2725
|
2552 }
|
nicholas@2725
|
2553 });
|
nicholas@2725
|
2554 };
|
nicholas@2712
|
2555 return commentBoxes;
|
nicholas@2712
|
2556 })();
|
nicholas@2498
|
2557
|
nicholas@2498
|
2558 this.commentQuestions = [];
|
nicholas@2498
|
2559
|
nicholas@2498
|
2560 this.commentBox = function (commentQuestion) {
|
nicholas@2498
|
2561 this.specification = commentQuestion;
|
nicholas@2498
|
2562 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2563 this.holder = document.createElement('div');
|
nicholas@2498
|
2564 this.holder.className = 'comment-div';
|
nicholas@2498
|
2565 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2566 this.string = document.createElement('span');
|
nicholas@2498
|
2567 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2568 // Create the HTML5 comment box 'textarea'
|
nicholas@2498
|
2569 this.textArea = document.createElement('textarea');
|
nicholas@2498
|
2570 this.textArea.rows = '4';
|
nicholas@2498
|
2571 this.textArea.cols = '100';
|
nicholas@2498
|
2572 this.textArea.className = 'trackComment';
|
nicholas@2498
|
2573 var br = document.createElement('br');
|
nicholas@2498
|
2574 // Add to the holder.
|
nicholas@2498
|
2575 this.holder.appendChild(this.string);
|
nicholas@2498
|
2576 this.holder.appendChild(br);
|
nicholas@2498
|
2577 this.holder.appendChild(this.textArea);
|
nicholas@2498
|
2578
|
nicholas@2498
|
2579 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2580 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2581 root.id = this.specification.id;
|
nicholas@2498
|
2582 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2583 console.log("Question: " + this.string.textContent);
|
nicholas@2498
|
2584 console.log("Response: " + root.textContent);
|
nicholas@2224
|
2585 var question = storePoint.parent.document.createElement('question');
|
nicholas@2224
|
2586 question.textContent = this.string.textContent;
|
nicholas@2224
|
2587 var response = storePoint.parent.document.createElement('response');
|
nicholas@2224
|
2588 response.textContent = this.textArea.value;
|
nicholas@2224
|
2589 root.appendChild(question);
|
nicholas@2224
|
2590 root.appendChild(response);
|
nicholas@2224
|
2591 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2592 return root;
|
nicholas@2498
|
2593 };
|
nicholas@2498
|
2594 this.resize = function () {
|
nicholas@2498
|
2595 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2596 if (boxwidth >= 600) {
|
nicholas@2498
|
2597 boxwidth = 600;
|
nicholas@2498
|
2598 } else if (boxwidth < 400) {
|
nicholas@2498
|
2599 boxwidth = 400;
|
nicholas@2498
|
2600 }
|
nicholas@2498
|
2601 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2602 this.textArea.style.width = boxwidth - 6 + "px";
|
nicholas@2498
|
2603 };
|
nicholas@2498
|
2604 this.resize();
|
nicholas@2498
|
2605 };
|
nicholas@2498
|
2606
|
nicholas@2498
|
2607 this.radioBox = function (commentQuestion) {
|
nicholas@2498
|
2608 this.specification = commentQuestion;
|
nicholas@2498
|
2609 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2610 this.holder = document.createElement('div');
|
nicholas@2498
|
2611 this.holder.className = 'comment-div';
|
nicholas@2498
|
2612 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2613 this.string = document.createElement('span');
|
nicholas@2498
|
2614 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2615 // Add to the holder.
|
nicholas@2498
|
2616 this.holder.appendChild(this.string);
|
nicholas@2498
|
2617 this.options = [];
|
nicholas@2498
|
2618 this.inputs = document.createElement('div');
|
nicholas@2711
|
2619 this.inputs.className = "comment-checkbox-inputs-holder";
|
nicholas@2498
|
2620
|
nicholas@2498
|
2621 var optCount = commentQuestion.options.length;
|
nicholas@2711
|
2622 for (var i = 0; i < optCount; i++) {
|
nicholas@2498
|
2623 var div = document.createElement('div');
|
nicholas@2711
|
2624 div.className = "comment-checkbox-inputs-flex";
|
nicholas@2722
|
2625
|
nicholas@2711
|
2626 var span = document.createElement('span');
|
nicholas@2711
|
2627 span.textContent = commentQuestion.options[i].text;
|
nicholas@2711
|
2628 span.className = 'comment-radio-span';
|
nicholas@2711
|
2629 div.appendChild(span);
|
nicholas@2722
|
2630
|
nicholas@2498
|
2631 var input = document.createElement('input');
|
nicholas@2498
|
2632 input.type = 'radio';
|
nicholas@2498
|
2633 input.name = commentQuestion.id;
|
nicholas@2711
|
2634 input.setAttribute('setvalue', commentQuestion.options[i].name);
|
nicholas@2498
|
2635 input.className = 'comment-radio';
|
nicholas@2498
|
2636 div.appendChild(input);
|
nicholas@2722
|
2637
|
nicholas@2498
|
2638 this.inputs.appendChild(div);
|
nicholas@2498
|
2639 this.options.push(input);
|
nicholas@2498
|
2640 }
|
nicholas@2498
|
2641 this.holder.appendChild(this.inputs);
|
nicholas@2498
|
2642
|
nicholas@2498
|
2643 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2644 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2645 root.id = this.specification.id;
|
nicholas@2498
|
2646 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2647 var question = document.createElement('question');
|
nicholas@2498
|
2648 question.textContent = this.string.textContent;
|
nicholas@2498
|
2649 var response = document.createElement('response');
|
nicholas@2498
|
2650 var i = 0;
|
nicholas@2708
|
2651 while (this.options[i].checked === false) {
|
nicholas@2498
|
2652 i++;
|
nicholas@2498
|
2653 if (i >= this.options.length) {
|
nicholas@2498
|
2654 break;
|
nicholas@2498
|
2655 }
|
nicholas@2498
|
2656 }
|
nicholas@2498
|
2657 if (i >= this.options.length) {
|
nicholas@2498
|
2658 response.textContent = 'null';
|
nicholas@2498
|
2659 } else {
|
nicholas@2498
|
2660 response.textContent = this.options[i].getAttribute('setvalue');
|
nicholas@2498
|
2661 response.setAttribute('number', i);
|
nicholas@2498
|
2662 }
|
nicholas@2498
|
2663 console.log('Comment: ' + question.textContent);
|
nicholas@2498
|
2664 console.log('Response: ' + response.textContent);
|
nicholas@2498
|
2665 root.appendChild(question);
|
nicholas@2498
|
2666 root.appendChild(response);
|
nicholas@2224
|
2667 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2668 return root;
|
nicholas@2498
|
2669 };
|
nicholas@2498
|
2670 this.resize = function () {
|
nicholas@2498
|
2671 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2672 if (boxwidth >= 600) {
|
nicholas@2498
|
2673 boxwidth = 600;
|
nicholas@2498
|
2674 } else if (boxwidth < 400) {
|
nicholas@2498
|
2675 boxwidth = 400;
|
nicholas@2498
|
2676 }
|
nicholas@2498
|
2677 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2678 };
|
nicholas@2498
|
2679 this.resize();
|
nicholas@2498
|
2680 };
|
nicholas@2498
|
2681
|
nicholas@2498
|
2682 this.checkboxBox = function (commentQuestion) {
|
nicholas@2498
|
2683 this.specification = commentQuestion;
|
nicholas@2498
|
2684 // Create document objects to hold the comment boxes
|
nicholas@2498
|
2685 this.holder = document.createElement('div');
|
nicholas@2498
|
2686 this.holder.className = 'comment-div';
|
nicholas@2498
|
2687 // Create a string next to each comment asking for a comment
|
nicholas@2498
|
2688 this.string = document.createElement('span');
|
nicholas@2498
|
2689 this.string.innerHTML = commentQuestion.statement;
|
nicholas@2498
|
2690 // Add to the holder.
|
nicholas@2498
|
2691 this.holder.appendChild(this.string);
|
nicholas@2498
|
2692 this.options = [];
|
nicholas@2498
|
2693 this.inputs = document.createElement('div');
|
nicholas@2294
|
2694 this.inputs.className = "comment-checkbox-inputs-holder";
|
nicholas@2498
|
2695
|
nicholas@2498
|
2696 var optCount = commentQuestion.options.length;
|
nicholas@2498
|
2697 for (var i = 0; i < optCount; i++) {
|
nicholas@2498
|
2698 var div = document.createElement('div');
|
nicholas@2711
|
2699 div.className = "comment-checkbox-inputs-flex";
|
nicholas@2722
|
2700
|
nicholas@2711
|
2701 var span = document.createElement('span');
|
nicholas@2711
|
2702 span.textContent = commentQuestion.options[i].text;
|
nicholas@2711
|
2703 span.className = 'comment-radio-span';
|
nicholas@2711
|
2704 div.appendChild(span);
|
nicholas@2722
|
2705
|
nicholas@2498
|
2706 var input = document.createElement('input');
|
nicholas@2498
|
2707 input.type = 'checkbox';
|
nicholas@2498
|
2708 input.name = commentQuestion.id;
|
nicholas@2498
|
2709 input.setAttribute('setvalue', commentQuestion.options[i].name);
|
nicholas@2498
|
2710 input.className = 'comment-radio';
|
nicholas@2498
|
2711 div.appendChild(input);
|
nicholas@2722
|
2712
|
nicholas@2498
|
2713 this.inputs.appendChild(div);
|
nicholas@2498
|
2714 this.options.push(input);
|
nicholas@2498
|
2715 }
|
nicholas@2498
|
2716 this.holder.appendChild(this.inputs);
|
nicholas@2498
|
2717
|
nicholas@2498
|
2718 this.exportXMLDOM = function (storePoint) {
|
nicholas@2498
|
2719 var root = storePoint.parent.document.createElement('comment');
|
nicholas@2498
|
2720 root.id = this.specification.id;
|
nicholas@2498
|
2721 root.setAttribute('type', this.specification.type);
|
nicholas@2498
|
2722 var question = document.createElement('question');
|
nicholas@2498
|
2723 question.textContent = this.string.textContent;
|
nicholas@2498
|
2724 root.appendChild(question);
|
nicholas@2498
|
2725 console.log('Comment: ' + question.textContent);
|
nicholas@2498
|
2726 for (var i = 0; i < this.options.length; i++) {
|
nicholas@2498
|
2727 var response = document.createElement('response');
|
nicholas@2498
|
2728 response.textContent = this.options[i].checked;
|
nicholas@2498
|
2729 response.setAttribute('name', this.options[i].getAttribute('setvalue'));
|
nicholas@2498
|
2730 root.appendChild(response);
|
nicholas@2498
|
2731 console.log('Response ' + response.getAttribute('name') + ': ' + response.textContent);
|
nicholas@2498
|
2732 }
|
nicholas@2224
|
2733 storePoint.XMLDOM.appendChild(root);
|
nicholas@2498
|
2734 return root;
|
nicholas@2498
|
2735 };
|
nicholas@2498
|
2736 this.resize = function () {
|
nicholas@2498
|
2737 var boxwidth = (window.innerWidth - 100) / 2;
|
nicholas@2498
|
2738 if (boxwidth >= 600) {
|
nicholas@2498
|
2739 boxwidth = 600;
|
nicholas@2498
|
2740 } else if (boxwidth < 400) {
|
nicholas@2498
|
2741 boxwidth = 400;
|
nicholas@2498
|
2742 }
|
nicholas@2498
|
2743 this.holder.style.width = boxwidth + "px";
|
nicholas@2498
|
2744 };
|
nicholas@2498
|
2745 this.resize();
|
nicholas@2498
|
2746 };
|
nicholas@2498
|
2747
|
n@2579
|
2748 this.sliderBox = function (commentQuestion) {
|
n@2579
|
2749 this.specification = commentQuestion;
|
n@2579
|
2750 this.holder = document.createElement("div");
|
n@2579
|
2751 this.holder.className = 'comment-div';
|
n@2579
|
2752 this.string = document.createElement("span");
|
n@2579
|
2753 this.string.innerHTML = commentQuestion.statement;
|
n@2579
|
2754 this.slider = document.createElement("input");
|
n@2579
|
2755 this.slider.type = "range";
|
n@2579
|
2756 this.slider.min = commentQuestion.min;
|
n@2579
|
2757 this.slider.max = commentQuestion.max;
|
n@2579
|
2758 this.slider.step = commentQuestion.step;
|
n@2579
|
2759 this.slider.value = commentQuestion.value;
|
n@2579
|
2760 var br = document.createElement('br');
|
n@2579
|
2761
|
n@2580
|
2762 var textHolder = document.createElement("div");
|
n@2580
|
2763 textHolder.className = "comment-slider-text-holder";
|
n@2580
|
2764
|
n@2580
|
2765 this.leftText = document.createElement("span");
|
n@2580
|
2766 this.leftText.textContent = commentQuestion.leftText;
|
n@2580
|
2767 this.rightText = document.createElement("span");
|
n@2580
|
2768 this.rightText.textContent = commentQuestion.rightText;
|
n@2580
|
2769 textHolder.appendChild(this.leftText);
|
n@2580
|
2770 textHolder.appendChild(this.rightText);
|
n@2580
|
2771
|
n@2579
|
2772 this.holder.appendChild(this.string);
|
n@2579
|
2773 this.holder.appendChild(br);
|
n@2579
|
2774 this.holder.appendChild(this.slider);
|
n@2580
|
2775 this.holder.appendChild(textHolder);
|
n@2579
|
2776
|
n@2579
|
2777 this.exportXMLDOM = function (storePoint) {
|
n@2579
|
2778 var root = storePoint.parent.document.createElement('comment');
|
n@2579
|
2779 root.id = this.specification.id;
|
n@2579
|
2780 root.setAttribute('type', this.specification.type);
|
n@2579
|
2781 console.log("Question: " + this.string.textContent);
|
n@2579
|
2782 console.log("Response: " + this.slider.value);
|
n@2579
|
2783 var question = storePoint.parent.document.createElement('question');
|
n@2579
|
2784 question.textContent = this.string.textContent;
|
n@2579
|
2785 var response = storePoint.parent.document.createElement('response');
|
n@2579
|
2786 response.textContent = this.slider.value;
|
n@2579
|
2787 root.appendChild(question);
|
n@2579
|
2788 root.appendChild(response);
|
n@2579
|
2789 storePoint.XMLDOM.appendChild(root);
|
n@2579
|
2790 return root;
|
n@2579
|
2791 };
|
n@2579
|
2792 this.resize = function () {
|
n@2579
|
2793 var boxwidth = (window.innerWidth - 100) / 2;
|
n@2579
|
2794 if (boxwidth >= 600) {
|
n@2579
|
2795 boxwidth = 600;
|
n@2579
|
2796 } else if (boxwidth < 400) {
|
n@2579
|
2797 boxwidth = 400;
|
n@2579
|
2798 }
|
n@2579
|
2799 this.holder.style.width = boxwidth + "px";
|
n@2579
|
2800 this.slider.style.width = boxwidth - 24 + "px";
|
n@2579
|
2801 };
|
n@2579
|
2802 this.resize();
|
n@2579
|
2803 };
|
n@2579
|
2804
|
nicholas@2498
|
2805 this.createCommentQuestion = function (element) {
|
nicholas@2498
|
2806 var node;
|
nicholas@2498
|
2807 if (element.type == 'question') {
|
nicholas@2498
|
2808 node = new this.commentBox(element);
|
nicholas@2498
|
2809 } else if (element.type == 'radio') {
|
nicholas@2498
|
2810 node = new this.radioBox(element);
|
nicholas@2498
|
2811 } else if (element.type == 'checkbox') {
|
nicholas@2498
|
2812 node = new this.checkboxBox(element);
|
n@2579
|
2813 } else if (element.type == 'slider') {
|
n@2579
|
2814 node = new this.sliderBox(element);
|
nicholas@2498
|
2815 }
|
nicholas@2498
|
2816 this.commentQuestions.push(node);
|
nicholas@2498
|
2817 return node;
|
nicholas@2498
|
2818 };
|
nicholas@2498
|
2819
|
nicholas@2498
|
2820 this.deleteCommentQuestions = function () {
|
nicholas@2498
|
2821 this.commentQuestions = [];
|
nicholas@2498
|
2822 };
|
nicholas@2498
|
2823
|
nicholas@2498
|
2824 this.outsideReferenceDOM = function (audioObject, index, inject) {
|
nicholas@2224
|
2825 this.parent = audioObject;
|
nicholas@2224
|
2826 this.outsideReferenceHolder = document.createElement('button');
|
nicholas@2224
|
2827 this.outsideReferenceHolder.className = 'outside-reference';
|
nicholas@2498
|
2828 this.outsideReferenceHolder.setAttribute('track-id', index);
|
nicholas@2409
|
2829 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
|
nicholas@2224
|
2830 this.outsideReferenceHolder.disabled = true;
|
nicholas@2708
|
2831 this.handleEvent = function (event) {
|
nicholas@2708
|
2832 audioEngineContext.play(this.parent.id);
|
nicholas@2224
|
2833 };
|
nicholas@2708
|
2834 this.outsideReferenceHolder.addEventListener("click", this);
|
nicholas@2224
|
2835 inject.appendChild(this.outsideReferenceHolder);
|
nicholas@2498
|
2836 this.enable = function () {
|
nicholas@2498
|
2837 if (this.parent.state == 1) {
|
nicholas@2224
|
2838 this.outsideReferenceHolder.disabled = false;
|
nicholas@2224
|
2839 }
|
nicholas@2224
|
2840 };
|
nicholas@2498
|
2841 this.updateLoading = function (progress) {
|
nicholas@2498
|
2842 if (progress != 100) {
|
nicholas@2224
|
2843 progress = String(progress);
|
nicholas@2224
|
2844 progress = progress.split('.')[0];
|
nicholas@2498
|
2845 this.outsideReferenceHolder.textContent = progress + '%';
|
nicholas@2224
|
2846 } else {
|
nicholas@2409
|
2847 this.outsideReferenceHolder.textContent = this.parent.specification.label || "Reference";
|
nicholas@2224
|
2848 }
|
nicholas@2224
|
2849 };
|
nicholas@2498
|
2850 this.startPlayback = function () {
|
nicholas@2224
|
2851 // Called when playback has begun
|
nicholas@2224
|
2852 $('.track-slider').removeClass('track-slider-playing');
|
nicholas@2224
|
2853 $('.comment-div').removeClass('comment-box-playing');
|
nicholas@2224
|
2854 this.outsideReferenceHolder.style.backgroundColor = "#FDD";
|
nicholas@2224
|
2855 };
|
nicholas@2498
|
2856 this.stopPlayback = function () {
|
nicholas@2224
|
2857 // Called when playback has stopped. This gets called even if playback never started!
|
nicholas@2224
|
2858 this.outsideReferenceHolder.style.backgroundColor = "";
|
nicholas@2224
|
2859 };
|
nicholas@2498
|
2860 this.exportXMLDOM = function (audioObject) {
|
nicholas@2224
|
2861 return null;
|
nicholas@2224
|
2862 };
|
nicholas@2498
|
2863 this.getValue = function () {
|
nicholas@2224
|
2864 return 0;
|
nicholas@2224
|
2865 };
|
nicholas@2498
|
2866 this.getPresentedId = function () {
|
nicholas@2409
|
2867 return this.parent.specification.label || "Reference";
|
nicholas@2224
|
2868 };
|
nicholas@2498
|
2869 this.canMove = function () {
|
nicholas@2224
|
2870 return false;
|
nicholas@2224
|
2871 };
|
nicholas@2498
|
2872 this.error = function () {
|
nicholas@2498
|
2873 // audioObject has an error!!
|
nicholas@2224
|
2874 this.outsideReferenceHolder.textContent = "Error";
|
nicholas@2224
|
2875 this.outsideReferenceHolder.style.backgroundColor = "#F00";
|
nicholas@2708
|
2876 };
|
nicholas@2708
|
2877 };
|
nicholas@2498
|
2878
|
nicholas@2712
|
2879 this.playhead = (function () {
|
nicholas@2722
|
2880 var playhead = {};
|
nicholas@2712
|
2881 playhead.object = document.createElement('div');
|
nicholas@2712
|
2882 playhead.object.className = 'playhead';
|
nicholas@2712
|
2883 playhead.object.align = 'left';
|
nicholas@2498
|
2884 var curTime = document.createElement('div');
|
nicholas@2498
|
2885 curTime.style.width = '50px';
|
nicholas@2712
|
2886 playhead.curTimeSpan = document.createElement('span');
|
nicholas@2712
|
2887 playhead.curTimeSpan.textContent = '00:00';
|
nicholas@2712
|
2888 curTime.appendChild(playhead.curTimeSpan);
|
nicholas@2712
|
2889 playhead.object.appendChild(curTime);
|
nicholas@2712
|
2890 playhead.scrubberTrack = document.createElement('div');
|
nicholas@2712
|
2891 playhead.scrubberTrack.className = 'playhead-scrub-track';
|
nicholas@2498
|
2892
|
nicholas@2712
|
2893 playhead.scrubberHead = document.createElement('div');
|
nicholas@2712
|
2894 playhead.scrubberHead.id = 'playhead-scrubber';
|
nicholas@2712
|
2895 playhead.scrubberTrack.appendChild(playhead.scrubberHead);
|
nicholas@2712
|
2896 playhead.object.appendChild(playhead.scrubberTrack);
|
nicholas@2498
|
2897
|
nicholas@2712
|
2898 playhead.timePerPixel = 0;
|
nicholas@2712
|
2899 playhead.maxTime = 0;
|
nicholas@2498
|
2900
|
nicholas@2712
|
2901 playhead.playbackObject = undefined;
|
nicholas@2498
|
2902
|
nicholas@2712
|
2903 playhead.setTimePerPixel = function (audioObject) {
|
nicholas@2498
|
2904 //maxTime must be in seconds
|
nicholas@2498
|
2905 this.playbackObject = audioObject;
|
nicholas@2498
|
2906 this.maxTime = audioObject.buffer.buffer.duration;
|
nicholas@2498
|
2907 var width = 490; //500 - 10, 5 each side of the tracker head
|
nicholas@2498
|
2908 this.timePerPixel = this.maxTime / 490;
|
nicholas@2498
|
2909 if (this.maxTime < 60) {
|
nicholas@2498
|
2910 this.curTimeSpan.textContent = '0.00';
|
nicholas@2498
|
2911 } else {
|
nicholas@2498
|
2912 this.curTimeSpan.textContent = '00:00';
|
nicholas@2498
|
2913 }
|
nicholas@2498
|
2914 };
|
nicholas@2498
|
2915
|
nicholas@2712
|
2916 playhead.update = function () {
|
nicholas@2498
|
2917 // Update the playhead position, startPlay must be called
|
nicholas@2498
|
2918 if (this.timePerPixel > 0) {
|
nicholas@2498
|
2919 var time = this.playbackObject.getCurrentPosition();
|
nicholas@2498
|
2920 if (time > 0 && time < this.maxTime) {
|
nicholas@2498
|
2921 var width = 490;
|
nicholas@2498
|
2922 var pix = Math.floor(time / this.timePerPixel);
|
nicholas@2498
|
2923 this.scrubberHead.style.left = pix + 'px';
|
nicholas@2498
|
2924 if (this.maxTime > 60.0) {
|
nicholas@2498
|
2925 var secs = time % 60;
|
nicholas@2498
|
2926 var mins = Math.floor((time - secs) / 60);
|
nicholas@2498
|
2927 secs = secs.toString();
|
nicholas@2498
|
2928 secs = secs.substr(0, 2);
|
nicholas@2498
|
2929 mins = mins.toString();
|
nicholas@2498
|
2930 this.curTimeSpan.textContent = mins + ':' + secs;
|
nicholas@2498
|
2931 } else {
|
nicholas@2498
|
2932 time = time.toString();
|
nicholas@2498
|
2933 this.curTimeSpan.textContent = time.substr(0, 4);
|
nicholas@2498
|
2934 }
|
nicholas@2498
|
2935 } else {
|
nicholas@2498
|
2936 this.scrubberHead.style.left = '0px';
|
nicholas@2498
|
2937 if (this.maxTime < 60) {
|
nicholas@2498
|
2938 this.curTimeSpan.textContent = '0.00';
|
nicholas@2498
|
2939 } else {
|
nicholas@2498
|
2940 this.curTimeSpan.textContent = '00:00';
|
nicholas@2498
|
2941 }
|
nicholas@2498
|
2942 }
|
nicholas@2498
|
2943 }
|
nicholas@2817
|
2944 if (this.playbackObject !== undefined && this.interval === undefined) {
|
nicholas@2817
|
2945 window.requestAnimationFrame(this.update.bind(this));
|
nicholas@2817
|
2946 }
|
nicholas@2498
|
2947 };
|
nicholas@2498
|
2948
|
nicholas@2712
|
2949 playhead.interval = undefined;
|
nicholas@2498
|
2950
|
nicholas@2712
|
2951 playhead.start = function () {
|
nicholas@2708
|
2952 if (this.playbackObject !== undefined && this.interval === undefined) {
|
nicholas@2817
|
2953 window.requestAnimationFrame(this.update.bind(this));
|
nicholas@2498
|
2954 }
|
nicholas@2498
|
2955 };
|
nicholas@2712
|
2956 playhead.stop = function () {
|
nicholas@2817
|
2957 this.timePerPixel = 0;
|
nicholas@2498
|
2958 };
|
nicholas@2712
|
2959 return playhead;
|
nicholas@2712
|
2960 })();
|
nicholas@2498
|
2961
|
nicholas@2712
|
2962 this.volume = (function () {
|
nicholas@2224
|
2963 // An in-built volume module which can be viewed on page
|
nicholas@2224
|
2964 // Includes trackers on page-by-page data
|
nicholas@2224
|
2965 // Volume does NOT reset to 0dB on each page load
|
nicholas@2712
|
2966 var volume = {};
|
nicholas@2712
|
2967 volume.valueLin = 1.0;
|
nicholas@2712
|
2968 volume.valueDB = 0.0;
|
nicholas@2712
|
2969 volume.root = document.createElement('div');
|
nicholas@2712
|
2970 volume.root.id = 'master-volume-root';
|
nicholas@2712
|
2971 volume.object = document.createElement('div');
|
nicholas@2712
|
2972 volume.object.className = 'master-volume-holder-float';
|
nicholas@2712
|
2973 volume.object.appendChild(volume.root);
|
nicholas@2712
|
2974 volume.slider = document.createElement('input');
|
nicholas@2712
|
2975 volume.slider.id = 'master-volume-control';
|
nicholas@2712
|
2976 volume.slider.type = 'range';
|
nicholas@2712
|
2977 volume.valueText = document.createElement('span');
|
nicholas@2712
|
2978 volume.valueText.id = 'master-volume-feedback';
|
nicholas@2712
|
2979 volume.valueText.textContent = '0dB';
|
nicholas@2498
|
2980
|
nicholas@2712
|
2981 volume.slider.min = -60;
|
nicholas@2712
|
2982 volume.slider.max = 12;
|
nicholas@2712
|
2983 volume.slider.value = 0;
|
nicholas@2712
|
2984 volume.slider.step = 1;
|
nicholas@2712
|
2985 volume.handleEvent = function (event) {
|
nicholas@2669
|
2986 if (event.type == "mousemove") {
|
nicholas@2669
|
2987 this.valueDB = Number(this.slider.value);
|
nicholas@2669
|
2988 this.valueLin = decibelToLinear(this.valueDB);
|
nicholas@2669
|
2989 this.valueText.textContent = this.valueDB + 'dB';
|
nicholas@2669
|
2990 audioEngineContext.outputGain.gain.value = this.valueLin;
|
nicholas@2669
|
2991 } else if (event.type == "mouseup") {
|
nicholas@2669
|
2992 this.onmouseup();
|
nicholas@2669
|
2993 }
|
nicholas@2669
|
2994 this.slider.value = this.valueDB;
|
nicholas@2669
|
2995
|
nicholas@2669
|
2996 if (event.stopPropagation) {
|
nicholas@2669
|
2997 event.stopPropagation();
|
nicholas@2669
|
2998 }
|
nicholas@2711
|
2999 };
|
nicholas@2712
|
3000 volume.onmouseup = function () {
|
nicholas@2224
|
3001 var storePoint = testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].getAllElementsByName('volumeTracker');
|
nicholas@2708
|
3002 if (storePoint.length === 0) {
|
nicholas@2224
|
3003 storePoint = storage.document.createElement('metricresult');
|
nicholas@2498
|
3004 storePoint.setAttribute('name', 'volumeTracker');
|
nicholas@2224
|
3005 testState.currentStore.XMLDOM.getElementsByTagName('metric')[0].appendChild(storePoint);
|
nicholas@2498
|
3006 } else {
|
nicholas@2224
|
3007 storePoint = storePoint[0];
|
nicholas@2224
|
3008 }
|
nicholas@2224
|
3009 var node = storage.document.createElement('movement');
|
nicholas@2498
|
3010 node.setAttribute('test-time', audioEngineContext.timer.getTestTime());
|
nicholas@2669
|
3011 node.setAttribute('volume', this.valueDB);
|
nicholas@2498
|
3012 node.setAttribute('format', 'dBFS');
|
nicholas@2224
|
3013 storePoint.appendChild(node);
|
nicholas@2711
|
3014 };
|
nicholas@2712
|
3015 volume.slider.addEventListener("mousemove", volume);
|
nicholas@2712
|
3016 volume.root.addEventListener("mouseup", volume);
|
nicholas@2498
|
3017
|
nicholas@2224
|
3018 var title = document.createElement('div');
|
nicholas@2224
|
3019 title.innerHTML = '<span>Master Volume Control</span>';
|
nicholas@2224
|
3020 title.style.fontSize = '0.75em';
|
nicholas@2224
|
3021 title.style.width = "100%";
|
nicholas@2224
|
3022 title.align = 'center';
|
nicholas@2712
|
3023 volume.root.appendChild(title);
|
nicholas@2498
|
3024
|
nicholas@2712
|
3025 volume.root.appendChild(volume.slider);
|
nicholas@2712
|
3026 volume.root.appendChild(volume.valueText);
|
nicholas@2498
|
3027
|
nicholas@2712
|
3028 volume.resize = function (event) {
|
nicholas@2352
|
3029 if (window.innerWidth < 1000) {
|
nicholas@2708
|
3030 this.object.className = "master-volume-holder-inline";
|
nicholas@2352
|
3031 } else {
|
nicholas@2352
|
3032 this.object.className = 'master-volume-holder-float';
|
nicholas@2352
|
3033 }
|
nicholas@2708
|
3034 };
|
nicholas@2712
|
3035 return volume;
|
nicholas@2712
|
3036 })();
|
nicholas@2498
|
3037
|
nicholas@2778
|
3038 this.imageHolder = (function () {
|
nicholas@2778
|
3039 var imageController = {};
|
nicholas@2778
|
3040 imageController.root = document.createElement("div");
|
nicholas@2778
|
3041 imageController.root.id = "imageController";
|
nicholas@2778
|
3042 imageController.img = document.createElement("img");
|
nicholas@2778
|
3043 imageController.root.appendChild(imageController.img);
|
nicholas@2778
|
3044 imageController.setImage = function (src) {
|
nicholas@2778
|
3045 imageController.img.src = "";
|
n@2785
|
3046 if (typeof src !== "string" || src.length === undefined) {
|
nicholas@2778
|
3047 return;
|
nicholas@2778
|
3048 }
|
nicholas@2778
|
3049 imageController.img.src = src;
|
n@2785
|
3050 };
|
nicholas@2778
|
3051 return imageController;
|
nicholas@2778
|
3052 })();
|
nicholas@2778
|
3053
|
nicholas@2224
|
3054 this.calibrationModuleObject = null;
|
nicholas@2498
|
3055 this.calibrationModule = function () {
|
nicholas@2224
|
3056 // This creates an on-page calibration module
|
nicholas@2224
|
3057 this.storeDOM = storage.document.createElement("calibration");
|
nicholas@2224
|
3058 storage.root.appendChild(this.storeDOM);
|
nicholas@2224
|
3059 // The calibration is a fixed state module
|
nicholas@2224
|
3060 this.calibrationNodes = [];
|
nicholas@2224
|
3061 this.holder = null;
|
nicholas@2498
|
3062 this.build = function (inject) {
|
nicholas@2224
|
3063 var f0 = 62.5;
|
nicholas@2224
|
3064 this.holder = document.createElement("div");
|
nicholas@2224
|
3065 this.holder.className = "calibration-holder";
|
nicholas@2224
|
3066 this.calibrationNodes = [];
|
nicholas@2498
|
3067 while (f0 < 20000) {
|
nicholas@2712
|
3068 /* jshint loopfunc: true */
|
nicholas@2224
|
3069 var obj = {
|
nicholas@2224
|
3070 root: document.createElement("div"),
|
nicholas@2224
|
3071 input: document.createElement("input"),
|
nicholas@2224
|
3072 oscillator: audioContext.createOscillator(),
|
nicholas@2224
|
3073 gain: audioContext.createGain(),
|
nicholas@2224
|
3074 f: f0,
|
nicholas@2224
|
3075 parent: this,
|
nicholas@2498
|
3076 handleEvent: function (event) {
|
nicholas@2498
|
3077 switch (event.type) {
|
nicholas@2224
|
3078 case "mouseenter":
|
nicholas@2224
|
3079 this.oscillator.start(0);
|
nicholas@2224
|
3080 break;
|
nicholas@2224
|
3081 case "mouseleave":
|
nicholas@2224
|
3082 this.oscillator.stop(0);
|
nicholas@2224
|
3083 this.oscillator = audioContext.createOscillator();
|
nicholas@2224
|
3084 this.oscillator.connect(this.gain);
|
nicholas@2224
|
3085 this.oscillator.frequency.value = this.f;
|
nicholas@2224
|
3086 break;
|
nicholas@2224
|
3087 case "mousemove":
|
nicholas@2498
|
3088 var value = Math.pow(10, this.input.value / 20);
|
nicholas@2224
|
3089 if (this.f == 1000) {
|
nicholas@2224
|
3090 audioEngineContext.outputGain.gain.value = value;
|
nicholas@2224
|
3091 interfaceContext.volume.slider.value = this.input.value;
|
nicholas@2224
|
3092 } else {
|
nicholas@2708
|
3093 this.gain.gain.value = value;
|
nicholas@2224
|
3094 }
|
nicholas@2224
|
3095 break;
|
nicholas@2224
|
3096 }
|
nicholas@2224
|
3097 },
|
nicholas@2498
|
3098 disconnect: function () {
|
nicholas@2224
|
3099 this.gain.disconnect();
|
nicholas@2224
|
3100 }
|
nicholas@2708
|
3101 };
|
nicholas@2224
|
3102 obj.root.className = "calibration-slider";
|
nicholas@2224
|
3103 obj.root.appendChild(obj.input);
|
nicholas@2224
|
3104 obj.oscillator.connect(obj.gain);
|
nicholas@2224
|
3105 obj.gain.connect(audioEngineContext.outputGain);
|
nicholas@2498
|
3106 obj.gain.gain.value = Math.random() * 2;
|
nicholas@2224
|
3107 obj.input.value = obj.gain.gain.value;
|
nicholas@2498
|
3108 obj.input.setAttribute('orient', 'vertical');
|
nicholas@2224
|
3109 obj.input.type = "range";
|
nicholas@2593
|
3110 obj.input.min = -12;
|
nicholas@2593
|
3111 obj.input.max = 0;
|
nicholas@2224
|
3112 obj.input.step = 0.25;
|
nicholas@2224
|
3113 if (f0 != 1000) {
|
nicholas@2498
|
3114 obj.input.value = (Math.random() * 12) - 6;
|
nicholas@2224
|
3115 } else {
|
nicholas@2224
|
3116 obj.input.value = 0;
|
nicholas@2498
|
3117 obj.root.style.backgroundColor = "rgb(255,125,125)";
|
nicholas@2224
|
3118 }
|
nicholas@2498
|
3119 obj.input.addEventListener("mousemove", obj);
|
nicholas@2498
|
3120 obj.input.addEventListener("mouseenter", obj);
|
nicholas@2498
|
3121 obj.input.addEventListener("mouseleave", obj);
|
nicholas@2498
|
3122 obj.gain.gain.value = Math.pow(10, obj.input.value / 20);
|
nicholas@2224
|
3123 obj.oscillator.frequency.value = f0;
|
nicholas@2224
|
3124 this.calibrationNodes.push(obj);
|
nicholas@2224
|
3125 this.holder.appendChild(obj.root);
|
nicholas@2224
|
3126 f0 *= 2;
|
nicholas@2224
|
3127 }
|
nicholas@2224
|
3128 inject.appendChild(this.holder);
|
nicholas@2708
|
3129 };
|
nicholas@2498
|
3130 this.collect = function () {
|
nicholas@2708
|
3131 this.calibrationNodes.forEach(function (obj) {
|
nicholas@2224
|
3132 var node = storage.document.createElement("calibrationresult");
|
nicholas@2498
|
3133 node.setAttribute("frequency", obj.f);
|
nicholas@2498
|
3134 node.setAttribute("range-min", obj.input.min);
|
nicholas@2498
|
3135 node.setAttribute("range-max", obj.input.max);
|
nicholas@2498
|
3136 node.setAttribute("gain-lin", obj.gain.gain.value);
|
nicholas@2224
|
3137 this.storeDOM.appendChild(node);
|
nicholas@2708
|
3138 }, this);
|
nicholas@2708
|
3139 };
|
nicholas@2708
|
3140 };
|
nicholas@2498
|
3141
|
nicholas@2498
|
3142
|
nicholas@2498
|
3143 // Global Checkers
|
nicholas@2498
|
3144 // These functions will help enforce the checkers
|
n@2789
|
3145 this.checkHiddenAnchor = function (message) {
|
nicholas@2708
|
3146 var anchors = audioEngineContext.audioObjects.filter(function (ao) {
|
nicholas@2708
|
3147 return ao.specification.type === "anchor";
|
nicholas@2708
|
3148 });
|
nicholas@2708
|
3149 var state = anchors.some(function (ao) {
|
nicholas@2708
|
3150 return (ao.interfaceDOM.getValue() > (ao.specification.marker / 100) && ao.specification.marker > 0);
|
nicholas@2708
|
3151 });
|
nicholas@2708
|
3152 if (state) {
|
nicholas@2708
|
3153 console.log('Anchor node not below marker value');
|
n@2789
|
3154 if (message) {
|
n@2789
|
3155 interfaceContext.lightbox.post("Message", message);
|
n@2789
|
3156 } else {
|
n@2789
|
3157 interfaceContext.lightbox.post("Message", 'Please keep listening');
|
n@2789
|
3158 }
|
nicholas@2708
|
3159 this.storeErrorNode('Anchor node not below marker value');
|
nicholas@2708
|
3160 return false;
|
nicholas@2498
|
3161 }
|
nicholas@2498
|
3162 return true;
|
nicholas@2498
|
3163 };
|
nicholas@2498
|
3164
|
n@2789
|
3165 this.checkHiddenReference = function (message) {
|
nicholas@2708
|
3166 var references = audioEngineContext.audioObjects.filter(function (ao) {
|
nicholas@2708
|
3167 return ao.specification.type === "reference";
|
nicholas@2708
|
3168 });
|
nicholas@2708
|
3169 var state = references.some(function (ao) {
|
nicholas@2708
|
3170 return (ao.interfaceDOM.getValue() < (ao.specification.marker / 100) && ao.specification.marker > 0);
|
nicholas@2708
|
3171 });
|
nicholas@2708
|
3172 if (state) {
|
nicholas@2708
|
3173 console.log('Reference node not below marker value');
|
n@2789
|
3174 if (message) {
|
n@2789
|
3175 interfaceContext.lightbox.post("Message", message);
|
n@2789
|
3176 } else {
|
n@2789
|
3177 interfaceContext.lightbox.post("Message", 'Please keep listening');
|
n@2789
|
3178 }
|
nicholas@2708
|
3179 this.storeErrorNode('Reference node not below marker value');
|
nicholas@2708
|
3180 return false;
|
nicholas@2498
|
3181 }
|
nicholas@2498
|
3182 return true;
|
nicholas@2498
|
3183 };
|
nicholas@2498
|
3184
|
n@2789
|
3185 this.checkFragmentsFullyPlayed = function (message) {
|
nicholas@2498
|
3186 // Checks the entire file has been played back
|
nicholas@2498
|
3187 // NOTE ! This will return true IF playback is Looped!!!
|
nicholas@2498
|
3188 if (audioEngineContext.loopPlayback) {
|
nicholas@2498
|
3189 console.log("WARNING - Looped source: Cannot check fragments are fully played");
|
nicholas@2498
|
3190 return true;
|
nicholas@2498
|
3191 }
|
nicholas@2498
|
3192 var check_pass = true;
|
nicholas@2708
|
3193 var error_obj = [],
|
nicholas@2708
|
3194 i;
|
nicholas@2708
|
3195 for (i = 0; i < audioEngineContext.audioObjects.length; i++) {
|
nicholas@2498
|
3196 var object = audioEngineContext.audioObjects[i];
|
nicholas@2498
|
3197 var time = object.buffer.buffer.duration;
|
nicholas@2498
|
3198 var metric = object.metric;
|
nicholas@2498
|
3199 var passed = false;
|
nicholas@2498
|
3200 for (var j = 0; j < metric.listenTracker.length; j++) {
|
nicholas@2498
|
3201 var bt = metric.listenTracker[j].getElementsByTagName('testtime');
|
nicholas@2498
|
3202 var start_time = Number(bt[0].getAttribute('start'));
|
nicholas@2498
|
3203 var stop_time = Number(bt[0].getAttribute('stop'));
|
nicholas@2498
|
3204 var delta = stop_time - start_time;
|
nicholas@2498
|
3205 if (delta >= time) {
|
nicholas@2498
|
3206 passed = true;
|
nicholas@2498
|
3207 break;
|
nicholas@2498
|
3208 }
|
nicholas@2498
|
3209 }
|
nicholas@2708
|
3210 if (passed === false) {
|
nicholas@2498
|
3211 check_pass = false;
|
nicholas@2498
|
3212 console.log("Continue listening to track-" + object.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3213 error_obj.push(object.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3214 }
|
nicholas@2498
|
3215 }
|
nicholas@2708
|
3216 if (check_pass === false) {
|
nicholas@2498
|
3217 var str_start = "You have not completely listened to fragments ";
|
nicholas@2708
|
3218 for (i = 0; i < error_obj.length; i++) {
|
nicholas@2498
|
3219 str_start += error_obj[i];
|
nicholas@2498
|
3220 if (i != error_obj.length - 1) {
|
nicholas@2498
|
3221 str_start += ', ';
|
nicholas@2498
|
3222 }
|
nicholas@2498
|
3223 }
|
nicholas@2498
|
3224 str_start += ". Please keep listening";
|
n@2789
|
3225 console.log(str_start);
|
n@2789
|
3226 this.storeErrorNode(str_start);
|
n@2789
|
3227 if (message) {
|
n@2789
|
3228 str_start = message;
|
n@2789
|
3229 }
|
nicholas@2498
|
3230 interfaceContext.lightbox.post("Error", str_start);
|
nicholas@2444
|
3231 return false;
|
nicholas@2498
|
3232 }
|
nicholas@2444
|
3233 return true;
|
nicholas@2498
|
3234 };
|
n@2789
|
3235 this.checkAllMoved = function (message) {
|
nicholas@2498
|
3236 var str = "You have not moved ";
|
nicholas@2498
|
3237 var failed = [];
|
nicholas@2708
|
3238 audioEngineContext.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
3239 if (ao.metric.wasMoved === false && ao.interfaceDOM.canMove() === true) {
|
nicholas@2498
|
3240 failed.push(ao.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3241 }
|
nicholas@2708
|
3242 }, this);
|
nicholas@2708
|
3243 if (failed.length === 0) {
|
nicholas@2498
|
3244 return true;
|
nicholas@2498
|
3245 } else if (failed.length == 1) {
|
nicholas@2498
|
3246 str += 'track ' + failed[0];
|
nicholas@2498
|
3247 } else {
|
nicholas@2498
|
3248 str += 'tracks ';
|
nicholas@2498
|
3249 for (var i = 0; i < failed.length - 1; i++) {
|
nicholas@2498
|
3250 str += failed[i] + ', ';
|
nicholas@2498
|
3251 }
|
nicholas@2498
|
3252 str += 'and ' + failed[i];
|
nicholas@2498
|
3253 }
|
nicholas@2498
|
3254 str += '.';
|
nicholas@2498
|
3255 console.log(str);
|
nicholas@2224
|
3256 this.storeErrorNode(str);
|
n@2789
|
3257 if (message) {
|
n@2789
|
3258 str = message;
|
n@2789
|
3259 }
|
n@2789
|
3260 interfaceContext.lightbox.post("Error", str);
|
nicholas@2498
|
3261 return false;
|
nicholas@2498
|
3262 };
|
n@2789
|
3263 this.checkAllPlayed = function (message) {
|
nicholas@2498
|
3264 var str = "You have not played ";
|
nicholas@2498
|
3265 var failed = [];
|
nicholas@2708
|
3266 audioEngineContext.audioObjects.forEach(function (ao) {
|
nicholas@2708
|
3267 if (ao.metric.wasListenedTo === false) {
|
nicholas@2498
|
3268 failed.push(ao.interfaceDOM.getPresentedId());
|
nicholas@2498
|
3269 }
|
nicholas@2708
|
3270 }, this);
|
nicholas@2708
|
3271 if (failed.length === 0) {
|
nicholas@2498
|
3272 return true;
|
nicholas@2498
|
3273 } else if (failed.length == 1) {
|
nicholas@2498
|
3274 str += 'track ' + failed[0];
|
nicholas@2498
|
3275 } else {
|
nicholas@2498
|
3276 str += 'tracks ';
|
nicholas@2498
|
3277 for (var i = 0; i < failed.length - 1; i++) {
|
nicholas@2498
|
3278 str += failed[i] + ', ';
|
nicholas@2498
|
3279 }
|
nicholas@2498
|
3280 str += 'and ' + failed[i];
|
nicholas@2498
|
3281 }
|
nicholas@2498
|
3282 str += '.';
|
nicholas@2498
|
3283 console.log(str);
|
nicholas@2224
|
3284 this.storeErrorNode(str);
|
n@2789
|
3285 if (message) {
|
n@2789
|
3286 str = message;
|
n@2789
|
3287 }
|
n@2789
|
3288 interfaceContext.lightbox.post("Error", str);
|
nicholas@2498
|
3289 return false;
|
nicholas@2498
|
3290 };
|
n@2789
|
3291 this.checkAllCommented = function (message) {
|
nicholas@2540
|
3292 var str = "You have not commented on all the fragments.";
|
nicholas@2540
|
3293 var cont = true,
|
nicholas@2540
|
3294 boxes = this.commentBoxes.boxes,
|
nicholas@2540
|
3295 numBoxes = boxes.length,
|
nicholas@2540
|
3296 i;
|
nicholas@2540
|
3297 for (i = 0; i < numBoxes; i++) {
|
nicholas@2540
|
3298 if (boxes[i].trackCommentBox.value === "") {
|
nicholas@2540
|
3299 console.log(str);
|
nicholas@2540
|
3300 this.storeErrorNode(str);
|
n@2789
|
3301 if (message) {
|
n@2789
|
3302 str = message;
|
n@2789
|
3303 }
|
n@2789
|
3304 interfaceContext.lightbox.post("Error", str);
|
nicholas@2540
|
3305 return false;
|
nicholas@2540
|
3306 }
|
nicholas@2540
|
3307 }
|
nicholas@2540
|
3308 return true;
|
nicholas@2708
|
3309 };
|
n@2789
|
3310 this.checkScaleRange = function (message) {
|
nicholas@2310
|
3311 var page = testState.getCurrentTestPage();
|
nicholas@2708
|
3312 var interfaceObject = page.interfaces;
|
nicholas@2310
|
3313 var state = true;
|
nicholas@2310
|
3314 var str = "Please keep listening. ";
|
nicholas@2708
|
3315 if (interfaceObject === undefined) {
|
nicholas@2708
|
3316 return true;
|
nicholas@2310
|
3317 }
|
nicholas@2708
|
3318 interfaceObject = interfaceObject[0];
|
nicholas@2708
|
3319 var scales = (function () {
|
nicholas@2708
|
3320 var scaleRange = interfaceObject.options.find(function (a) {
|
nicholas@2708
|
3321 return a.name == "scalerange";
|
nicholas@2708
|
3322 });
|
nicholas@2708
|
3323 return {
|
nicholas@2708
|
3324 min: scaleRange.min,
|
nicholas@2708
|
3325 max: scaleRange.max
|
nicholas@2708
|
3326 };
|
nicholas@2708
|
3327 })();
|
nicholas@2708
|
3328 var range = audioEngineContext.audioObjects.reduce(function (a, b) {
|
nicholas@2742
|
3329 var v = b.interfaceDOM.getValue() * 100.0;
|
nicholas@2708
|
3330 return {
|
nicholas@2708
|
3331 min: Math.min(a.min, v),
|
nicholas@2708
|
3332 max: Math.max(a.max, v)
|
nicholas@2712
|
3333 };
|
nicholas@2708
|
3334 }, {
|
nicholas@2708
|
3335 min: 100,
|
nicholas@2708
|
3336 max: 0
|
nicholas@2708
|
3337 });
|
nicholas@2708
|
3338 if (range.min > scales.min) {
|
nicholas@2742
|
3339 str += "At least one fragment must be below the " + scales.min + " mark.";
|
nicholas@2708
|
3340 state = false;
|
nicholas@2712
|
3341 } else if (range.max < scales.max) {
|
nicholas@2742
|
3342 str += "At least one fragment must be above the " + scales.max + " mark.";
|
nicholas@2310
|
3343 state = false;
|
nicholas@2310
|
3344 }
|
nicholas@2708
|
3345 if (state === false) {
|
nicholas@2310
|
3346 console.log(str);
|
nicholas@2310
|
3347 this.storeErrorNode(str);
|
n@2789
|
3348 if (message) {
|
n@2789
|
3349 str = message;
|
n@2789
|
3350 }
|
nicholas@2498
|
3351 interfaceContext.lightbox.post("Error", str);
|
nicholas@2310
|
3352 }
|
nicholas@2310
|
3353 return state;
|
nicholas@2708
|
3354 };
|
nicholas@2826
|
3355 this.checkFragmentMinPlays = function () {
|
nicholas@2826
|
3356 var failedObjects = audioEngineContext.audioObjects.filter(function (a) {
|
nicholas@2826
|
3357 var minPlays = a.specification.minNumberPlays || a.specification.parent.minNumberPlays || specification.minNumberPlays;
|
nicholas@2826
|
3358 if (minPlays === undefined || a.numberOfPlays >= minPlays) {
|
nicholas@2826
|
3359 return false;
|
nicholas@2826
|
3360 }
|
nicholas@2826
|
3361 return true;
|
nicholas@2826
|
3362 });
|
nicholas@2826
|
3363 if (failedObjects.length === 0) {
|
nicholas@2827
|
3364 return true;
|
nicholas@2826
|
3365 }
|
nicholas@2826
|
3366 var failedString = [];
|
nicholas@2826
|
3367 failedObjects.forEach(function (a) {
|
nicholas@2826
|
3368 failedString.push(a.interfaceDOM.getPresentedId());
|
nicholas@2826
|
3369 });
|
nicholas@2826
|
3370 var str = "You have not played fragments " + failedString.join(", ") + " enough. Please keep listening";
|
nicholas@2826
|
3371 interfaceContext.lightbox.post("Message", str);
|
nicholas@2826
|
3372 this.storeErrorNode(str);
|
nicholas@2827
|
3373 return false;
|
nicholas@2826
|
3374 };
|
nicholas@2826
|
3375
|
nicholas@2498
|
3376
|
nicholas@2849
|
3377 this.sortFragmentsByScore = function () {
|
nicholas@2849
|
3378 var elements = audioEngineContext.audioObjects.filter(function (elem) {
|
nicholas@2849
|
3379 return elem.specification.type !== "outside-reference";
|
nicholas@2849
|
3380 });
|
nicholas@2849
|
3381 var indexes = [];
|
nicholas@2849
|
3382 var i = 0;
|
nicholas@2849
|
3383 while (indexes.push(i++) < elements.length);
|
nicholas@2849
|
3384 return indexes.sort(function (x, y) {
|
nicholas@2849
|
3385 var a = elements[x].interfaceDOM.getValue();
|
nicholas@2849
|
3386 var b = elements[y].interfaceDOM.getValue();
|
nicholas@2849
|
3387 if (a > b) {
|
nicholas@2849
|
3388 return 1;
|
nicholas@2849
|
3389 } else if (a < b) {
|
nicholas@2849
|
3390 return -1;
|
nicholas@2849
|
3391 }
|
nicholas@2849
|
3392 return 0;
|
nicholas@2849
|
3393 }, elements[0].interfaceDOM.getValue());
|
nicholas@2849
|
3394 };
|
nicholas@2849
|
3395
|
nicholas@2498
|
3396 this.storeErrorNode = function (errorMessage) {
|
nicholas@2224
|
3397 var time = audioEngineContext.timer.getTestTime();
|
nicholas@2224
|
3398 var node = storage.document.createElement('error');
|
nicholas@2498
|
3399 node.setAttribute('time', time);
|
nicholas@2224
|
3400 node.textContent = errorMessage;
|
nicholas@2224
|
3401 testState.currentStore.XMLDOM.appendChild(node);
|
nicholas@2224
|
3402 };
|
nicholas@2595
|
3403
|
nicholas@2595
|
3404 this.getLabel = function (labelType, index, labelStart) {
|
nicholas@2595
|
3405 /*
|
nicholas@2595
|
3406 Get the correct label based on type, index and offset
|
nicholas@2595
|
3407 */
|
nicholas@2595
|
3408
|
nicholas@2595
|
3409 function calculateLabel(labelType, index, offset) {
|
nicholas@2595
|
3410 if (labelType == "none") {
|
nicholas@2595
|
3411 return "";
|
nicholas@2595
|
3412 }
|
nicholas@2595
|
3413 switch (labelType) {
|
nicholas@2595
|
3414 case "letter":
|
nicholas@2596
|
3415 return String.fromCharCode((index + offset) % 26 + 97);
|
nicholas@2595
|
3416 case "capital":
|
nicholas@2607
|
3417 return String.fromCharCode((index + offset) % 26 + 65);
|
nicholas@2625
|
3418 case "samediff":
|
nicholas@2708
|
3419 if (index === 0) {
|
nicholas@2625
|
3420 return "Same";
|
nicholas@2625
|
3421 } else if (index == 1) {
|
nicholas@2625
|
3422 return "Difference";
|
nicholas@2625
|
3423 }
|
nicholas@2708
|
3424 return "";
|
nicholas@2595
|
3425 case "number":
|
nicholas@2595
|
3426 return String(index + offset);
|
nicholas@2595
|
3427 default:
|
nicholas@2595
|
3428 return "";
|
nicholas@2595
|
3429 }
|
nicholas@2595
|
3430 }
|
nicholas@2595
|
3431
|
nicholas@2708
|
3432 if (typeof labelStart !== "string" || labelStart.length === 0) {
|
nicholas@2595
|
3433 labelStart = String.fromCharCode(0);
|
nicholas@2595
|
3434 }
|
nicholas@2595
|
3435
|
nicholas@2595
|
3436 switch (labelType) {
|
nicholas@2595
|
3437 case "letter":
|
nicholas@2595
|
3438 labelStart = labelStart.charCodeAt(0);
|
nicholas@2596
|
3439 if (labelStart < 97 || labelStart > 122) {
|
nicholas@2595
|
3440 labelStart = 97;
|
nicholas@2595
|
3441 }
|
nicholas@2595
|
3442 labelStart -= 97;
|
nicholas@2595
|
3443 break;
|
nicholas@2595
|
3444 case "capital":
|
nicholas@2595
|
3445 labelStart = labelStart.charCodeAt(0);
|
nicholas@2596
|
3446 if (labelStart < 65 || labelStart > 90) {
|
nicholas@2595
|
3447 labelStart = 65;
|
nicholas@2595
|
3448 }
|
nicholas@2595
|
3449 labelStart -= 65;
|
nicholas@2595
|
3450 break;
|
nicholas@2595
|
3451 case "number":
|
nicholas@2608
|
3452 labelStart = Number(labelStart);
|
nicholas@2608
|
3453 if (!isFinite(labelStart)) {
|
nicholas@2595
|
3454 labelStart = 1;
|
nicholas@2595
|
3455 }
|
nicholas@2595
|
3456 break;
|
nicholas@2595
|
3457 default:
|
nicholas@2596
|
3458 labelStart = 0;
|
nicholas@2595
|
3459 }
|
nicholas@2595
|
3460 if (typeof index == "number") {
|
nicholas@2595
|
3461 return calculateLabel(labelType, index, labelStart);
|
nicholas@2595
|
3462 } else if (index.length && index.length > 0) {
|
nicholas@2595
|
3463 var a = [],
|
nicholas@2595
|
3464 l = index.length,
|
nicholas@2595
|
3465 i;
|
nicholas@2595
|
3466 for (i = 0; i < l; i++) {
|
nicholas@2595
|
3467 a[i] = calculateLabel(labelType, index[i], labelStart);
|
nicholas@2595
|
3468 }
|
nicholas@2595
|
3469 return a;
|
nicholas@2595
|
3470 } else {
|
nicholas@2595
|
3471 throw ("Invalid arguments");
|
nicholas@2595
|
3472 }
|
nicholas@2708
|
3473 };
|
nicholas@2649
|
3474
|
nicholas@2649
|
3475 this.getCombinedInterfaces = function (page) {
|
nicholas@2649
|
3476 // Combine the interfaces with the global interface nodes
|
nicholas@2649
|
3477 var global = specification.interfaces,
|
nicholas@2649
|
3478 local = page.interfaces;
|
nicholas@2649
|
3479 local.forEach(function (locInt) {
|
nicholas@2649
|
3480 // Iterate through the options nodes
|
nicholas@2649
|
3481 var addList = [];
|
nicholas@2649
|
3482 global.options.forEach(function (gopt) {
|
nicholas@2649
|
3483 var lopt = locInt.options.find(function (lopt) {
|
nicholas@2649
|
3484 return (lopt.name == gopt.name) && (lopt.type == gopt.type);
|
nicholas@2649
|
3485 });
|
nicholas@2649
|
3486 if (!lopt) {
|
nicholas@2649
|
3487 // Global option doesn't exist locally
|
nicholas@2649
|
3488 addList.push(gopt);
|
nicholas@2649
|
3489 }
|
nicholas@2649
|
3490 });
|
nicholas@2649
|
3491 locInt.options = locInt.options.concat(addList);
|
nicholas@2649
|
3492 if (!locInt.scales && global.scales) {
|
nicholas@2649
|
3493 // Use the global default scales
|
nicholas@2649
|
3494 locInt.scales = global.scales;
|
nicholas@2649
|
3495 }
|
nicholas@2649
|
3496 });
|
nicholas@2649
|
3497 return local;
|
nicholas@2708
|
3498 };
|
nicholas@2224
|
3499 }
|
nicholas@2224
|
3500
|
nicholas@2498
|
3501 function Storage() {
|
nicholas@2498
|
3502 // Holds results in XML format until ready for collection
|
nicholas@2498
|
3503 this.globalPreTest = null;
|
nicholas@2498
|
3504 this.globalPostTest = null;
|
nicholas@2498
|
3505 this.testPages = [];
|
nicholas@2498
|
3506 this.document = null;
|
nicholas@2498
|
3507 this.root = null;
|
nicholas@2498
|
3508 this.state = 0;
|
nicholas@2733
|
3509 var pFilenamePrefix = "save";
|
nicholas@2498
|
3510
|
nicholas@2498
|
3511 this.initialise = function (existingStore) {
|
nicholas@2708
|
3512 if (existingStore === undefined) {
|
nicholas@2224
|
3513 // We need to get the sessionKey
|
nicholas@2510
|
3514 this.SessionKey.requestKey();
|
nicholas@2498
|
3515 this.document = document.implementation.createDocument(null, "waetresult", null);
|
nicholas@2224
|
3516 this.root = this.document.childNodes[0];
|
nicholas@2224
|
3517 var projectDocument = specification.projectXML;
|
nicholas@2708
|
3518 projectDocument.setAttribute('file-name', specification.url);
|
nicholas@2708
|
3519 projectDocument.setAttribute('url', qualifyURL(specification.url));
|
nicholas@2224
|
3520 this.root.appendChild(projectDocument);
|
nicholas@2224
|
3521 this.root.appendChild(interfaceContext.returnDateNode());
|
nicholas@2224
|
3522 this.root.appendChild(interfaceContext.returnNavigator());
|
nicholas@2224
|
3523 } else {
|
nicholas@2224
|
3524 this.document = existingStore;
|
nicholas@2294
|
3525 this.root = existingStore.firstChild;
|
nicholas@2224
|
3526 this.SessionKey.key = this.root.getAttribute("key");
|
nicholas@2224
|
3527 }
|
nicholas@2708
|
3528 if (specification.preTest !== undefined) {
|
nicholas@2498
|
3529 this.globalPreTest = new this.surveyNode(this, this.root, specification.preTest);
|
nicholas@2498
|
3530 }
|
nicholas@2708
|
3531 if (specification.postTest !== undefined) {
|
nicholas@2498
|
3532 this.globalPostTest = new this.surveyNode(this, this.root, specification.postTest);
|
nicholas@2498
|
3533 }
|
nicholas@2498
|
3534 };
|
nicholas@2498
|
3535
|
nicholas@2224
|
3536 this.SessionKey = {
|
nicholas@2224
|
3537 key: null,
|
nicholas@2224
|
3538 request: new XMLHttpRequest(),
|
nicholas@2224
|
3539 parent: this,
|
nicholas@2498
|
3540 handleEvent: function () {
|
nicholas@2224
|
3541 var parse = new DOMParser();
|
nicholas@2498
|
3542 var xml = parse.parseFromString(this.request.response, "text/xml");
|
nicholas@2708
|
3543 if (this.request.response.length === 0) {
|
nicholas@2515
|
3544 console.error("An unspecified error occured, no server key could be generated");
|
nicholas@2376
|
3545 return;
|
nicholas@2376
|
3546 }
|
nicholas@2498
|
3547 if (xml.getElementsByTagName("state").length > 0) {
|
nicholas@2498
|
3548 if (xml.getElementsByTagName("state")[0].textContent == "OK") {
|
nicholas@2498
|
3549 this.key = xml.getAllElementsByTagName("key")[0].textContent;
|
nicholas@2498
|
3550 this.parent.root.setAttribute("key", this.key);
|
nicholas@2498
|
3551 this.parent.root.setAttribute("state", "empty");
|
nicholas@2516
|
3552 this.update();
|
nicholas@2515
|
3553 return;
|
nicholas@2514
|
3554 } else if (xml.getElementsByTagName("state")[0].textContent == "ERROR") {
|
nicholas@2515
|
3555 this.key = null;
|
nicholas@2514
|
3556 console.error("Could not generate server key. Server responded with error message: \"" + xml.getElementsByTagName("message")[0].textContent + "\"");
|
nicholas@2515
|
3557 return;
|
nicholas@2498
|
3558 }
|
nicholas@2498
|
3559 }
|
nicholas@2515
|
3560 this.key = null;
|
nicholas@2515
|
3561 console.error("An unspecified error occured, no server key could be generated");
|
nicholas@2224
|
3562 },
|
nicholas@2510
|
3563 requestKey: function () {
|
nicholas@2510
|
3564 // For new servers, request a new key from the server
|
nicholas@2510
|
3565 var returnURL = "";
|
nicholas@2510
|
3566 if (typeof specification.projectReturn == "string") {
|
nicholas@2510
|
3567 if (specification.projectReturn.substr(0, 4) == "http") {
|
nicholas@2510
|
3568 returnURL = specification.projectReturn;
|
nicholas@2510
|
3569 }
|
nicholas@2510
|
3570 }
|
nicholas@2510
|
3571 this.request.open("GET", returnURL + "php/requestKey.php", true);
|
nicholas@2510
|
3572 this.request.addEventListener("load", this);
|
nicholas@2510
|
3573 this.request.send();
|
nicholas@2510
|
3574 },
|
nicholas@2498
|
3575 update: function () {
|
nicholas@2708
|
3576 if (this.key === null) {
|
nicholas@2357
|
3577 console.log("Cannot save as key == null");
|
nicholas@2357
|
3578 return;
|
nicholas@2357
|
3579 }
|
nicholas@2498
|
3580 this.parent.root.setAttribute("state", "update");
|
nicholas@2224
|
3581 var xmlhttp = new XMLHttpRequest();
|
nicholas@2302
|
3582 var returnURL = "";
|
nicholas@2302
|
3583 if (typeof specification.projectReturn == "string") {
|
nicholas@2498
|
3584 if (specification.projectReturn.substr(0, 4) == "http") {
|
nicholas@2302
|
3585 returnURL = specification.projectReturn;
|
nicholas@2302
|
3586 }
|
nicholas@2302
|
3587 }
|
nicholas@2722
|
3588 xmlhttp.open("POST", returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=" + this.parent.filenamePrefix);
|
nicholas@2224
|
3589 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
|
nicholas@2498
|
3590 xmlhttp.onerror = function () {
|
nicholas@2224
|
3591 console.log('Error updating file to server!');
|
nicholas@2224
|
3592 };
|
nicholas@2224
|
3593 var hold = document.createElement("div");
|
nicholas@2224
|
3594 var clone = this.parent.root.cloneNode(true);
|
nicholas@2224
|
3595 hold.appendChild(clone);
|
nicholas@2498
|
3596 xmlhttp.onload = function () {
|
nicholas@2224
|
3597 if (this.status >= 300) {
|
nicholas@2224
|
3598 console.log("WARNING - Could not update at this time");
|
nicholas@2224
|
3599 } else {
|
nicholas@2224
|
3600 var parser = new DOMParser();
|
nicholas@2224
|
3601 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
|
nicholas@2224
|
3602 var response = xmlDoc.getElementsByTagName('response')[0];
|
nicholas@2224
|
3603 if (response.getAttribute("state") == "OK") {
|
nicholas@2224
|
3604 var file = response.getElementsByTagName("file")[0];
|
nicholas@2498
|
3605 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
|
nicholas@2224
|
3606 } else {
|
nicholas@2224
|
3607 var message = response.getElementsByTagName("message");
|
nicholas@2498
|
3608 console.log("Intermediate save: Error! " + message.textContent);
|
nicholas@2224
|
3609 }
|
nicholas@2224
|
3610 }
|
nicholas@2708
|
3611 };
|
nicholas@2224
|
3612 xmlhttp.send([hold.innerHTML]);
|
nicholas@2723
|
3613 },
|
nicholas@2723
|
3614 finish: function () {
|
nicholas@2723
|
3615 // Final upload to complete the test
|
nicholas@2723
|
3616 this.parent.finish();
|
nicholas@2723
|
3617 var hold = document.createElement("div");
|
nicholas@2723
|
3618 var clone = this.parent.root.cloneNode(true);
|
nicholas@2723
|
3619 hold.appendChild(clone);
|
nicholas@2733
|
3620 var saveURL = specification.returnURL + "php/save.php?key=" + this.key + "&saveFilenamePrefix=";
|
nicholas@2742
|
3621 if (this.parent.filenamePrefix.length === 0) {
|
nicholas@2733
|
3622 saveURL += "save";
|
nicholas@2733
|
3623 } else {
|
nicholas@2733
|
3624 saveURL += this.parent.filenamePrefix;
|
nicholas@2733
|
3625 }
|
nicholas@2723
|
3626 return new Promise(function (resolve, reject) {
|
nicholas@2723
|
3627 var xmlhttp = new XMLHttpRequest();
|
nicholas@2723
|
3628 xmlhttp.open("POST", saveURL);
|
nicholas@2723
|
3629 xmlhttp.setRequestHeader('Content-Type', 'text/xml');
|
nicholas@2723
|
3630 xmlhttp.onerror = function () {
|
nicholas@2723
|
3631 console.log('Error updating file to server!');
|
nicholas@2723
|
3632 createProjectSave("local");
|
nicholas@2723
|
3633 };
|
nicholas@2723
|
3634 xmlhttp.onload = function () {
|
nicholas@2723
|
3635 if (this.status >= 300) {
|
nicholas@2723
|
3636 console.log("WARNING - Could not update at this time");
|
nicholas@2723
|
3637 createProjectSave("local");
|
nicholas@2723
|
3638 } else {
|
nicholas@2723
|
3639 var parser = new DOMParser();
|
nicholas@2723
|
3640 var xmlDoc = parser.parseFromString(xmlhttp.responseText, "application/xml");
|
nicholas@2723
|
3641 var response = xmlDoc.getElementsByTagName('response')[0];
|
nicholas@2723
|
3642 if (response.getAttribute("state") == "OK") {
|
nicholas@2723
|
3643 var file = response.getElementsByTagName("file")[0];
|
nicholas@2723
|
3644 console.log("Intermediate save: OK, written " + file.getAttribute("bytes") + "B");
|
nicholas@2723
|
3645 resolve(response);
|
nicholas@2723
|
3646 } else {
|
nicholas@2723
|
3647 var message = response.getElementsByTagName("message");
|
nicholas@2723
|
3648 reject(message);
|
nicholas@2723
|
3649 }
|
nicholas@2723
|
3650 }
|
nicholas@2723
|
3651 };
|
nicholas@2723
|
3652 xmlhttp.send([hold.innerHTML]);
|
nicholas@2723
|
3653 });
|
nicholas@2224
|
3654 }
|
nicholas@2708
|
3655 };
|
nicholas@2498
|
3656
|
nicholas@2498
|
3657 this.createTestPageStore = function (specification) {
|
nicholas@2498
|
3658 var store = new this.pageNode(this, specification);
|
nicholas@2498
|
3659 this.testPages.push(store);
|
nicholas@2498
|
3660 return this.testPages[this.testPages.length - 1];
|
nicholas@2498
|
3661 };
|
nicholas@2498
|
3662
|
nicholas@2498
|
3663 this.surveyNode = function (parent, root, specification) {
|
nicholas@2498
|
3664 this.specification = specification;
|
nicholas@2498
|
3665 this.parent = parent;
|
nicholas@2224
|
3666 this.state = "empty";
|
nicholas@2498
|
3667 this.XMLDOM = this.parent.document.createElement('survey');
|
nicholas@2498
|
3668 this.XMLDOM.setAttribute('location', this.specification.location);
|
nicholas@2498
|
3669 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3670 this.specification.options.forEach(function (optNode) {
|
nicholas@2498
|
3671 if (optNode.type != 'statement') {
|
nicholas@2498
|
3672 var node = this.parent.document.createElement('surveyresult');
|
nicholas@2498
|
3673 node.setAttribute("ref", optNode.id);
|
nicholas@2498
|
3674 node.setAttribute('type', optNode.type);
|
nicholas@2498
|
3675 this.XMLDOM.appendChild(node);
|
nicholas@2498
|
3676 }
|
nicholas@2708
|
3677 }, this);
|
nicholas@2498
|
3678 root.appendChild(this.XMLDOM);
|
nicholas@2498
|
3679
|
nicholas@2498
|
3680 this.postResult = function (node) {
|
nicholas@2708
|
3681 function postNumber(doc, value) {
|
nicholas@2708
|
3682 var child = doc.createElement("response");
|
nicholas@2708
|
3683 child.textContent = value;
|
nicholas@2708
|
3684 return child;
|
nicholas@2708
|
3685 }
|
nicholas@2708
|
3686
|
nicholas@2708
|
3687 function postRadio(doc, node) {
|
nicholas@2708
|
3688 var child = doc.createElement('response');
|
nicholas@2708
|
3689 if (node.response !== null) {
|
nicholas@2708
|
3690 child.setAttribute('name', node.response.name);
|
nicholas@2708
|
3691 child.textContent = node.response.text;
|
nicholas@2708
|
3692 }
|
nicholas@2708
|
3693 return child;
|
nicholas@2708
|
3694 }
|
nicholas@2708
|
3695
|
nicholas@2708
|
3696 function postCheckbox(doc, node) {
|
nicholas@2708
|
3697 var checkNode = doc.createElement('response');
|
nicholas@2708
|
3698 checkNode.setAttribute('name', node.name);
|
nicholas@2708
|
3699 checkNode.setAttribute('checked', node.checked);
|
nicholas@2708
|
3700 return checkNode;
|
nicholas@2708
|
3701 }
|
nicholas@2498
|
3702 // From popup: node is the popupOption node containing both spec. and results
|
nicholas@2498
|
3703 // ID is the position
|
nicholas@2498
|
3704 if (node.specification.type == 'statement') {
|
nicholas@2498
|
3705 return;
|
nicholas@2498
|
3706 }
|
nicholas@2498
|
3707 var surveyresult = this.XMLDOM.firstChild;
|
nicholas@2708
|
3708 while (surveyresult !== null) {
|
nicholas@2498
|
3709 if (surveyresult.getAttribute("ref") == node.specification.id) {
|
nicholas@2224
|
3710 break;
|
nicholas@2224
|
3711 }
|
nicholas@2224
|
3712 surveyresult = surveyresult.nextElementSibling;
|
nicholas@2224
|
3713 }
|
nicholas@2775
|
3714 surveyresult.setAttribute("duration", node.elapsedTime);
|
nicholas@2498
|
3715 switch (node.specification.type) {
|
nicholas@2498
|
3716 case "number":
|
nicholas@2498
|
3717 case "question":
|
n@2583
|
3718 case "slider":
|
nicholas@2708
|
3719 surveyresult.appendChild(postNumber(this.parent.document, node.response));
|
nicholas@2464
|
3720 break;
|
nicholas@2498
|
3721 case "radio":
|
nicholas@2708
|
3722 surveyresult.appendChild(postRadio(this.parent.document, node));
|
nicholas@2498
|
3723 break;
|
nicholas@2498
|
3724 case "checkbox":
|
nicholas@2708
|
3725 if (node.response === undefined) {
|
nicholas@2498
|
3726 surveyresult.appendChild(this.parent.document.createElement('response'));
|
nicholas@2498
|
3727 break;
|
nicholas@2498
|
3728 }
|
nicholas@2498
|
3729 for (var i = 0; i < node.response.length; i++) {
|
nicholas@2708
|
3730 surveyresult.appendChild(postCheckbox(this.parent.document, node.response[i]));
|
nicholas@2498
|
3731 }
|
nicholas@2498
|
3732 break;
|
nicholas@2498
|
3733 }
|
nicholas@2498
|
3734 };
|
nicholas@2498
|
3735 this.complete = function () {
|
nicholas@2498
|
3736 this.state = "complete";
|
nicholas@2498
|
3737 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3738 };
|
nicholas@2498
|
3739 };
|
nicholas@2498
|
3740
|
nicholas@2498
|
3741 this.pageNode = function (parent, specification) {
|
nicholas@2498
|
3742 // Create one store per test page
|
nicholas@2498
|
3743 this.specification = specification;
|
nicholas@2498
|
3744 this.parent = parent;
|
nicholas@2498
|
3745 this.state = "empty";
|
nicholas@2498
|
3746 this.XMLDOM = this.parent.document.createElement('page');
|
nicholas@2498
|
3747 this.XMLDOM.setAttribute('ref', specification.id);
|
nicholas@2498
|
3748 this.XMLDOM.setAttribute('presentedId', specification.presentedId);
|
nicholas@2498
|
3749 this.XMLDOM.setAttribute("state", this.state);
|
nicholas@2708
|
3750 if (specification.preTest !== undefined) {
|
nicholas@2498
|
3751 this.preTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.preTest);
|
nicholas@2498
|
3752 }
|
nicholas@2708
|
3753 if (specification.postTest !== undefined) {
|
nicholas@2498
|
3754 this.postTest = new this.parent.surveyNode(this.parent, this.XMLDOM, this.specification.postTest);
|
nicholas@2498
|
3755 }
|
nicholas@2498
|
3756
|
nicholas@2498
|
3757 // Add any page metrics
|
nicholas@2498
|
3758 var page_metric = this.parent.document.createElement('metric');
|
nicholas@2498
|
3759 this.XMLDOM.appendChild(page_metric);
|
nicholas@2498
|
3760
|
nicholas@2498
|
3761 // Add the audioelement
|
nicholas@2708
|
3762 this.specification.audioElements.forEach(function (element) {
|
nicholas@2498
|
3763 var aeNode = this.parent.document.createElement('audioelement');
|
nicholas@2498
|
3764 aeNode.setAttribute('ref', element.id);
|
nicholas@2708
|
3765 if (element.name !== undefined) {
|
nicholas@2708
|
3766 aeNode.setAttribute('name', element.name);
|
nicholas@2708
|
3767 }
|
nicholas@2498
|
3768 aeNode.setAttribute('type', element.type);
|
nicholas@2498
|
3769 aeNode.setAttribute('url', element.url);
|
nicholas@2498
|
3770 aeNode.setAttribute('fqurl', qualifyURL(element.url));
|
nicholas@2498
|
3771 aeNode.setAttribute('gain', element.gain);
|
nicholas@2498
|
3772 if (element.type == 'anchor' || element.type == 'reference') {
|
nicholas@2498
|
3773 if (element.marker > 0) {
|
nicholas@2498
|
3774 aeNode.setAttribute('marker', element.marker);
|
nicholas@2464
|
3775 }
|
nicholas@2498
|
3776 }
|
nicholas@2498
|
3777 var ae_metric = this.parent.document.createElement('metric');
|
nicholas@2498
|
3778 aeNode.appendChild(ae_metric);
|
nicholas@2498
|
3779 this.XMLDOM.appendChild(aeNode);
|
nicholas@2708
|
3780 }, this);
|
nicholas@2498
|
3781
|
nicholas@2498
|
3782 this.parent.root.appendChild(this.XMLDOM);
|
nicholas@2498
|
3783
|
nicholas@2498
|
3784 this.complete = function () {
|
nicholas@2224
|
3785 this.state = "complete";
|
nicholas@2498
|
3786 this.XMLDOM.setAttribute("state", "complete");
|
nicholas@2708
|
3787 };
|
nicholas@2498
|
3788 };
|
nicholas@2498
|
3789 this.update = function () {
|
nicholas@2224
|
3790 this.SessionKey.update();
|
nicholas@2708
|
3791 };
|
nicholas@2498
|
3792 this.finish = function () {
|
nicholas@2498
|
3793 this.state = 1;
|
nicholas@2498
|
3794 this.root.setAttribute("state", "complete");
|
nicholas@2498
|
3795 return this.root;
|
nicholas@2498
|
3796 };
|
nicholas@2722
|
3797
|
nicholas@2722
|
3798 Object.defineProperties(this, {
|
nicholas@2722
|
3799 'filenamePrefix': {
|
nicholas@2722
|
3800 'get': function () {
|
nicholas@2722
|
3801 return pFilenamePrefix;
|
nicholas@2722
|
3802 },
|
nicholas@2722
|
3803 'set': function (value) {
|
nicholas@2722
|
3804 if (typeof value !== "string") {
|
nicholas@2722
|
3805 value = String(value);
|
nicholas@2722
|
3806 }
|
nicholas@2722
|
3807 pFilenamePrefix = value;
|
nicholas@2722
|
3808 return value;
|
nicholas@2722
|
3809 }
|
nicholas@2722
|
3810 }
|
nicholas@2725
|
3811 });
|
nicholas@2224
|
3812 }
|
nicholas@2384
|
3813
|
nicholas@2401
|
3814 var window_depedancy_callback;
|
nicholas@2498
|
3815 window_depedancy_callback = window.setInterval(function () {
|
nicholas@2401
|
3816 if (check_dependancies()) {
|
nicholas@2401
|
3817 window.clearInterval(window_depedancy_callback);
|
nicholas@2401
|
3818 onload();
|
nicholas@2401
|
3819 } else {
|
nicholas@2401
|
3820 document.getElementById("topLevelBody").innerHTML = "<h1>Loading Resources</h1>";
|
nicholas@2401
|
3821 }
|
nicholas@2498
|
3822 }, 100);
|