Mercurial > hg > webaudioevaluationtool
changeset 619:c99d334d8534 Dev_main
WIP. ABX Framework. Minor core.js modifications.
author | Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk> |
---|---|
date | Wed, 16 Mar 2016 13:31:42 +0000 |
parents | 4a221e761384 |
children | e0934138c676 |
files | core.js example_eval/ABX_example.xml interfaces/ABX.css interfaces/ABX.js |
diffstat | 4 files changed, 588 insertions(+), 53 deletions(-) [+] |
line wrap: on
line diff
--- a/core.js Tue Mar 15 15:33:29 2016 +0000 +++ b/core.js Wed Mar 16 13:31:42 2016 +0000 @@ -281,69 +281,82 @@ switch(specification.interface) { case "APE": - interfaceJS.setAttribute("src","interfaces/ape.js"); - - // APE comes with a css file - var css = document.createElement('link'); - css.rel = 'stylesheet'; - css.type = 'text/css'; - css.href = 'interfaces/ape.css'; - - document.getElementsByTagName("head")[0].appendChild(css); - break; - + interfaceJS.setAttribute("src","interfaces/ape.js"); + + // APE comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/ape.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; + case "MUSHRA": - interfaceJS.setAttribute("src","interfaces/mushra.js"); - - // MUSHRA comes with a css file - var css = document.createElement('link'); - css.rel = 'stylesheet'; - css.type = 'text/css'; - css.href = 'interfaces/mushra.css'; - - document.getElementsByTagName("head")[0].appendChild(css); - break; + interfaceJS.setAttribute("src","interfaces/mushra.js"); + + // MUSHRA comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/mushra.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; case "AB": - interfaceJS.setAttribute("src","interfaces/AB.js"); - - // AB comes with a css file - var css = document.createElement('link'); - css.rel = 'stylesheet'; - css.type = 'text/css'; - css.href = 'interfaces/AB.css'; - - document.getElementsByTagName("head")[0].appendChild(css); - break; + interfaceJS.setAttribute("src","interfaces/AB.js"); + + // AB comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/AB.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; + + case "ABX": + interfaceJS.setAttribute("src","interfaces/ABX.js"); + + // AB comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/ABX.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; + case "Bipolar": case "ACR": case "DCR": case "CCR": case "ABC": - // Above enumerate to horizontal sliders - interfaceJS.setAttribute("src","interfaces/horizontal-sliders.js"); - - // horizontal-sliders comes with a css file - var css = document.createElement('link'); - css.rel = 'stylesheet'; - css.type = 'text/css'; - css.href = 'interfaces/horizontal-sliders.css'; - - document.getElementsByTagName("head")[0].appendChild(css); - break; + // Above enumerate to horizontal sliders + interfaceJS.setAttribute("src","interfaces/horizontal-sliders.js"); + + // horizontal-sliders comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/horizontal-sliders.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; case "discrete": case "likert": - // Above enumerate to horizontal discrete radios - interfaceJS.setAttribute("src","interfaces/discrete.js"); - - // horizontal-sliders comes with a css file - var css = document.createElement('link'); - css.rel = 'stylesheet'; - css.type = 'text/css'; - css.href = 'interfaces/discrete.css'; - - document.getElementsByTagName("head")[0].appendChild(css); - break; + // Above enumerate to horizontal discrete radios + interfaceJS.setAttribute("src","interfaces/discrete.js"); + + // horizontal-sliders comes with a css file + var css = document.createElement('link'); + css.rel = 'stylesheet'; + css.type = 'text/css'; + css.href = 'interfaces/discrete.css'; + + document.getElementsByTagName("head")[0].appendChild(css); + break; } document.getElementsByTagName("head")[0].appendChild(interfaceJS);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/example_eval/ABX_example.xml Wed Mar 16 13:31:42 2016 +0000 @@ -0,0 +1,87 @@ +<?xml version="1.0" encoding="utf-8"?> +<waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd"> + <setup interface="ABX" projectReturn="save.php" randomiseOrder='true' testPages="2" loudness="-23" sampleRate="44100"> + <survey location="before"> + <surveyentry type="question" id="sessionId" mandatory="true"> + <statement>Please enter your name.</statement> + </surveyentry> + <surveyentry type="checkbox" id="checkboxtest" mandatory="true"> + <statement>Please select with which activities you have any experience (example checkbox question)</statement> + <option name="musician">Playing a musical instrument</option> + <option name="soundengineer">Recording or mixing audio</option> + <option name="developer">Developing audio software</option> + <option name="hwdesigner">Designing or building audio hardware</option> + <option name="researcher">Research in the field of audio</option> + </surveyentry> + <surveyentry type="statement" id="test-intro"> + <statement>This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'.</statement> + </surveyentry> + </survey> + <survey location="after"> + <surveyentry type="question" id="location" mandatory="true" boxsize="large"> + <statement>Please enter your location. (example mandatory text question)</statement> + </surveyentry> + <surveyentry type="number" id="age" min="0"> + <statement>Please enter your age (example non-mandatory number question)</statement> + </surveyentry> + <surveyentry type="radio" id="rating"> + <statement>Please rate this interface (example radio button question)</statement> + <option name="bad">Bad</option> + <option name="poor">Poor</option> + <option name="good">Good</option> + <option name="great">Great</option> + </surveyentry> + <surveyentry type="statement" id="test-thank-you"> + <statement>Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder.</statement> + </surveyentry> + </survey> + <metric> + <metricenable>testTimer</metricenable> + <metricenable>elementTimer</metricenable> + <metricenable>elementInitialPosition</metricenable> + <metricenable>elementTracker</metricenable> + <metricenable>elementFlagListenedTo</metricenable> + <metricenable>elementFlagMoved</metricenable> + <metricenable>elementListenTracker</metricenable> + </metric> + <interface> + <interfaceoption type="check" name="fragmentMoved"/> + <interfaceoption type="check" name="scalerange" min="25" max="75"/> + <interfaceoption type="show" name='playhead'/> + <interfaceoption type="show" name="page-count"/> + <interfaceoption type="show" name='volume'/> + </interface> + </setup> + <page id='test-0' hostURL="example_eval/" randomiseOrder='true' repeatCount='0' loop='true' showElementComments='true' loudness="-12"> + <commentboxprefix>Comment on fragment</commentboxprefix> + <interface> + <title>Depth</title> + </interface> + <audioelement url="0.wav" id="track-0"/> + <audioelement url="1.wav" id="track-1"/> + <survey location="before"> + <surveyentry type="statement" id="test-0-intro"> + <statement>A two way comparison using randomised element order, automatic loudness and synchronised looping.</statement> + </surveyentry> + </survey> + <survey location="after"> + <surveyentry type="question" id="genre-0" mandatory="true"> + <statement>Please enter the genre.</statement> + </surveyentry> + </survey> + </page> + <page id='test-1' hostURL="example_eval/" randomiseOrder='true' repeatCount='0' loop='false' showElementComments='true' loudness="-12"> + <commentboxprefix>Comment on fragment</commentboxprefix> + <interface> + <title>Depth</title> + </interface> + <audioelement url="0.wav" id="track-2"/> + <audioelement url="1.wav" id="track-3"/> + <survey location="before"/> + <survey location="after"> + <surveyentry type="question" id="genre-1" mandatory="true"> + <statement>Please enter the genre.</statement> + </surveyentry> + </survey> + </page> +</waet> \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/interfaces/ABX.css Wed Mar 16 13:31:42 2016 +0000 @@ -0,0 +1,90 @@ +body { + /* Set the background colour (note US English spelling) to grey*/ + background-color: #fff +} + +div.pageTitle { + width: auto; + height: 20px; + margin-top: 20px; +} + +div.pageTitle span{ + font-size: 1.5em; +} + +div.testHalt { + /* Specify any colouring during the test halt for pre/post questions */ + background-color: rgba(0,0,0,0.5); + /* Don't mess with this bit */ + z-index: 2; + width: 100%; + height: 100%; + position: absolute; + left: 0px; + top: 0px; +} + +button { + /* Specify any button structure or style */ + min-width: 20px; + background-color: #ddd +} + +button.big-button { + width: 250px; + height: 40px; + font-size: 1.2em; +} + +div.comparator-holder { + width: 260px; + height: 300px; + border: black 1px solid; + float: left; + padding-top: 5px; + margin: 25px; +} + +div.comparator-selector { + width: 248px; + height: 250px; + border: black 1px solid; + position: relative; + background-color: #FF0000; + border-radius: 20px; +} + +div.disabled { + background-color: #AAA; +} + +div.selected { + background-color: #008000; +} + +div.comparator-selector span { + font-size: 4em; +} + +button.comparator-button { + width: 250px; + height: 38px; + position: relative; + margin-top: 5px; +} + +div.playhead { + margin: 5px; +} + +div#page-count { + float: left; + margin: 0px 5px; +} + +div#master-volume-holder { + position: absolute; + top: 10px; + left: 120px; +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/interfaces/ABX.js Wed Mar 16 13:31:42 2016 +0000 @@ -0,0 +1,345 @@ +/** + * WAET Blank Template + * Use this to start building your custom interface + */ + +// Once this is loaded and parsed, begin execution +loadInterface(); + +function loadInterface() { + // Use this to do any one-time page / element construction. For instance, placing any stationary text objects, + // holding div's, or setting up any nodes which are present for the entire test sequence + + // Custom comparator Object + Interface.prototype.comparator = null; + + // The injection point into the HTML page + interfaceContext.insertPoint = document.getElementById("topLevelBody"); + var testContent = document.createElement('div'); + testContent.id = 'testContent'; + + // Create the top div for the Title element + var titleAttr = specification.title; + var title = document.createElement('div'); + title.className = "title"; + title.align = "center"; + var titleSpan = document.createElement('span'); + + // Set title to that defined in XML, else set to default + if (titleAttr != undefined) { + titleSpan.textContent = titleAttr; + } else { + titleSpan.textContent = 'Listening test'; + } + // Insert the titleSpan element into the title div element. + title.appendChild(titleSpan); + + var pagetitle = document.createElement('div'); + pagetitle.className = "pageTitle"; + pagetitle.align = "center"; + var titleSpan = document.createElement('span'); + titleSpan.id = "pageTitle"; + pagetitle.appendChild(titleSpan); + + // Create Interface buttons! + var interfaceButtons = document.createElement('div'); + interfaceButtons.id = 'interface-buttons'; + interfaceButtons.style.height = '25px'; + + // Create playback start/stop points + var playback = document.createElement("button"); + playback.innerHTML = 'Stop'; + playback.id = 'playback-button'; + playback.style.float = 'left'; + // onclick function. Check if it is playing or not, call the correct function in the + // audioEngine, change the button text to reflect the next state. + playback.onclick = function() { + if (audioEngineContext.status == 1) { + audioEngineContext.stop(); + this.innerHTML = 'Stop'; + var time = audioEngineContext.timer.getTestTime(); + console.log('Stopped at ' + time); // DEBUG/SAFETY + } + }; + // Append the interface buttons into the interfaceButtons object. + interfaceButtons.appendChild(playback); + + // Global parent for the comment boxes on the page + var feedbackHolder = document.createElement('div'); + feedbackHolder.id = 'feedbackHolder'; + + // Construct the AB Boxes + var boxes = document.createElement('div'); + boxes.align = "center"; + boxes.id = "box-holders"; + boxes.style.float = "left"; + + var submit = document.createElement('button'); + submit.id = "submit"; + submit.onclick = buttonSubmitClick; + submit.className = "big-button"; + submit.textContent = "submit"; + submit.style.position = "relative"; + submit.style.left = (window.innerWidth-250)/2 + 'px'; + + feedbackHolder.appendChild(boxes); + + // Inject into HTML + testContent.appendChild(title); // Insert the title + testContent.appendChild(pagetitle); + testContent.appendChild(interfaceButtons); + testContent.appendChild(feedbackHolder); + testContent.appendChild(submit); + interfaceContext.insertPoint.appendChild(testContent); + + // Load the full interface + testState.initialise(); + testState.advanceState(); +}; + +function loadTest(page) +{ + // Called each time a new test page is to be build. The page specification node is the only item passed in + interfaceContext.comparator = new comparator(page); +} + +function comparator(page) +{ + // Build prototype constructor + this.interfaceObject = function(element,label) + { + // An example node, you can make this however you want for each audioElement. + // However, every audioObject (audioEngineContext.audioObject) MUST have an interface object with the following + // You attach them by calling audioObject.bindInterface( ) + this.parent = element; + this.id = element.id; + this.value = 0; + this.disabled = true; + this.box = document.createElement('div'); + this.box.className = 'comparator-holder'; + this.box.setAttribute('track-id',element.id); + this.box.id = 'comparator-'+label; + this.selector = document.createElement('div'); + this.selector.className = 'comparator-selector disabled'; + var selectorText = document.createElement('span'); + selectorText.textContent = label; + this.selector.appendChild(selectorText); + this.playback = document.createElement('button'); + this.playback.className = 'comparator-button'; + this.playback.disabled = true; + this.playback.textContent = "Listen"; + this.box.appendChild(this.selector); + this.box.appendChild(this.playback); + this.selector.onclick = function(event) + { + var label = event.currentTarget.children[0].textContent; + if (label == "X" || label == "x") {return;} + var time = audioEngineContext.timer.getTestTime(); + if ($(event.currentTarget).hasClass('disabled')) + { + console.log("Please wait until sample has loaded"); + return; + } + if (audioEngineContext.status == 0) + { + alert("Please listen to the samples before making a selection"); + console.log("Please listen to the samples before making a selection"); + return; + } + var id = event.currentTarget.parentElement.getAttribute('track-id'); + interfaceContext.comparator.selected = id; + if ($(event.currentTarget).hasClass("selected")) { + $(".comparator-selector").removeClass('selected'); + for (var i=0; i<interfaceContext.comparator.comparators.length; i++) + { + var obj = interfaceContext.comparator.comparators[i]; + obj.parent.metric.moved(time,0); + } + } else { + $(".comparator-selector").removeClass('selected'); + $(event.currentTarget).addClass('selected'); + for (var i=0; i<interfaceContext.comparator.comparators.length; i++) + { + var obj = interfaceContext.comparator.comparators[i]; + if (i == id) { + obj.value = 1; + } else { + obj.value = 0; + } + obj.parent.metric.moved(time,obj.value); + } + console.log("Selected "+id+' ('+time+')'); + } + }; + this.playback.setAttribute("playstate","ready"); + this.playback.onclick = function(event) + { + var id = event.currentTarget.parentElement.getAttribute('track-id'); + if (event.currentTarget.getAttribute("playstate") == "ready") + { + audioEngineContext.play(id); + } else if (event.currentTarget.getAttribute("playstate") == "playing") { + audioEngineContext.stop(); + } + + }; + this.enable = function() + { + // This is used to tell the interface object that playback of this node is ready + if (this.parent.state == 1) + { + $(this.selector).removeClass('disabled'); + this.playback.disabled = false; + } + }; + this.updateLoading = function(progress) + { + // progress is a value from 0 to 100 indicating the current download state of media files + if (progress != 100) + { + progress = String(progress); + progress = progress.split('.')[0]; + this.playback.textContent = progress+'%'; + } else { + this.playback.textContent = "Play"; + } + }; + this.error = function() { + // audioObject has an error!! + this.playback.textContent = "Error"; + $(this.playback).addClass("error-colour"); + }; + this.startPlayback = function() + { + // Called when playback has begun + $('.comparator-button').text('Listen'); + $(this.playback).text('Stop'); + this.playback.setAttribute("playstate","playing"); + }; + this.stopPlayback = function() + { + // Called when playback has stopped. This gets called even if playback never started! + $(this.playback).text('Listen'); + this.playback.setAttribute("playstate","ready"); + }; + this.getValue = function() + { + // Return the current value of the object. If there is no value, return 0 + return this.value; + }; + this.getPresentedId = function() + { + // Return the presented ID of the object. For instance, the APE has sliders starting from 0. Whilst AB has alphabetical scale + return this.selector.children[0].textContent; + }; + this.canMove = function() + { + // Return either true or false if the interface object can be moved. AB / Reference cannot, whilst sliders can and therefore have a continuous scale. + // These are checked primarily if the interface check option 'fragmentMoved' is enabled. + return false; + }; + this.exportXMLDOM = function(audioObject) { + // Called by the audioObject holding this element to export the interface <value> node. + // If there is no value node (such as outside reference), return null + // 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 + // Use storage.document.createElement('value'); to generate the XML node. + var node = storage.document.createElement('value'); + node.textContent = this.value; + return node; + + }; + this.error = function() { + // If there is an error with the audioObject, this will be called to indicate a failure + } + }; + // Ensure there are only two comparisons per page + if (page.audioElements.length != 2) { + console.error('FATAL - There must be 2 <audioelement> nodes on each <page>: '+page.id); + return; + } + // Build the three audio elements + this.pair = []; + this.X = null; + this.boxHolders = document.getElementById('box-holders'); + for (var index=0; index<page.audioElements.length; index++) { + var element = page.audioElements[index]; + if (element.type != 'normal') + { + console.log("WARNING - ABX can only have normal elements. Page "+page.id+", Element "+element.id); + element.type = "normal"; + } + var audioObject = audioEngineContext.newTrack(element); + var label; + switch(audioObject.specification.parent.label) { + case "none": + label = ""; + break; + case "number": + label = ""+index; + break; + case "letter": + label = String.fromCharCode(97 + index); + break; + default: + label = String.fromCharCode(65 + index); + break; + } + var node = new this.interfaceObject(audioObject,label); + audioObject.bindInterface(node); + this.pair.push(node); + this.boxHolders.appendChild(node.box); + } + var elementId = Math.floor(Math.random() * 2); //Randomly pick A or B to be X + var element = new page.audioElementNode(); + for (var atr in page.audioElements[elementId]) { + eval("element."+atr+" = page.audioElements[elementId]."+atr); + } + element.id += "-X"; + if (typeof element.name == "string") {element.name+="-X";} + page.audioElements.push(element); + // Create the save place-holder for the 'X' element + var root = testState.currentStore.XMLDOM; + var aeNode = storage.document.createElement('audioelement'); + aeNode.setAttribute('ref',element.id); + if (typeof element.name == "string"){aeNode.setAttribute('name',element.name);} + aeNode.setAttribute('type','normal'); + aeNode.setAttribute('url',element.url); + aeNode.setAttribute('gain',element.gain); + aeNode.appendChild(storage.document.createElement('metric')); + root.appendChild(aeNode); + // Build the 'X' element + var audioObject = audioEngineContext.newTrack(element); + var label; + switch(audioObject.specification.parent.label) { + case "letter": + label = "x"; + break; + default: + label = "X"; + break; + } + var node = new this.interfaceObject(audioObject,label); + audioObject.bindInterface(node); + this.X = node; + this.boxHolders.appendChild(node.box); +} + +function resizeWindow(event) +{ + // Called on every window resize event, use this to scale your page properly +} + +function buttonSubmitClick() +{ + testState.advanceState(); +} + +function pageXMLSave(store, pageSpecification) +{ + // MANDATORY + // Saves a specific test page + // You can use this space to add any extra nodes to your XML <audioHolder> saves + // Get the current <page> information in store (remember to appendChild your data to it) + // pageSpecification is the current page node configuration + // To create new XML nodes, use storage.document.createElement(); +} \ No newline at end of file