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