Mercurial > hg > webaudioevaluationtool
comparison interfaces/discrete.js @ 1316:279930a008ca
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 <nickjillings@users.noreply.github.com> |
---|---|
date | Fri, 29 Jan 2016 11:11:57 +0000 |
parents | |
children | c0022a09c4f6 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 1316:279930a008ca |
---|---|
1 // Once this is loaded and parsed, begin execution | |
2 loadInterface(); | |
3 | |
4 function loadInterface() { | |
5 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, | |
6 // holding div's, or setting up any nodes which are present for the entire test sequence | |
7 | |
8 // The injection point into the HTML page | |
9 interfaceContext.insertPoint = document.getElementById("topLevelBody"); | |
10 var testContent = document.createElement('div'); | |
11 testContent.id = 'testContent'; | |
12 | |
13 // Create the top div for the Title element | |
14 var titleAttr = specification.title; | |
15 var title = document.createElement('div'); | |
16 title.className = "title"; | |
17 title.align = "center"; | |
18 var titleSpan = document.createElement('span'); | |
19 | |
20 // Set title to that defined in XML, else set to default | |
21 if (titleAttr != undefined) { | |
22 titleSpan.textContent = titleAttr; | |
23 } else { | |
24 titleSpan.textContent = 'Listening test'; | |
25 } | |
26 // Insert the titleSpan element into the title div element. | |
27 title.appendChild(titleSpan); | |
28 | |
29 var pagetitle = document.createElement('div'); | |
30 pagetitle.className = "pageTitle"; | |
31 pagetitle.align = "center"; | |
32 var titleSpan = document.createElement('span'); | |
33 titleSpan.id = "pageTitle"; | |
34 pagetitle.appendChild(titleSpan); | |
35 | |
36 // Create Interface buttons! | |
37 var interfaceButtons = document.createElement('div'); | |
38 interfaceButtons.id = 'interface-buttons'; | |
39 interfaceButtons.style.height = '25px'; | |
40 | |
41 // Create playback start/stop points | |
42 var playback = document.createElement("button"); | |
43 playback.innerHTML = 'Stop'; | |
44 playback.id = 'playback-button'; | |
45 playback.style.float = 'left'; | |
46 // onclick function. Check if it is playing or not, call the correct function in the | |
47 // audioEngine, change the button text to reflect the next state. | |
48 playback.onclick = function() { | |
49 if (audioEngineContext.status == 1) { | |
50 audioEngineContext.stop(); | |
51 this.innerHTML = 'Stop'; | |
52 var time = audioEngineContext.timer.getTestTime(); | |
53 console.log('Stopped at ' + time); // DEBUG/SAFETY | |
54 } | |
55 }; | |
56 // Create Submit (save) button | |
57 var submit = document.createElement("button"); | |
58 submit.innerHTML = 'Submit'; | |
59 submit.onclick = buttonSubmitClick; | |
60 submit.id = 'submit-button'; | |
61 submit.style.float = 'left'; | |
62 // Append the interface buttons into the interfaceButtons object. | |
63 interfaceButtons.appendChild(playback); | |
64 interfaceButtons.appendChild(submit); | |
65 | |
66 // Create a slider box | |
67 var sliderBox = document.createElement('div'); | |
68 sliderBox.style.width = "100%"; | |
69 sliderBox.style.height = window.innerHeight - 200+12 + 'px'; | |
70 sliderBox.style.marginBottom = '10px'; | |
71 sliderBox.id = 'slider'; | |
72 var scaleHolder = document.createElement('div'); | |
73 scaleHolder.id = "scale-holder"; | |
74 scaleHolder.style.marginLeft = "107px"; | |
75 sliderBox.appendChild(scaleHolder); | |
76 var scaleText = document.createElement('div'); | |
77 scaleText.id = "scale-text-holder"; | |
78 scaleText.style.height = "25px"; | |
79 scaleText.style.width = "100%"; | |
80 scaleHolder.appendChild(scaleText); | |
81 var scaleCanvas = document.createElement('canvas'); | |
82 scaleCanvas.id = "scale-canvas"; | |
83 scaleCanvas.style.marginLeft = "150px"; | |
84 scaleHolder.appendChild(scaleCanvas); | |
85 var sliderObjectHolder = document.createElement('div'); | |
86 sliderObjectHolder.id = 'slider-holder'; | |
87 sliderObjectHolder.align = "center"; | |
88 sliderBox.appendChild(sliderObjectHolder); | |
89 | |
90 // Global parent for the comment boxes on the page | |
91 var feedbackHolder = document.createElement('div'); | |
92 feedbackHolder.id = 'feedbackHolder'; | |
93 | |
94 testContent.style.zIndex = 1; | |
95 interfaceContext.insertPoint.innerHTML = null; // Clear the current schema | |
96 | |
97 // Inject into HTML | |
98 testContent.appendChild(title); // Insert the title | |
99 testContent.appendChild(pagetitle); | |
100 testContent.appendChild(interfaceButtons); | |
101 testContent.appendChild(sliderBox); | |
102 testContent.appendChild(feedbackHolder); | |
103 interfaceContext.insertPoint.appendChild(testContent); | |
104 | |
105 // Load the full interface | |
106 testState.initialise(); | |
107 testState.advanceState(); | |
108 }; | |
109 | |
110 function loadTest(page) | |
111 { | |
112 // Called each time a new test page is to be build. The page specification node is the only item passed in | |
113 var id = page.id; | |
114 | |
115 var feedbackHolder = document.getElementById('feedbackHolder'); | |
116 feedbackHolder.innerHTML = null; | |
117 var interfaceObj = page.interfaces; | |
118 if (interfaceObj.length > 1) | |
119 { | |
120 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node"); | |
121 } | |
122 interfaceObj = interfaceObj[0]; | |
123 if(interfaceObj.title != null) | |
124 { | |
125 document.getElementById("pageTitle").textContent = interfaceObj.title; | |
126 } | |
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 '+(page.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 = page.loop; | |
180 | |
181 $(page.commentQuestions).each(function(index,element) { | |
182 var node = interfaceContext.createCommentQuestion(element); | |
183 feedbackHolder.appendChild(node.holder); | |
184 }); | |
185 | |
186 // Find all the audioElements from the audioHolder | |
187 var label = 0; | |
188 var interfaceScales = testState.currentStateMap.interfaces[0].scales; | |
189 $(page.audioElements).each(function(index,element){ | |
190 // Find URL of track | |
191 // In this jQuery loop, variable 'this' holds the current audioElement. | |
192 | |
193 var audioObject = audioEngineContext.newTrack(element); | |
194 if (element.type == 'outside-reference') | |
195 { | |
196 // Construct outside reference; | |
197 var orNode = new outsideReferenceDOM(audioObject,index,document.getElementById('interface-buttons')); | |
198 audioObject.bindInterface(orNode); | |
199 } else { | |
200 // Create a slider per track | |
201 var sliderObj = new discreteObject(audioObject,label,interfaceScales); | |
202 sliderBox.appendChild(sliderObj.holder); | |
203 audioObject.bindInterface(sliderObj); | |
204 interfaceContext.createCommentBox(audioObject); | |
205 label += 1; | |
206 } | |
207 | |
208 }); | |
209 | |
210 if (page.showElementComments) | |
211 { | |
212 interfaceContext.showCommentBoxes(feedbackHolder,true); | |
213 } | |
214 | |
215 // Auto-align | |
216 resizeWindow(null); | |
217 } | |
218 | |
219 function discreteObject(audioObject,label,interfaceScales) | |
220 { | |
221 // An example node, you can make this however you want for each audioElement. | |
222 // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following | |
223 // You attach them by calling audioObject.bindInterface( ) | |
224 if (interfaceScales == null || interfaceScales.length == 0) | |
225 { | |
226 console.log("WARNING: The discrete radio's are built depending on the number of scale points specified! Ensure you have some specified. Defaulting to 5 for now!"); | |
227 numOptions = 5; | |
228 } | |
229 this.parent = audioObject; | |
230 | |
231 this.holder = document.createElement('div'); | |
232 this.title = document.createElement('div'); | |
233 this.discreteHolder = document.createElement('div'); | |
234 this.discretes = []; | |
235 this.play = document.createElement('button'); | |
236 | |
237 this.holder.className = 'track-slider'; | |
238 this.holder.style.width = window.innerWidth-200 + 'px'; | |
239 this.holder.appendChild(this.title); | |
240 this.holder.appendChild(this.discreteHolder); | |
241 this.holder.appendChild(this.play); | |
242 this.holder.setAttribute('trackIndex',audioObject.id); | |
243 this.title.textContent = label; | |
244 this.title.className = 'track-slider-title'; | |
245 | |
246 this.discreteHolder.className = "track-slider-range"; | |
247 this.discreteHolder.style.width = window.innerWidth-500 + 'px'; | |
248 for (var i=0; i<interfaceScales.length; i++) | |
249 { | |
250 var node = document.createElement('input'); | |
251 node.setAttribute('type','radio'); | |
252 node.className = 'track-radio'; | |
253 node.disabled = true; | |
254 node.setAttribute('position',interfaceScales[i].position); | |
255 node.setAttribute('name',audioObject.specification.id); | |
256 node.setAttribute('id',audioObject.specification.id+'-'+String(i)); | |
257 this.discretes.push(node); | |
258 this.discreteHolder.appendChild(node); | |
259 node.onclick = function(event) | |
260 { | |
261 if (audioEngineContext.status == 0) | |
262 { | |
263 event.currentTarget.checked = false; | |
264 return; | |
265 } | |
266 var time = audioEngineContext.timer.getTestTime(); | |
267 var id = Number(event.currentTarget.parentNode.parentNode.getAttribute('trackIndex')); | |
268 var value = event.currentTarget.getAttribute('position') / 100.0; | |
269 audioEngineContext.audioObjects[id].metric.moved(time,value); | |
270 console.log('slider '+id+' moved to '+value+' ('+time+')'); | |
271 }; | |
272 } | |
273 | |
274 this.play.className = 'track-slider-button'; | |
275 this.play.textContent = "Loading..."; | |
276 this.play.value = audioObject.id; | |
277 this.play.disabled = true; | |
278 this.play.onclick = function(event) | |
279 { | |
280 var id = Number(event.currentTarget.value); | |
281 //audioEngineContext.metric.sliderPlayed(id); | |
282 audioEngineContext.play(id); | |
283 }; | |
284 this.resize = function(event) | |
285 { | |
286 this.holder.style.width = window.innerWidth-200 + 'px'; | |
287 this.discreteHolder.style.width = window.innerWidth-500 + 'px'; | |
288 //text.style.left = (posPix+150-($(text).width()/2)) +'px'; | |
289 for (var i=0; i<this.discretes.length; i++) | |
290 { | |
291 var width = $(this.discreteHolder).width() - 20; | |
292 var node = this.discretes[i]; | |
293 var nodeW = $(node).width(); | |
294 var position = node.getAttribute('position'); | |
295 var posPix = Math.round(width * (position / 100.0)); | |
296 node.style.left = (posPix+10 - (nodeW/2)) + 'px'; | |
297 } | |
298 }; | |
299 this.enable = function() | |
300 { | |
301 // This is used to tell the interface object that playback of this node is ready | |
302 this.play.disabled = false; | |
303 this.play.textContent = "Play"; | |
304 $(this.slider).removeClass('track-slider-disabled'); | |
305 for (var radio of this.discretes) | |
306 { | |
307 radio.disabled = false; | |
308 } | |
309 }; | |
310 this.updateLoading = function(progress) | |
311 { | |
312 // progress is a value from 0 to 100 indicating the current download state of media files | |
313 if (progress != 100) | |
314 { | |
315 progress = String(progress); | |
316 progress = progress.split('.')[0]; | |
317 this.play.textContent = progress+'%'; | |
318 } else { | |
319 this.play.textContent = "Play"; | |
320 } | |
321 }; | |
322 | |
323 this.startPlayback = function() | |
324 { | |
325 // Called by audioObject when playback begins | |
326 $(".track-slider").removeClass('track-slider-playing'); | |
327 $(this.holder).addClass('track-slider-playing'); | |
328 var outsideReference = document.getElementById('outside-reference'); | |
329 this.play.textContent = "Listening"; | |
330 if (outsideReference != null) { | |
331 $(outsideReference).removeClass('track-slider-playing'); | |
332 } | |
333 } | |
334 this.stopPlayback = function() | |
335 { | |
336 // Called by audioObject when playback stops | |
337 $(this.holder).removeClass('track-slider-playing'); | |
338 this.play.textContent = "Play"; | |
339 } | |
340 | |
341 this.getValue = function() | |
342 { | |
343 // Return the current value of the object. If there is no value, return -1 | |
344 var value = -1; | |
345 for (var i=0; i<this.discretes.length; i++) | |
346 { | |
347 if (this.discretes[i].checked == true) | |
348 { | |
349 value = this.discretes[i].getAttribute('position') / 100.0; | |
350 break; | |
351 } | |
352 } | |
353 return value; | |
354 }; | |
355 this.getPresentedId = function() | |
356 { | |
357 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale | |
358 return this.title.textContent; | |
359 }; | |
360 this.canMove = function() | |
361 { | |
362 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. | |
363 // These are checked primarily if the interface check option 'fragmentMoved' is enabled. | |
364 return true; | |
365 }; | |
366 this.exportXMLDOM = function(audioObject) { | |
367 // Called by the audioObject holding this element to export the interface <value> node. | |
368 // If there is no value node (such as outside reference), return null | |
369 // If there are multiple value nodes (such as multiple scale / 2D scales), return an array of nodes with each value node having an 'interfaceName' attribute | |
370 // Use storage.document.createElement('value'); to generate the XML node. | |
371 var node = storage.document.createElement('value'); | |
372 node.textContent = this.getValue(); | |
373 return node; | |
374 }; | |
375 }; | |
376 | |
377 function resizeWindow(event) | |
378 { | |
379 // Called on every window resize event, use this to scale your page properly | |
380 var numObj = document.getElementsByClassName('track-slider').length; | |
381 var totalHeight = (numObj * 66)-30; | |
382 document.getElementById('scale-holder').style.width = window.innerWidth-220 + 'px'; | |
383 var canvas = document.getElementById('scale-canvas'); | |
384 canvas.width = window.innerWidth-520; | |
385 canvas.height = totalHeight; | |
386 for (var i in audioEngineContext.audioObjects) | |
387 { | |
388 if (audioEngineContext.audioObjects[i].specification.type != 'outside-reference'){ | |
389 audioEngineContext.audioObjects[i].interfaceDOM.resize(event); | |
390 } | |
391 } | |
392 document.getElementById('slider-holder').style.height = totalHeight + 'px'; | |
393 document.getElementById('slider').style.height = totalHeight + 70 + 'px'; | |
394 drawScale(); | |
395 } | |
396 | |
397 function drawScale() | |
398 { | |
399 var interfaceObj = testState.currentStateMap.interfaces[0]; | |
400 var scales = testState.currentStateMap.interfaces[0].scales; | |
401 scales = scales.sort(function(a,b) { | |
402 return a.position - b.position; | |
403 }); | |
404 var canvas = document.getElementById('scale-canvas'); | |
405 var ctx = canvas.getContext("2d"); | |
406 var height = canvas.height; | |
407 var width = canvas.width; | |
408 var textHolder = document.getElementById('scale-text-holder'); | |
409 textHolder.innerHTML = null; | |
410 ctx.fillStyle = "#000000"; | |
411 ctx.setLineDash([1,4]); | |
412 for (var scale of scales) | |
413 { | |
414 var posPercent = scale.position / 100.0; | |
415 var posPix = Math.round(width * posPercent); | |
416 if(posPix<=0){posPix=1;} | |
417 if(posPix>=width){posPix=width-1;} | |
418 ctx.moveTo(posPix,0); | |
419 ctx.lineTo(posPix,height); | |
420 ctx.stroke(); | |
421 | |
422 var text = document.createElement('div'); | |
423 text.align = "center"; | |
424 var textC = document.createElement('span'); | |
425 textC.textContent = scale.text; | |
426 text.appendChild(textC); | |
427 text.className = "scale-text"; | |
428 textHolder.appendChild(text); | |
429 text.style.width = $(text.children[0]).width()+'px'; | |
430 text.style.left = (posPix+150-($(text).width()/2)) +'px'; | |
431 } | |
432 } | |
433 | |
434 function buttonSubmitClick() // TODO: Only when all songs have been played! | |
435 { | |
436 var checks = []; | |
437 checks = checks.concat(testState.currentStateMap.interfaces[0].options); | |
438 checks = checks.concat(specification.interfaces.options); | |
439 var canContinue = true; | |
440 | |
441 // Check that the anchor and reference objects are correctly placed | |
442 if (interfaceContext.checkHiddenAnchor() == false) {return;} | |
443 if (interfaceContext.checkHiddenReference() == false) {return;} | |
444 | |
445 for (var i=0; i<checks.length; i++) { | |
446 if (checks[i].type == 'check') | |
447 { | |
448 switch(checks[i].name) { | |
449 case 'fragmentPlayed': | |
450 // Check if all fragments have been played | |
451 var checkState = interfaceContext.checkAllPlayed(); | |
452 if (checkState == false) {canContinue = false;} | |
453 break; | |
454 case 'fragmentFullPlayback': | |
455 // Check all fragments have been played to their full length | |
456 var checkState = interfaceContext.checkAllPlayed(); | |
457 if (checkState == false) {canContinue = false;} | |
458 console.log('NOTE: fragmentFullPlayback not currently implemented, performing check fragmentPlayed instead'); | |
459 break; | |
460 case 'fragmentMoved': | |
461 // Check all fragment sliders have been moved. | |
462 var checkState = interfaceContext.checkAllMoved(); | |
463 if (checkState == false) {canContinue = false;} | |
464 break; | |
465 case 'fragmentComments': | |
466 // Check all fragment sliders have been moved. | |
467 var checkState = interfaceContext.checkAllCommented(); | |
468 if (checkState == false) {canContinue = false;} | |
469 break; | |
470 //case 'scalerange': | |
471 // Check the scale is used to its full width outlined by the node | |
472 //var checkState = interfaceContext.checkScaleRange(); | |
473 //if (checkState == false) {canContinue = false;} | |
474 // break; | |
475 default: | |
476 console.log("WARNING - Check option "+checks[i].check+" is not supported on this interface"); | |
477 break; | |
478 } | |
479 | |
480 } | |
481 if (!canContinue) {break;} | |
482 } | |
483 | |
484 if (canContinue) { | |
485 if (audioEngineContext.status == 1) { | |
486 var playback = document.getElementById('playback-button'); | |
487 playback.click(); | |
488 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options | |
489 } else | |
490 { | |
491 if (audioEngineContext.timer.testStarted == false) | |
492 { | |
493 alert('You have not started the test! Please press start to begin the test!'); | |
494 return; | |
495 } | |
496 } | |
497 testState.advanceState(); | |
498 } | |
499 } | |
500 | |
501 function pageXMLSave(store, pageSpecification) | |
502 { | |
503 // MANDATORY | |
504 // Saves a specific test page | |
505 // You can use this space to add any extra nodes to your XML <audioHolder> saves | |
506 // Get the current <page> information in store (remember to appendChild your data to it) | |
507 // pageSpecification is the current page node configuration | |
508 // To create new XML nodes, use storage.document.createElement(); | |
509 } |