Mercurial > hg > webaudioevaluationtool
comparison interfaces/timeline.js @ 2479:dbe43b4ab7aa
Starting timeline.js interface
author | Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk> |
---|---|
date | Wed, 03 Aug 2016 14:52:04 +0100 |
parents | |
children | 713a2d059a16 |
comparison
equal
deleted
inserted
replaced
2477:f6f4693a84eb | 2479:dbe43b4ab7aa |
---|---|
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 { | |
84 // Called each time a new test page is to be build. The page specification node is the only item passed in | |
85 var content = document.getElementById("timeline-test-content"); | |
86 content.innerHTML = ""; | |
87 var interfaceObj = page.interfaces; | |
88 if (interfaceObj.length > 1) | |
89 { | |
90 console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node"); | |
91 } | |
92 interfaceObj = interfaceObj[0]; | |
93 | |
94 //Set the page title | |
95 if (typeof page.title == "string" && page.title.length > 0) | |
96 { | |
97 document.getElementById("test-title").textContent = page.title; | |
98 } | |
99 | |
100 if (interfaceObj.title != null) { | |
101 document.getElementById("page-title").textContent = interfaceObj.title; | |
102 } | |
103 | |
104 // Delete outside reference | |
105 var outsideReferenceHolder = document.getElementById("outside-reference-holder"); | |
106 outsideReferenceHolder.innerHTML = ""; | |
107 | |
108 var commentBoxPrefix = "Comment on track"; | |
109 if (interfaceObj.commentBoxPrefix != undefined) { | |
110 commentBoxPrefix = interfaceObj.commentBoxPrefix; | |
111 } | |
112 | |
113 $(page.audioElements).each(function(index,element){ | |
114 var audioObject = audioEngineContext.newTrack(element); | |
115 if (page.audioElements.type == 'outside-reference') { | |
116 var refNode = interfaceContext.outsideReferenceDOM(audioObject,index,outsideReferenceHolder); | |
117 audioObject.bindInterface(orNode); | |
118 } else { | |
119 switch(audioObject.specification.parent.label) { | |
120 case "none": | |
121 label = ""; | |
122 break; | |
123 case "letter": | |
124 label = String.fromCharCode(97 + index); | |
125 break; | |
126 case "capital": | |
127 label = String.fromCharCode(65 + index); | |
128 break; | |
129 default: | |
130 label = ""+index; | |
131 break; | |
132 } | |
133 var node = new interfaceObject(audioObject,label); | |
134 | |
135 content.appendChild(node.DOM); | |
136 audioObject.bindInterface(node); | |
137 } | |
138 }); | |
139 | |
140 resizeWindow(); | |
141 } | |
142 | |
143 function interfaceObject(audioObject,labelstr) | |
144 { | |
145 // Each audio object has a waveform guide and self-generated comments | |
146 this.parent = audioObject; | |
147 this.DOM = document.createElement("div"); | |
148 this.DOM.className = "timeline-element"; | |
149 this.DOM.id = audioObject.specification.id; | |
150 | |
151 var root = document.createElement("div"); | |
152 root.className = "timeline-element-content"; | |
153 this.DOM.appendChild(root); | |
154 | |
155 var label = document.createElement("div"); | |
156 label.style.textAlign = "center"; | |
157 var labelSpan = document.createElement("span"); | |
158 labelSpan.textContent = "Fragment "+labelstr; | |
159 label.appendChild(labelSpan); | |
160 root.appendChild(label); | |
161 | |
162 var canvasHolder = document.createElement("div"); | |
163 canvasHolder.className = "timeline-element-canvas-holder"; | |
164 var buttonHolder = document.createElement("div"); | |
165 buttonHolder.className = "timeline-element-button-holder"; | |
166 var commentHolder = document.createElement("div"); | |
167 commentHolder.className = "timeline-element-comment-holder"; | |
168 | |
169 root.appendChild(canvasHolder); | |
170 root.appendChild(buttonHolder); | |
171 root.appendChild(commentHolder); | |
172 | |
173 this.comments = { | |
174 parent: this, | |
175 list: [], | |
176 Comment: function(parent,time, str) { | |
177 this.parent = parent; | |
178 this.time = time; | |
179 this.DOM = document.createElement("div"); | |
180 this.DOM.className = "comment-div"; | |
181 this.title = document.createElement("span"); | |
182 if (str != undefined) { | |
183 this.title.textContent = str; | |
184 } else { | |
185 this.title.textContent = "Time: "+time.toFixed(2)+"s"; | |
186 } | |
187 this.textarea = document.createElement("textarea"); | |
188 this.textarea.className = "trackComment"; | |
189 this.DOM.appendChild(this.title); | |
190 this.DOM.appendChild(document.createElement("br")); | |
191 this.DOM.appendChild(this.textarea); | |
192 this.resize = function() { | |
193 var w = window.innerWidth; | |
194 w = Math.min(w,800); | |
195 w = Math.max(w,200); | |
196 var elem_w = w / 2.5; | |
197 elem_w = Math.max(elem_w,190); | |
198 this.DOM.style.width = elem_w+"px"; | |
199 this.textarea.style.width = (elem_w-5)+"px"; | |
200 } | |
201 this.resize(); | |
202 }, | |
203 newComment: function(time) { | |
204 var node = new this.Comment(this,time); | |
205 this.list.push(node); | |
206 commentHolder.appendChild(node.DOM); | |
207 return node; | |
208 }, | |
209 deleteComment: function(comment) { | |
210 | |
211 }, | |
212 clearList: function() { | |
213 | |
214 } | |
215 } | |
216 | |
217 this.canvas = { | |
218 parent: this, | |
219 comments: this.comments, | |
220 layer1: document.createElement("canvas"), | |
221 layer2: document.createElement("canvas"), | |
222 layer3: document.createElement("canvas"), | |
223 layer4: document.createElement("canvas"), | |
224 resize: function(w) { | |
225 this.layer1.width = w; | |
226 this.layer2.width = w; | |
227 this.layer3.width = w; | |
228 this.layer4.width = w; | |
229 this.layer1.style.width = w+"px"; | |
230 this.layer2.style.width = w+"px"; | |
231 this.layer3.style.width = w+"px"; | |
232 this.layer4.style.width = w+"px"; | |
233 }, | |
234 handleEvent: function(event) { | |
235 switch(event.currentTarget) { | |
236 case this.layer1: | |
237 switch(event.type) { | |
238 case "mousemove": | |
239 this.drawMouse(event); | |
240 break; | |
241 case "mouseleave": | |
242 this.clearCanvas(this.layer1); | |
243 break; | |
244 case "click": | |
245 var rect = this.layer1.getBoundingClientRect(); | |
246 var pixX = event.clientX - rect.left; | |
247 var tpp = this.parent.parent.buffer.buffer.duration/this.layer1.width; | |
248 this.comments.newComment(pixX*tpp); | |
249 this.drawMarkers(); | |
250 break; | |
251 } | |
252 break; | |
253 } | |
254 }, | |
255 drawWaveform: function() { | |
256 var buffer = this.parent.parent.buffer.buffer; | |
257 var context = this.layer4.getContext("2d"); | |
258 context.lineWidth = 1; | |
259 context.strokeStyle = "#888"; | |
260 context.clearRect(0,0,this.layer4.width, this.layer4.height); | |
261 var data = buffer.getChannelData(0); | |
262 var t_per_pixel = buffer.duration/this.layer4.width; | |
263 var s_per_pixel = data.length/this.layer4.width; | |
264 var pixX = 0; | |
265 while (pixX < this.layer4.width) { | |
266 var start = Math.floor(s_per_pixel*pixX); | |
267 var end = Math.min(Math.ceil(s_per_pixel*(pixX+1)),data.length); | |
268 var frame = data.subarray(start,end); | |
269 var min = frame[0]; | |
270 var max = min; | |
271 for (var n=0; n<frame.length; n++) { | |
272 if (frame[n] < min) {min = frame[n];} | |
273 if (frame[n] > max) {max = frame[n];} | |
274 } | |
275 // Assuming min/max normalised between [-1, 1] to map to [150, 0] | |
276 context.beginPath(); | |
277 context.moveTo(pixX+0.5,(min+1)*-75+150); | |
278 context.lineTo(pixX+0.5,(max+1)*-75+150); | |
279 context.stroke(); | |
280 pixX++; | |
281 } | |
282 }, | |
283 drawMouse: function(event) { | |
284 var context = this.layer1.getContext("2d"); | |
285 context.clearRect(0,0,this.layer1.width, this.layer1.height); | |
286 var rect = this.layer1.getBoundingClientRect(); | |
287 var pixX = event.clientX - rect.left; | |
288 pixX = Math.floor(pixX)-0.5; | |
289 context.strokeStyle = "#800"; | |
290 context.beginPath(); | |
291 context.moveTo(pixX,0); | |
292 context.lineTo(pixX,this.layer1.height); | |
293 context.stroke(); | |
294 }, | |
295 drawTicker: function() { | |
296 var context = this.layer2.getContext("2d"); | |
297 context.clearRect(0,0,this.layer2.width, this.layer2.height); | |
298 var time = this.parent.parent.getCurrentPosition(); | |
299 var ratio = time / this.parent.parent.buffer.buffer.duration; | |
300 var pixX = Math.floor(ratio*this.layer2.width)+0.5; | |
301 context.strokeStyle = "#080"; | |
302 context.beginPath(); | |
303 context.moveTo(pixX,0); | |
304 context.lineTo(pixX,this.layer2.height); | |
305 context.stroke(); | |
306 }, | |
307 drawMarkers: function() { | |
308 var context = this.layer3.getContext("2d"); | |
309 context.clearRect(0,0,this.layer3.width, this.layer3.height); | |
310 context.strokeStyle = "#008"; | |
311 var tpp = this.parent.parent.buffer.buffer.duration/this.layer1.width; | |
312 for (var i=0; i<this.comments.list.length; i++) { | |
313 var comment = this.comments.list[i]; | |
314 var pixX = Math.floor(comment.time/tpp)+0.5; | |
315 context.beginPath(); | |
316 context.moveTo(pixX,0); | |
317 context.lineTo(pixX,this.layer3.height); | |
318 context.stroke(); | |
319 } | |
320 }, | |
321 clearCanvas: function(canvas) { | |
322 var context = canvas.getContext("2d"); | |
323 context.clearRect(0,0,canvas.width, canvas.height); | |
324 } | |
325 } | |
326 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled"; | |
327 this.canvas.layer2.className = "timeline-element-canvas canvas-layer2"; | |
328 this.canvas.layer3.className = "timeline-element-canvas canvas-layer3"; | |
329 this.canvas.layer4.className = "timeline-element-canvas canvas-layer3"; | |
330 this.canvas.layer1.height = "150"; | |
331 this.canvas.layer2.height = "150"; | |
332 this.canvas.layer3.height = "150"; | |
333 this.canvas.layer4.height = "150"; | |
334 canvasHolder.appendChild(this.canvas.layer1); | |
335 canvasHolder.appendChild(this.canvas.layer2); | |
336 canvasHolder.appendChild(this.canvas.layer3); | |
337 canvasHolder.appendChild(this.canvas.layer4); | |
338 this.canvas.layer1.addEventListener("mousemove",this.canvas); | |
339 this.canvas.layer1.addEventListener("mouseleave",this.canvas); | |
340 this.canvas.layer1.addEventListener("click",this.canvas); | |
341 | |
342 var canvasIntervalID = null; | |
343 | |
344 this.playButton = { | |
345 parent: this, | |
346 DOM: document.createElement("button"), | |
347 handleEvent: function(event) { | |
348 var id = this.parent.parent.id; | |
349 var str = this.DOM.textContent; | |
350 if (str == "Play") { | |
351 audioEngineContext.play(id); | |
352 } else if (str == "Stop") { | |
353 audioEngineContext.stop(); | |
354 } | |
355 } | |
356 } | |
357 this.playButton.DOM.addEventListener("click",this.playButton); | |
358 this.playButton.DOM.className = "timeline-button timeline-button-disabled"; | |
359 this.playButton.DOM.disabled = true; | |
360 this.playButton.DOM.textContent = "Wait"; | |
361 | |
362 this.clearButton = { | |
363 parent: this, | |
364 DOM: document.createElement("button"), | |
365 handleEvent: function(event) { | |
366 this.parent.comments.clearList(); | |
367 } | |
368 } | |
369 this.clearButton.DOM.addEventListener("click",this.clearButton); | |
370 this.clearButton.DOM.className = "timeline-button"; | |
371 this.clearButton.DOM.textContent = "Clear"; | |
372 | |
373 | |
374 buttonHolder.appendChild(this.playButton.DOM); | |
375 buttonHolder.appendChild(this.clearButton.DOM); | |
376 | |
377 this.resize = function() { | |
378 var w = window.innerWidth; | |
379 w = Math.min(w,800); | |
380 w = Math.max(w,200); | |
381 root.style.width = w+"px"; | |
382 var c_w = w-100; | |
383 this.canvas.resize(c_w); | |
384 } | |
385 | |
386 this.enable = function() | |
387 { | |
388 // This is used to tell the interface object that playback of this node is ready | |
389 this.canvas.layer1.addEventListener("click",this.canvas); | |
390 this.canvas.layer1.className = "timeline-element-canvas canvas-layer1"; | |
391 this.playButton.DOM.className = "timeline-button timeline-button-play"; | |
392 this.playButton.DOM.textContent = "Play"; | |
393 this.playButton.DOM.disabled = false; | |
394 | |
395 this.canvas.drawWaveform(); | |
396 }; | |
397 this.updateLoading = function(progress) | |
398 { | |
399 // progress is a value from 0 to 100 indicating the current download state of media files | |
400 progress = String(progress); | |
401 progress = progress.substr(0,5); | |
402 this.playButton.DOM.textContent = "Loading: "+progress+'%'; | |
403 }; | |
404 this.startPlayback = function() | |
405 { | |
406 // Called when playback has begun | |
407 canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas),100); | |
408 this.playButton.DOM.textContent = "Stop"; | |
409 }; | |
410 this.stopPlayback = function() | |
411 { | |
412 // Called when playback has stopped. This gets called even if playback never started! | |
413 window.clearInterval(canvasIntervalID); | |
414 this.canvas.clearCanvas(this.canvas.layer2); | |
415 this.playButton.DOM.textContent = "Play"; | |
416 }; | |
417 this.getValue = function() | |
418 { | |
419 // Return the current value of the object. If there is no value, return 0 | |
420 return 0; | |
421 }; | |
422 this.getPresentedId = function() | |
423 { | |
424 // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale | |
425 return labelSpan.textContent; | |
426 }; | |
427 this.canMove = function() | |
428 { | |
429 // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. | |
430 // These are checked primarily if the interface check option 'fragmentMoved' is enabled. | |
431 return false; | |
432 }; | |
433 this.exportXMLDOM = function(audioObject) { | |
434 // Called by the audioObject holding this element to export the interface <value> node. | |
435 // If there is no value node (such as outside reference), return null | |
436 // 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 | |
437 // Use storage.document.createElement('value'); to generate the XML node. | |
438 return null; | |
439 }; | |
440 this.error = function() { | |
441 // If there is an error with the audioObject, this will be called to indicate a failure | |
442 } | |
443 }; | |
444 | |
445 function resizeWindow(event) | |
446 { | |
447 // Called on every window resize event, use this to scale your page properly | |
448 for (var i=0; i<audioEngineContext.audioObjects.length; i++) { | |
449 audioEngineContext.audioObjects[i].interfaceDOM.resize(); | |
450 } | |
451 } | |
452 | |
453 function buttonSubmitClick() | |
454 { | |
455 } | |
456 | |
457 function pageXMLSave(store, pageSpecification) | |
458 { | |
459 // MANDATORY | |
460 // Saves a specific test page | |
461 // You can use this space to add any extra nodes to your XML <audioHolder> saves | |
462 // Get the current <page> information in store (remember to appendChild your data to it) | |
463 // pageSpecification is the current page node configuration | |
464 // To create new XML nodes, use storage.document.createElement(); | |
465 } |