Mercurial > hg > webaudioevaluationtool
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 } |