Mercurial > hg > webaudioevaluationtool
comparison interfaces/timeline.js @ 2484:83c1352ce756
Merge branch 'master' of https://github.com/BrechtDeMan/WebAudioEvaluationTool
author | www-data <www-data@sucuk.dcs.qmul.ac.uk> |
---|---|
date | Thu, 04 Aug 2016 12:20:54 +0100 |
parents | 3c92c732fb05 |
children | 03a9a90717ed c8e6991951ad |
comparison
equal
deleted
inserted
replaced
2478:1cc68e4ce885 | 2484:83c1352ce756 |
---|---|
1 /** | |
2 * WAET Timeline | |
3 * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing. | |
4 */ | |
5 | |
6 // Once this is loaded and parsed, begin execution | |
7 loadInterface(); | |
8 | |
9 function loadInterface() { | |
10 // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, | |
11 // holding div's, or setting up any nodes which are present for the entire test sequence | |
12 | |
13 interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema | |
14 | |
15 interfaceContext.insertPoint = document.getElementById("topLevelBody"); | |
16 var testContent = document.createElement("div"); | |
17 | |
18 // Create the top div and Title element | |
19 var title = document.createElement("div"); | |
20 title.className = "title"; | |
21 title.align = "center"; | |
22 var titleSpan = document.createElement("span"); | |
23 titleSpan.id = "test-title"; | |
24 titleSpan.textContent = "Listening Test"; | |
25 title.appendChild(titleSpan); | |
26 | |
27 var pagetitle = document.createElement("div"); | |
28 pagetitle.className = "pageTitle"; | |
29 pagetitle.align = "center"; | |
30 titleSpan = document.createElement("span"); | |
31 titleSpan.id = "page-title"; | |
32 pagetitle.appendChild(titleSpan); | |
33 | |
34 // Create Interface buttons | |
35 var interfaceButtons = document.createElement("div"); | |
36 interfaceButtons.id = 'interface-buttons'; | |
37 interfaceButtons.style.height = "25px"; | |
38 | |
39 // Create playback start/stop points | |
40 var playback = document.createElement("button"); | |
41 playback.innerHTML = "Stop"; | |
42 playback.id = "playback-button"; | |
43 playback.onclick = function () { | |
44 if (audioEngineContext.status == 1) { | |
45 audioEngineContext.stop(); | |
46 this.innerHTML = "Stop"; | |
47 var time = audioEngineContext.timer.getTestTime(); | |
48 console.log("Stopped at " + time); | |
49 } | |
50 }; | |
51 // Create Submit (save) button | |
52 var submit = document.createElement("button"); | |
53 submit.innerHTML = 'Next'; | |
54 submit.onclick = buttonSubmitClick; | |
55 submit.id = 'submit-button'; | |
56 submit.style.float = 'left'; | |
57 // Append the interface buttons into the interfaceButtons object. | |
58 interfaceButtons.appendChild(playback); | |
59 interfaceButtons.appendChild(submit); | |
60 | |
61 // Create outside reference holder | |
62 var outsideRef = document.createElement("div"); | |
63 outsideRef.id = "outside-reference-holder"; | |
64 | |
65 // Create content point | |
66 var content = document.createElement("div"); | |
67 content.id = "timeline-test-content"; | |
68 | |
69 //Inject | |
70 testContent.appendChild(title); | |
71 testContent.appendChild(pagetitle); | |
72 testContent.appendChild(interfaceButtons); | |
73 testContent.appendChild(outsideRef); | |
74 testContent.appendChild(content); | |
75 interfaceContext.insertPoint.appendChild(testContent); | |
76 | |
77 // Load the full interface | |
78 testState.initialise(); | |
79 testState.advanceState(); | |
80 }; | |
81 | |
82 function loadTest(page) { | |
83 // Called each time a new test page is to be build. The page specification node is the only item passed in | |
84 var content = document.getElementById("timeline-test-content"); | |
85 content.innerHTML = ""; | |
86 var interfaceObj = page.interfaces; | |
87 if (interfaceObj.length > 1) { | |
88 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node"); | |
89 } | |
90 interfaceObj = interfaceObj[0]; | |
91 | |
92 //Set the page title | |
93 if (typeof page.title == "string" && page.title.length > 0) { | |
94 document.getElementById("test-title").textContent = page.title; | |
95 } | |
96 | |
97 if (interfaceObj.title != null) { | |
98 document.getElementById("page-title").textContent = interfaceObj.title; | |
99 } | |
100 | |
101 // Delete outside reference | |
102 var outsideReferenceHolder = document.getElementById("outside-reference-holder"); | |
103 outsideReferenceHolder.innerHTML = ""; | |
104 | |
105 var commentBoxPrefix = "Comment on track"; | |
106 if (interfaceObj.commentBoxPrefix != undefined) { | |
107 commentBoxPrefix = interfaceObj.commentBoxPrefix; | |
108 } | |
109 | |
110 $(page.audioElements).each(function (index, element) { | |
111 var audioObject = audioEngineContext.newTrack(element); | |
112 if (page.audioElements.type == 'outside-reference') { | |
113 var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder); | |
114 audioObject.bindInterface(orNode); | |
115 } else { | |
116 switch (audioObject.specification.parent.label) { | |
117 case "none": | |
118 label = ""; | |
119 break; | |
120 case "letter": | |
121 label = String.fromCharCode(97 + index); | |
122 break; | |
123 case "capital": | |
124 label = String.fromCharCode(65 + index); | |
125 break; | |
126 default: | |
127 label = "" + index; | |
128 break; | |
129 } | |
130 var node = new interfaceObject(audioObject, label); | |
131 | |
132 content.appendChild(node.DOM); | |
133 audioObject.bindInterface(node); | |
134 } | |
135 }); | |
136 | |
137 resizeWindow(); | |
138 } | |
139 | |
140 function interfaceObject(audioObject, labelstr) { | |
141 // Each audio object has a waveform guide and self-generated comments | |
142 this.parent = audioObject; | |
143 this.DOM = document.createElement("div"); | |
144 this.DOM.className = "timeline-element"; | |
145 this.DOM.id = audioObject.specification.id; | |
146 | |
147 var root = document.createElement("div"); | |
148 root.className = "timeline-element-content"; | |
149 this.DOM.appendChild(root); | |
150 | |
151 var label = document.createElement("div"); | |
152 label.style.textAlign = "center"; | |
153 var labelSpan = document.createElement("span"); | |
154 labelSpan.textContent = "Fragment " + labelstr; | |
155 label.appendChild(labelSpan); | |
156 root.appendChild(label); | |
157 | |
158 var canvasHolder = document.createElement("div"); | |
159 canvasHolder.className = "timeline-element-canvas-holder"; | |
160 var buttonHolder = document.createElement("div"); | |
161 buttonHolder.className = "timeline-element-button-holder"; | |
162 var commentHolder = document.createElement("div"); | |
163 commentHolder.className = "timeline-element-comment-holder"; | |
164 | |
165 root.appendChild(canvasHolder); | |
166 root.appendChild(buttonHolder); | |
167 root.appendChild(commentHolder); | |
168 | |
169 this.comments = { | |
170 parent: this, | |
171 list: [], | |
172 Comment: function (parent, time, str) { | |
173 this.parent = parent; | |
174 this.time = time; | |
175 this.DOM = document.createElement("div"); | |
176 this.DOM.className = "comment-entry"; | |
177 var titleHolder = document.createElement("div"); | |
178 titleHolder.className = "comment-entry-header"; | |
179 this.title = document.createElement("span"); | |
180 if (str != undefined) { | |
181 this.title.textContent = str; | |
182 } else { | |
183 this.title.textContent = "Time: " + time.toFixed(2) + "s"; | |
184 } | |
185 titleHolder.appendChild(this.title); | |
186 this.textarea = document.createElement("textarea"); | |
187 this.textarea.className = "comment-entry-text"; | |
188 this.DOM.appendChild(titleHolder); | |
189 this.DOM.appendChild(this.textarea); | |
190 | |
191 this.clear = { | |
192 DOM: document.createElement("button"), | |
193 parent: this, | |
194 handleEvent: function () { | |
195 this.parent.parent.deleteComment(this.parent); | |
196 } | |
197 } | |
198 this.clear.DOM.textContent = "Delete"; | |
199 this.clear.DOM.addEventListener("click", this.clear); | |
200 titleHolder.appendChild(this.clear.DOM); | |
201 | |
202 this.resize = function () { | |
203 var w = window.innerWidth; | |
204 w = Math.min(w, 800); | |
205 w = Math.max(w, 200); | |
206 var elem_w = w / 2.5; | |
207 elem_w = Math.max(elem_w, 190); | |
208 this.DOM.style.width = elem_w + "px"; | |
209 this.textarea.style.width = (elem_w - 5) + "px"; | |
210 } | |
211 this.buildXML = function (root) { | |
212 //storage.document.createElement(); | |
213 var node = storage.document.createElement("comment"); | |
214 var question = storage.document.createElement("question"); | |
215 var comment = storage.document.createElement("response"); | |
216 node.setAttribute("time", this.time); | |
217 question.textContent = this.title.textContent; | |
218 comment.textContent = this.textarea.value; | |
219 node.appendChild(question); | |
220 node.appendChild(comment); | |
221 root.appendChild(node); | |
222 } | |
223 this.resize(); | |
224 }, | |
225 newComment: function (time) { | |
226 var node = new this.Comment(this, time); | |
227 this.list.push(node); | |
228 commentHolder.appendChild(node.DOM); | |
229 return node; | |
230 }, | |
231 deleteComment: function (comment) { | |
232 var index = this.list.findIndex(function (element, index, array) { | |
233 if (element == comment) { | |
234 return true; | |
235 } | |
236 return false; | |
237 }, comment); | |
238 if (index == -1) { | |
239 return false; | |
240 } | |
241 var node = this.list.splice(index, 1); | |
242 comment.DOM.remove(); | |
243 this.parent.canvas.drawMarkers(); | |
244 return true; | |
245 }, | |
246 clearList: function () { | |
247 while (this.list.length > 0) { | |
248 this.deleteComment(this.list[0]); | |
249 } | |
250 } | |
251 } | |
252 | |
253 this.canvas = { | |
254 parent: this, | |
255 comments: this.comments, | |
256 layer1: document.createElement("canvas"), | |
257 layer2: document.createElement("canvas"), | |
258 layer3: document.createElement("canvas"), | |
259 layer4: document.createElement("canvas"), | |
260 resize: function (w) { | |
261 this.layer1.width = w; | |
262 this.layer2.width = w; | |
263 this.layer3.width = w; | |
264 this.layer4.width = w; | |
265 this.layer1.style.width = w + "px"; | |
266 this.layer2.style.width = w + "px"; | |
267 this.layer3.style.width = w + "px"; | |
268 this.layer4.style.width = w + "px"; | |
269 this.drawWaveform(); | |
270 this.drawMarkers(); | |
271 }, | |
272 handleEvent: function (event) { | |
273 switch (event.currentTarget) { | |
274 case this.layer1: | |
275 switch (event.type) { | |
276 case "mousemove": | |
277 this.drawMouse(event); | |
278 break; | |
279 case "mouseleave": | |
280 this.clearCanvas(this.layer1); | |
281 break; | |
282 case "click": | |
283 var rect = this.layer1.getBoundingClientRect(); | |
284 var pixX = event.clientX - rect.left; | |
285 var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width; | |
286 this.comments.newComment(pixX * tpp); | |
287 this.drawMarkers(); | |
288 break; | |
289 } | |
290 break; | |
291 } | |
292 }, | |
293 drawWaveform: function () { | |
294 if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) { | |
295 return; | |
296 } | |
297 var buffer = this.parent.parent.buffer.buffer; | |
298 var context = this.layer4.getContext("2d"); | |
299 context.lineWidth = 1; | |
300 context.strokeStyle = "#888"; | |
301 context.clearRect(0, 0, this.layer4.width, this.layer4.height); | |
302 var data = buffer.getChannelData(0); | |
303 var t_per_pixel = buffer.duration / this.layer4.width; | |
304 var s_per_pixel = data.length / this.layer4.width; | |
305 var pixX = 0; | |
306 while (pixX < this.layer4.width) { | |
307 var start = Math.floor(s_per_pixel * pixX); | |
308 var end = Math.min(Math.ceil(s_per_pixel * (pixX + 1)), data.length); | |
309 var frame = data.subarray(start, end); | |
310 var min = frame[0]; | |
311 var max = min; | |
312 for (var n = 0; n < frame.length; n++) { | |
313 if (frame[n] < min) { | |
314 min = frame[n]; | |
315 } | |
316 if (frame[n] > max) { | |
317 max = frame[n]; | |
318 } | |
319 } | |
320 // Assuming min/max normalised between [-1, 1] to map to [150, 0] | |
321 context.beginPath(); | |
322 context.moveTo(pixX + 0.5, (min + 1) * -75 + 150); | |
323 context.lineTo(pixX + 0.5, (max + 1) * -75 + 150); | |
324 context.stroke(); | |
325 pixX++; | |
326 } | |
327 }, | |
328 drawMouse: function (event) { | |
329 var context = this.layer1.getContext("2d"); | |
330 context.clearRect(0, 0, this.layer1.width, this.layer1.height); | |
331 var rect = this.layer1.getBoundingClientRect(); | |
332 var pixX = event.clientX - rect.left; | |
333 pixX = Math.floor(pixX) - 0.5; | |
334 context.strokeStyle = "#800"; | |
335 context.beginPath(); | |
336 context.moveTo(pixX, 0); | |
337 context.lineTo(pixX, this.layer1.height); | |
338 context.stroke(); | |
339 }, | |
340 drawTicker: function () { | |
341 var context = this.layer2.getContext("2d"); | |
342 context.clearRect(0, 0, this.layer2.width, this.layer2.height); | |
343 var time = this.parent.parent.getCurrentPosition(); | |
344 var ratio = time / this.parent.parent.buffer.buffer.duration; | |
345 var pixX = Math.floor(ratio * this.layer2.width) + 0.5; | |
346 context.strokeStyle = "#080"; | |
347 context.beginPath(); | |
348 context.moveTo(pixX, 0); | |
349 context.lineTo(pixX, this.layer2.height); | |
350 context.stroke(); | |
351 }, | |
352 drawMarkers: function () { | |
353 if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) { | |
354 return; | |
355 } | |
356 var context = this.layer3.getContext("2d"); | |
357 context.clearRect(0, 0, this.layer3.width, this.layer3.height); | |
358 context.strokeStyle = "#008"; | |
359 var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width; | |
360 for (var i = 0; i < this.comments.list.length; i++) { | |
361 var comment = this.comments.list[i]; | |
362 var pixX = Math.floor(comment.time / tpp) + 0.5; | |
363 context.beginPath(); | |
364 context.moveTo(pixX, 0); | |
365 context.lineTo(pixX, this.layer3.height); | |
366 context.stroke(); | |
367 } | |
368 }, | |
369 clearCanvas: function (canvas) { | |
370 var context = canvas.getContext("2d"); | |
371 context.clearRect(0, 0, canvas.width, canvas.height); | |
372 } | |
373 } | |
374 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled"; | |
375 this.canvas.layer2.className = "timeline-element-canvas canvas-layer2"; | |
376 this.canvas.layer3.className = "timeline-element-canvas canvas-layer3"; | |
377 this.canvas.layer4.className = "timeline-element-canvas canvas-layer3"; | |
378 this.canvas.layer1.height = "150"; | |
379 this.canvas.layer2.height = "150"; | |
380 this.canvas.layer3.height = "150"; | |
381 this.canvas.layer4.height = "150"; | |
382 canvasHolder.appendChild(this.canvas.layer1); | |
383 canvasHolder.appendChild(this.canvas.layer2); | |
384 canvasHolder.appendChild(this.canvas.layer3); | |
385 canvasHolder.appendChild(this.canvas.layer4); | |
386 this.canvas.layer1.addEventListener("mousemove", this.canvas); | |
387 this.canvas.layer1.addEventListener("mouseleave", this.canvas); | |
388 this.canvas.layer1.addEventListener("click", this.canvas); | |
389 | |
390 var canvasIntervalID = null; | |
391 | |
392 this.playButton = { | |
393 parent: this, | |
394 DOM: document.createElement("button"), | |
395 handleEvent: function (event) { | |
396 var id = this.parent.parent.id; | |
397 var str = this.DOM.textContent; | |
398 if (str == "Play") { | |
399 audioEngineContext.play(id); | |
400 } else if (str == "Stop") { | |
401 audioEngineContext.stop(); | |
402 } | |
403 } | |
404 } | |
405 this.playButton.DOM.addEventListener("click", this.playButton); | |
406 this.playButton.DOM.className = "timeline-button timeline-button-disabled"; | |
407 this.playButton.DOM.disabled = true; | |
408 this.playButton.DOM.textContent = "Wait"; | |
409 | |
410 buttonHolder.appendChild(this.playButton.DOM); | |
411 | |
412 this.resize = function () { | |
413 var w = window.innerWidth; | |
414 w = Math.min(w, 800); | |
415 w = Math.max(w, 200); | |
416 root.style.width = w + "px"; | |
417 var c_w = w - 100; | |
418 this.canvas.resize(c_w); | |
419 } | |
420 | |
421 this.enable = function () { | |
422 // This is used to tell the interface object that playback of this node is ready | |
423 this.canvas.layer1.addEventListener("click", this.canvas); | |
424 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1"; | |
425 this.playButton.DOM.className = "timeline-button timeline-button-play"; | |
426 this.playButton.DOM.textContent = "Play"; | |
427 this.playButton.DOM.disabled = false; | |
428 | |
429 this.canvas.drawWaveform(); | |
430 }; | |
431 this.updateLoading = function (progress) { | |
432 // progress is a value from 0 to 100 indicating the current download state of media files | |
433 progress = String(progress); | |
434 progress = progress.substr(0, 5); | |
435 this.playButton.DOM.textContent = "Loading: " + progress + '%'; | |
436 }; | |
437 this.startPlayback = function () { | |
438 // Called when playback has begun | |
439 canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100); | |
440 this.playButton.DOM.textContent = "Stop"; | |
441 }; | |
442 this.stopPlayback = function () { | |
443 // Called when playback has stopped. This gets called even if playback never started! | |
444 window.clearInterval(canvasIntervalID); | |
445 this.canvas.clearCanvas(this.canvas.layer2); | |
446 this.playButton.DOM.textContent = "Play"; | |
447 }; | |
448 this.getValue = function () { | |
449 // Return the current value of the object. If there is no value, return 0 | |
450 return 0; | |
451 }; | |
452 this.getPresentedId = function () { | |
453 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale | |
454 return labelSpan.textContent; | |
455 }; | |
456 this.canMove = function () { | |
457 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. | |
458 // These are checked primarily if the interface check option 'fragmentMoved' is enabled. | |
459 return false; | |
460 }; | |
461 this.exportXMLDOM = function (audioObject) { | |
462 // Called by the audioObject holding this element to export the interface <value> node. | |
463 // If there is no value node (such as outside reference), return null | |
464 // 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 | |
465 // Use storage.document.createElement('value'); to generate the XML node. | |
466 return null; | |
467 }; | |
468 this.error = function () { | |
469 // If there is an error with the audioObject, this will be called to indicate a failure | |
470 } | |
471 }; | |
472 | |
473 function resizeWindow(event) { | |
474 // Called on every window resize event, use this to scale your page properly | |
475 for (var i = 0; i < audioEngineContext.audioObjects.length; i++) { | |
476 audioEngineContext.audioObjects[i].interfaceDOM.resize(); | |
477 } | |
478 } | |
479 | |
480 function buttonSubmitClick() { | |
481 if (audioEngineContext.timer.testStarted == false) { | |
482 interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!'); | |
483 return; | |
484 } | |
485 var checks = []; | |
486 checks = checks.concat(testState.currentStateMap.interfaces[0].options); | |
487 checks = checks.concat(specification.interfaces.options); | |
488 var canContinue = true; | |
489 for (var i = 0; i < checks.length; i++) { | |
490 var checkState = true; | |
491 if (checks[i].type == 'check') { | |
492 switch (checks[i].name) { | |
493 case 'fragmentPlayed': | |
494 //Check if all fragments have been played | |
495 checkState = interfaceContext.checkAllPlayed(); | |
496 break; | |
497 case 'fragmentFullPlayback': | |
498 //Check if all fragments have played to their full length | |
499 checkState = interfaceContext.checkFragmentsFullyPlayed(); | |
500 break; | |
501 case 'fragmentComments': | |
502 checkState = interfaceContext.checkAllCommented(); | |
503 break; | |
504 default: | |
505 console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface"); | |
506 break; | |
507 } | |
508 if (checkState == false) { | |
509 canContinue == false; | |
510 } | |
511 } | |
512 if (!canContinue) { | |
513 return; | |
514 } | |
515 } | |
516 | |
517 if (canContinue) { | |
518 if (audioEngineContext.status == 1) { | |
519 var playback = document.getElementById('playback-button'); | |
520 playback.click(); | |
521 // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options | |
522 } | |
523 testState.advanceState(); | |
524 } | |
525 } | |
526 | |
527 function pageXMLSave(store, pageSpecification) { | |
528 // MANDATORY | |
529 // Saves a specific test page | |
530 // You can use this space to add any extra nodes to your XML <audioHolder> saves | |
531 // Get the current <page> information in store (remember to appendChild your data to it) | |
532 // pageSpecification is the current page node configuration | |
533 // To create new XML nodes, use storage.document.createElement(); | |
534 | |
535 for (var i = 0; i < audioEngineContext.audioObjects.length; i++) { | |
536 var id = audioEngineContext.audioObjects[i].specification.id; | |
537 var commentsList = audioEngineContext.audioObjects[i].interfaceDOM.comments.list; | |
538 var root = audioEngineContext.audioObjects[i].storeDOM; | |
539 for (var j = 0; j < commentsList.length; j++) { | |
540 commentsList[j].buildXML(root); | |
541 } | |
542 } | |
543 } |