comparison interfaces/mushra.js @ 1116:c44fbf72f7f2

All interfaces support comment boxes. Comment box identification matches presented tag (for instance, AB will be Comment on fragment A, rather than 1). Tighter buffer loading protocol, audioObjects register with the buffer rather than checking for buffer existence (which can be buggy depending on the buffer state). Buffers now have a state to ensure exact location in loading chain (downloading, decoding, LUFS, ready).
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Fri, 29 Jan 2016 11:11:57 +0000
parents
children c0022a09c4f6
comparison
equal deleted inserted replaced
-1:000000000000 1116:c44fbf72f7f2
1 /**
2 * mushra.js
3 * Create the MUSHRA interface
4 */
5
6 // Once this is loaded and parsed, begin execution
7 loadInterface();
8
9 function loadInterface() {
10 // Get the dimensions of the screen available to the page
11 var width = window.innerWidth;
12 var height = window.innerHeight;
13
14 // The injection point into the HTML page
15 interfaceContext.insertPoint = document.getElementById("topLevelBody");
16 var testContent = document.createElement('div');
17 testContent.id = 'testContent';
18
19 // Create the top div for the Title element
20 var titleAttr = specification.title;
21 var title = document.createElement('div');
22 title.className = "title";
23 title.align = "center";
24 var titleSpan = document.createElement('span');
25
26 // Set title to that defined in XML, else set to default
27 if (titleAttr != undefined) {
28 titleSpan.textContent = titleAttr;
29 } else {
30 titleSpan.textContent = 'Listening test';
31 }
32 // Insert the titleSpan element into the title div element.
33 title.appendChild(titleSpan);
34
35 var pagetitle = document.createElement('div');
36 pagetitle.className = "pageTitle";
37 pagetitle.align = "center";
38 var titleSpan = document.createElement('span');
39 titleSpan.id = "pageTitle";
40 pagetitle.appendChild(titleSpan);
41
42 // Create Interface buttons!
43 var interfaceButtons = document.createElement('div');
44 interfaceButtons.id = 'interface-buttons';
45 interfaceButtons.style.height = '25px';
46
47 // Create playback start/stop points
48 var playback = document.createElement("button");
49 playback.innerHTML = 'Stop';
50 playback.id = 'playback-button';
51 playback.style.float = 'left';
52 // onclick function. Check if it is playing or not, call the correct function in the
53 // audioEngine, change the button text to reflect the next state.
54 playback.onclick = function() {
55 if (audioEngineContext.status == 1) {
56 audioEngineContext.stop();
57 this.innerHTML = 'Stop';
58 var time = audioEngineContext.timer.getTestTime();
59 console.log('Stopped at ' + time); // DEBUG/SAFETY
60 }
61 };
62 // Create Submit (save) button
63 var submit = document.createElement("button");
64 submit.innerHTML = 'Submit';
65 submit.onclick = buttonSubmitClick;
66 submit.id = 'submit-button';
67 submit.style.float = 'left';
68 // Append the interface buttons into the interfaceButtons object.
69 interfaceButtons.appendChild(playback);
70 interfaceButtons.appendChild(submit);
71
72 // Create a slider box
73 var sliderBox = document.createElement('div');
74 sliderBox.style.width = "100%";
75 sliderBox.style.height = window.innerHeight - 200+12 + 'px';
76 sliderBox.style.marginBottom = '10px';
77 sliderBox.id = 'slider';
78 var scaleHolder = document.createElement('div');
79 scaleHolder.id = "scale-holder";
80 sliderBox.appendChild(scaleHolder);
81 var scaleText = document.createElement('div');
82 scaleText.id = "scale-text-holder";
83 scaleHolder.appendChild(scaleText);
84 var scaleCanvas = document.createElement('canvas');
85 scaleCanvas.id = "scale-canvas";
86 scaleHolder.appendChild(scaleCanvas);
87 var sliderObjectHolder = document.createElement('div');
88 sliderObjectHolder.id = 'slider-holder';
89 sliderObjectHolder.align = "center";
90 sliderBox.appendChild(sliderObjectHolder);
91
92 // Global parent for the comment boxes on the page
93 var feedbackHolder = document.createElement('div');
94 feedbackHolder.id = 'feedbackHolder';
95
96 testContent.style.zIndex = 1;
97 interfaceContext.insertPoint.innerHTML = null; // Clear the current schema
98
99 // Inject into HTML
100 testContent.appendChild(title); // Insert the title
101 testContent.appendChild(pagetitle);
102 testContent.appendChild(interfaceButtons);
103 testContent.appendChild(sliderBox);
104 testContent.appendChild(feedbackHolder);
105 interfaceContext.insertPoint.appendChild(testContent);
106
107 // Load the full interface
108 testState.initialise();
109 testState.advanceState();
110 }
111
112 function loadTest(audioHolderObject)
113 {
114 var id = audioHolderObject.id;
115
116 var feedbackHolder = document.getElementById('feedbackHolder');
117 feedbackHolder.innerHTML = null;
118 var interfaceObj = audioHolderObject.interfaces;
119 if (interfaceObj.length > 1)
120 {
121 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
122 }
123 interfaceObj = interfaceObj[0];
124 if(interfaceObj.title != null)
125 {
126 document.getElementById("pageTitle").textContent = interfaceObj.title;
127 }
128 var interfaceOptions = specification.interfaces.options.concat(interfaceObj.options);
129 for (var option of interfaceOptions)
130 {
131 if (option.type == "show")
132 {
133 switch(option.name) {
134 case "playhead":
135 var playbackHolder = document.getElementById('playback-holder');
136 if (playbackHolder == null)
137 {
138 playbackHolder = document.createElement('div');
139 playbackHolder.style.width = "100%";
140 playbackHolder.align = 'center';
141 playbackHolder.appendChild(interfaceContext.playhead.object);
142 feedbackHolder.appendChild(playbackHolder);
143 }
144 break;
145 case "page-count":
146 var pagecountHolder = document.getElementById('page-count');
147 if (pagecountHolder == null)
148 {
149 pagecountHolder = document.createElement('div');
150 pagecountHolder.id = 'page-count';
151 }
152 pagecountHolder.innerHTML = '<span>Page '+(audioHolderObject.presentedId+1)+' of '+specification.pages.length+'</span>';
153 var inject = document.getElementById('interface-buttons');
154 inject.appendChild(pagecountHolder);
155 break;
156 case "volume":
157 if (document.getElementById('master-volume-holder') == null)
158 {
159 feedbackHolder.appendChild(interfaceContext.volume.object);
160 }
161 break;
162 }
163 }
164 }
165
166 // Delete outside reference
167 var outsideReferenceHolder = document.getElementById('outside-reference');
168 if (outsideReferenceHolder != null) {
169 document.getElementById('interface-buttons').removeChild(outsideReferenceHolder);
170 }
171
172 var sliderBox = document.getElementById('slider-holder');
173 sliderBox.innerHTML = null;
174
175 var commentBoxPrefix = "Comment on track";
176 if (interfaceObj.commentBoxPrefix != undefined) {
177 commentBoxPrefix = interfaceObj.commentBoxPrefix;
178 }
179 var loopPlayback = audioHolderObject.loop;
180
181 currentTestHolder = document.createElement('audioHolder');
182 currentTestHolder.id = audioHolderObject.id;
183 currentTestHolder.repeatCount = audioHolderObject.repeatCount;
184
185 $(audioHolderObject.commentQuestions).each(function(index,element) {
186 var node = interfaceContext.createCommentQuestion(element);
187 feedbackHolder.appendChild(node.holder);
188 });
189
190 // Find all the audioElements from the audioHolder
191 var label = 0;
192 $(audioHolderObject.audioElements).each(function(index,element){
193 // Find URL of track
194 // In this jQuery loop, variable 'this' holds the current audioElement.
195
196 var audioObject = audioEngineContext.newTrack(element);
197 if (element.type == 'outside-reference')
198 {
199 // Construct outside reference;
200 var orNode = new outsideReferenceDOM(audioObject,index,document.getElementById('interface-buttons'));
201 audioObject.bindInterface(orNode);
202 } else {
203 // Create a slider per track
204 var sliderObj = new sliderObject(audioObject,label);
205
206 if (typeof audioHolderObject.initialPosition === "number")
207 {
208 // Set the values
209 sliderObj.slider.value = audioHolderObject.initalPosition;
210 } else {
211 // Distribute it randomnly
212 sliderObj.slider.value = Math.random();
213 }
214 sliderBox.appendChild(sliderObj.holder);
215 audioObject.bindInterface(sliderObj);
216 interfaceContext.createCommentBox(audioObject);
217 label += 1;
218 }
219
220 });
221
222 // Auto-align
223 resizeWindow(null);
224 }
225
226 function sliderObject(audioObject,label)
227 {
228 // Constructs the slider object. We use the HTML5 slider object
229 this.parent = audioObject;
230 this.holder = document.createElement('div');
231 this.title = document.createElement('span');
232 this.slider = document.createElement('input');
233 this.play = document.createElement('button');
234
235 this.holder.className = 'track-slider';
236 this.holder.style.height = window.innerHeight-200 + 'px';
237 this.holder.appendChild(this.title);
238 this.holder.appendChild(this.slider);
239 this.holder.appendChild(this.play);
240 this.holder.align = "center";
241 if (label == 0)
242 {
243 this.holder.style.marginLeft = '0px';
244 }
245 this.holder.setAttribute('trackIndex',audioObject.id);
246
247 this.title.textContent = label;
248 this.title.style.width = "100%";
249 this.title.style.float = "left";
250
251 this.slider.type = "range";
252 this.slider.className = "track-slider-range track-slider-not-moved";
253 this.slider.min = "0";
254 this.slider.max = "1";
255 this.slider.step = "0.01";
256 this.slider.setAttribute('orient','vertical');
257 this.slider.style.height = window.innerHeight-250 + 'px';
258 this.slider.onchange = function()
259 {
260 var time = audioEngineContext.timer.getTestTime();
261 var id = Number(this.parentNode.getAttribute('trackIndex'));
262 audioEngineContext.audioObjects[id].metric.moved(time,this.value);
263 console.log('slider '+id+' moved to '+this.value+' ('+time+')');
264 $(this).removeClass('track-slider-not-moved');
265 };
266
267 this.play.textContent = "Loading...";
268 this.play.value = audioObject.id;
269 this.play.style.float = "left";
270 this.play.style.width = "100%";
271 this.play.disabled = true;
272 this.play.onclick = function(event)
273 {
274 var id = Number(event.currentTarget.value);
275 //audioEngineContext.metric.sliderPlayed(id);
276 audioEngineContext.play(id);
277 $(".track-slider").removeClass('track-slider-playing');
278 $(event.currentTarget.parentElement).addClass('track-slider-playing');
279 var outsideReference = document.getElementById('outside-reference');
280 if (outsideReference != null) {
281 $(outsideReference).removeClass('track-slider-playing');
282 }
283 };
284
285 this.enable = function() {
286 this.play.disabled = false;
287 this.play.textContent = "Play";
288 $(this.slider).removeClass('track-slider-disabled');
289 };
290
291 this.exportXMLDOM = function(audioObject) {
292 // Called by the audioObject holding this element. Must be present
293 var node = storage.document.createElement('value');
294 node.textContent = this.slider.value;
295 return node;
296 };
297 this.startPlayback = function()
298 {
299 // Called when playback has begun
300 $(".track-slider").removeClass('track-slider-playing');
301 $(this.holder).addClass('track-slider-playing');
302 var outsideReference = document.getElementById('outside-reference');
303 if (outsideReference != null) {
304 $(outsideReference).removeClass('track-slider-playing');
305 }
306 };
307 this.stopPlayback = function()
308 {
309 // Called when playback has stopped. This gets called even if playback never started!
310 $(this.holder).removeClass('track-slider-playing');
311 };
312 this.getValue = function() {
313 return this.slider.value;
314 };
315
316 this.resize = function(event)
317 {
318 this.holder.style.height = window.innerHeight-200 + 'px';
319 this.slider.style.height = window.innerHeight-250 + 'px';
320 };
321 this.updateLoading = function(progress)
322 {
323 progress = String(progress);
324 progress = progress.substr(0,5);
325 this.play.textContent = "Loading: "+progress+"%";
326 };
327
328 if (this.parent.state == 1)
329 {
330 this.enable();
331 }
332 this.getPresentedId = function()
333 {
334 return this.title.textContent;
335 };
336 this.canMove = function()
337 {
338 return true;
339 };
340 }
341
342 function outsideReferenceDOM(audioObject,index,inject)
343 {
344 this.parent = audioObject;
345 this.outsideReferenceHolder = document.createElement('button');
346 this.outsideReferenceHolder.id = 'outside-reference';
347 this.outsideReferenceHolder.className = 'outside-reference';
348 this.outsideReferenceHolder.setAttribute('track-id',index);
349 this.outsideReferenceHolder.textContent = "Play Reference";
350 this.outsideReferenceHolder.disabled = true;
351
352 this.outsideReferenceHolder.onclick = function(event)
353 {
354 audioEngineContext.play(event.currentTarget.getAttribute('track-id'));
355 };
356 inject.appendChild(this.outsideReferenceHolder);
357 this.enable = function()
358 {
359 if (this.parent.state == 1)
360 {
361 this.outsideReferenceHolder.disabled = false;
362 }
363 };
364 this.updateLoading = function(progress)
365 {
366 if (progress != 100)
367 {
368 progress = String(progress);
369 progress = progress.split('.')[0];
370 this.outsideReferenceHolder[0].children[0].textContent = progress+'%';
371 } else {
372 this.outsideReferenceHolder[0].children[0].textContent = "Play Reference";
373 }
374 };
375 this.startPlayback = function()
376 {
377 // Called when playback has begun
378 $('.track-slider').removeClass('track-slider-playing');
379 $('.comment-div').removeClass('comment-box-playing');
380 $(this.outsideReferenceHolder).addClass('track-slider-playing');
381 };
382 this.stopPlayback = function()
383 {
384 // Called when playback has stopped. This gets called even if playback never started!
385 $(this.outsideReferenceHolder).removeClass('track-slider-playing');
386 };
387 this.exportXMLDOM = function(audioObject)
388 {
389 return null;
390 };
391 this.getValue = function()
392 {
393 return 0;
394 };
395 this.getPresentedId = function()
396 {
397 return 'reference';
398 };
399 this.canMove = function()
400 {
401 return false;
402 };
403 }
404
405 function resizeWindow(event)
406 {
407 // Function called when the window has been resized.
408 // MANDATORY FUNCTION
409
410 var outsideRef = document.getElementById('outside-reference');
411 if(outsideRef != null)
412 {
413 outsideRef.style.left = (window.innerWidth-120)/2 + 'px';
414 }
415
416 // Auto-align
417 var numObj = document.getElementsByClassName('track-slider').length;
418 var totalWidth = (numObj-1)*150+100;
419 var diff = (window.innerWidth - totalWidth)/2;
420 document.getElementById('slider').style.height = window.innerHeight - 180 + 'px';
421 if (diff <= 0){diff = 0;}
422 document.getElementById('slider-holder').style.marginLeft = diff + 'px';
423 for (var i in audioEngineContext.audioObjects)
424 {
425 if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference'){
426 audioEngineContext.audioObjects[i].interfaceDOM.resize(event);
427 }
428 }
429 document.getElementById('scale-holder').style.marginLeft = (diff-100) + 'px';
430 document.getElementById('scale-text-holder').style.height = window.innerHeight-194 + 'px';
431 var canvas = document.getElementById('scale-canvas');
432 canvas.width = totalWidth;
433 canvas.height = window.innerHeight-194;
434 drawScale();
435 }
436
437 function drawScale()
438 {
439 var interfaceObj = testState.currentStateMap.interfaces[0];
440 var scales = testState.currentStateMap.interfaces[0].scales;
441 scales = scales.sort(function(a,b) {
442 return a.position - b.position;
443 });
444 var canvas = document.getElementById('scale-canvas');
445 var ctx = canvas.getContext("2d");
446 var height = canvas.height;
447 var width = canvas.width;
448 var draw_heights = [24, height-34];
449 var textHolder = document.getElementById('scale-text-holder');
450 textHolder.innerHTML = null;
451 var lastHeight = 0;
452 for (var scale of scales)
453 {
454 var posPercent = scale.position / 100.0;
455 var posPix = (1-posPercent)*(draw_heights[1]-draw_heights[0])+draw_heights[0];
456 ctx.fillStyle = "#000000";
457 ctx.setLineDash([1,2]);
458 ctx.moveTo(0,posPix);
459 ctx.lineTo(width,posPix);
460 ctx.stroke();
461 var text = document.createElement('div');
462 text.align = "right";
463 var textC = document.createElement('span');
464 textC.textContent = scale.text;
465 text.appendChild(textC);
466 text.className = "scale-text";
467 textHolder.appendChild(text);
468 text.style.top = (posPix-9) + 'px';
469 text.style.left = 100 - ($(text).width()+3) + 'px';
470 lastHeight = posPix;
471 }
472 }
473
474 function buttonSubmitClick() // TODO: Only when all songs have been played!
475 {
476 var checks = [];
477 checks = checks.concat(testState.currentStateMap.interfaces[0].options);
478 checks = checks.concat(specification.interfaces.options);
479 var canContinue = true;
480
481 // Check that the anchor and reference objects are correctly placed
482 if (interfaceContext.checkHiddenAnchor() == false) {return;}
483 if (interfaceContext.checkHiddenReference() == false) {return;}
484
485 for (var i=0; i<checks.length; i++) {
486 if (checks[i].type == 'check')
487 {
488 switch(checks[i].name) {
489 case 'fragmentPlayed':
490 // Check if all fragments have been played
491 var checkState = interfaceContext.checkAllPlayed();
492 if (checkState == false) {canContinue = false;}
493 break;
494 case 'fragmentFullPlayback':
495 // Check all fragments have been played to their full length
496 var checkState = interfaceContext.checkAllPlayed();
497 if (checkState == false) {canContinue = false;}
498 console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead');
499 break;
500 case 'fragmentMoved':
501 // Check all fragment sliders have been moved.
502 var checkState = interfaceContext.checkAllMoved();
503 if (checkState == false) {canContinue = false;}
504 break;
505 case 'fragmentComments':
506 // Check all fragment sliders have been moved.
507 var checkState = interfaceContext.checkAllCommented();
508 if (checkState == false) {canContinue = false;}
509 break;
510 //case 'scalerange':
511 // Check the scale is used to its full width outlined by the node
512 //var checkState = interfaceContext.checkScaleRange();
513 //if (checkState == false) {canContinue = false;}
514 // break;
515 default:
516 console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface");
517 break;
518 }
519
520 }
521 if (!canContinue) {break;}
522 }
523
524 if (canContinue) {
525 if (audioEngineContext.status == 1) {
526 var playback = document.getElementById('playback-button');
527 playback.click();
528 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
529 } else
530 {
531 if (audioEngineContext.timer.testStarted == false)
532 {
533 alert('You have not started the test! Please press start to begin the test!');
534 return;
535 }
536 }
537 testState.advanceState();
538 }
539 }
540
541 function pageXMLSave(store, pageSpecification)
542 {
543 // MANDATORY
544 // Saves a specific test page
545 // You can use this space to add any extra nodes to your XML <audioHolder> saves
546 // Get the current <page> information in store (remember to appendChild your data to it)
547 // pageSpecification is the current page node configuration
548 // To create new XML nodes, use storage.document.createElement();
549 }