changeset 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 1cc68e4ce885 (current diff) 51191d791e7e (diff)
children f377350e7259
files
diffstat 5 files changed, 743 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/timeline.css	Thu Aug 04 12:20:54 2016 +0100
@@ -0,0 +1,71 @@
+div.timeline-element {
+    display: flex;
+    justify-content: center;
+}
+
+div.timeline-element-content {
+    max-width: 800px;
+    min-width: 200px;
+    border: 1px solid black;
+    margin: 10px 0px;
+    padding: 20px;
+}
+
+div.timeline-element-canvas-holder {
+    display: flex;
+    width: inherit;
+    height: 160px;
+    margin-left: 50px;
+}
+
+canvas.canvas-layer1{
+    position: absolute;
+}
+canvas.canvas-layer2 {
+    position: absolute;
+    z-index: -1;
+}
+canvas.canvas-layer3 {
+    position: absolute;
+    z-index: -2;
+}
+canvas.canvas-layer4 {
+    position: absolute;
+    z-index: -3;
+}
+
+canvas.timeline-element-canvas {
+    border: 1px solid black;
+}
+
+canvas.canvas-disabled {
+    background-color: gray;
+}
+
+div.timeline-element-comment-holder {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+}
+
+div.comment-entry {
+    border: 1px solid #444444;
+    max-width: 600px;
+    min-width: 400px;
+    margin: 0px 0px 5px 5px;
+    padding: 5px;
+    height: 80px;
+    border-radius: 10px;
+    display: flex;
+    flex-direction: column;
+}
+
+div.comment-entry-header {
+    display: flex;
+    justify-content: space-between;
+}
+
+textarea.comment-entry-text {
+    resize: none;
+    margin: auto;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/interfaces/timeline.js	Thu Aug 04 12:20:54 2016 +0100
@@ -0,0 +1,543 @@
+/**
+ * WAET Timeline
+ * This interface plots a waveform timeline per audio fragment on a page. Clicking on the fragment will generate a comment box for processing.
+ */
+
+// 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
+
+    interfaceContext.insertPoint.innerHTML = ""; // Clear the current schema
+
+    interfaceContext.insertPoint = document.getElementById("topLevelBody");
+    var testContent = document.createElement("div");
+
+    // Create the top div and Title element
+    var title = document.createElement("div");
+    title.className = "title";
+    title.align = "center";
+    var titleSpan = document.createElement("span");
+    titleSpan.id = "test-title";
+    titleSpan.textContent = "Listening Test";
+    title.appendChild(titleSpan);
+
+    var pagetitle = document.createElement("div");
+    pagetitle.className = "pageTitle";
+    pagetitle.align = "center";
+    titleSpan = document.createElement("span");
+    titleSpan.id = "page-title";
+    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.onclick = function () {
+        if (audioEngineContext.status == 1) {
+            audioEngineContext.stop();
+            this.innerHTML = "Stop";
+            var time = audioEngineContext.timer.getTestTime();
+            console.log("Stopped at " + time);
+        }
+    };
+    // Create Submit (save) button
+    var submit = document.createElement("button");
+    submit.innerHTML = 'Next';
+    submit.onclick = buttonSubmitClick;
+    submit.id = 'submit-button';
+    submit.style.float = 'left';
+    // Append the interface buttons into the interfaceButtons object.
+    interfaceButtons.appendChild(playback);
+    interfaceButtons.appendChild(submit);
+
+    // Create outside reference holder
+    var outsideRef = document.createElement("div");
+    outsideRef.id = "outside-reference-holder";
+
+    // Create content point
+    var content = document.createElement("div");
+    content.id = "timeline-test-content";
+
+    //Inject
+    testContent.appendChild(title);
+    testContent.appendChild(pagetitle);
+    testContent.appendChild(interfaceButtons);
+    testContent.appendChild(outsideRef);
+    testContent.appendChild(content);
+    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
+    var content = document.getElementById("timeline-test-content");
+    content.innerHTML = "";
+    var interfaceObj = page.interfaces;
+    if (interfaceObj.length > 1) {
+        console.log("WARNING - This interface only supports one <interface> node per page. Using first interface node");
+    }
+    interfaceObj = interfaceObj[0];
+
+    //Set the page title
+    if (typeof page.title == "string" && page.title.length > 0) {
+        document.getElementById("test-title").textContent = page.title;
+    }
+
+    if (interfaceObj.title != null) {
+        document.getElementById("page-title").textContent = interfaceObj.title;
+    }
+
+    // Delete outside reference
+    var outsideReferenceHolder = document.getElementById("outside-reference-holder");
+    outsideReferenceHolder.innerHTML = "";
+
+    var commentBoxPrefix = "Comment on track";
+    if (interfaceObj.commentBoxPrefix != undefined) {
+        commentBoxPrefix = interfaceObj.commentBoxPrefix;
+    }
+
+    $(page.audioElements).each(function (index, element) {
+        var audioObject = audioEngineContext.newTrack(element);
+        if (page.audioElements.type == 'outside-reference') {
+            var refNode = interfaceContext.outsideReferenceDOM(audioObject, index, outsideReferenceHolder);
+            audioObject.bindInterface(orNode);
+        } else {
+            switch (audioObject.specification.parent.label) {
+                case "none":
+                    label = "";
+                    break;
+                case "letter":
+                    label = String.fromCharCode(97 + index);
+                    break;
+                case "capital":
+                    label = String.fromCharCode(65 + index);
+                    break;
+                default:
+                    label = "" + index;
+                    break;
+            }
+            var node = new interfaceObject(audioObject, label);
+
+            content.appendChild(node.DOM);
+            audioObject.bindInterface(node);
+        }
+    });
+
+    resizeWindow();
+}
+
+function interfaceObject(audioObject, labelstr) {
+    // Each audio object has a waveform guide and self-generated comments
+    this.parent = audioObject;
+    this.DOM = document.createElement("div");
+    this.DOM.className = "timeline-element";
+    this.DOM.id = audioObject.specification.id;
+
+    var root = document.createElement("div");
+    root.className = "timeline-element-content";
+    this.DOM.appendChild(root);
+
+    var label = document.createElement("div");
+    label.style.textAlign = "center";
+    var labelSpan = document.createElement("span");
+    labelSpan.textContent = "Fragment " + labelstr;
+    label.appendChild(labelSpan);
+    root.appendChild(label);
+
+    var canvasHolder = document.createElement("div");
+    canvasHolder.className = "timeline-element-canvas-holder";
+    var buttonHolder = document.createElement("div");
+    buttonHolder.className = "timeline-element-button-holder";
+    var commentHolder = document.createElement("div");
+    commentHolder.className = "timeline-element-comment-holder";
+
+    root.appendChild(canvasHolder);
+    root.appendChild(buttonHolder);
+    root.appendChild(commentHolder);
+
+    this.comments = {
+        parent: this,
+        list: [],
+        Comment: function (parent, time, str) {
+            this.parent = parent;
+            this.time = time;
+            this.DOM = document.createElement("div");
+            this.DOM.className = "comment-entry";
+            var titleHolder = document.createElement("div");
+            titleHolder.className = "comment-entry-header";
+            this.title = document.createElement("span");
+            if (str != undefined) {
+                this.title.textContent = str;
+            } else {
+                this.title.textContent = "Time: " + time.toFixed(2) + "s";
+            }
+            titleHolder.appendChild(this.title);
+            this.textarea = document.createElement("textarea");
+            this.textarea.className = "comment-entry-text";
+            this.DOM.appendChild(titleHolder);
+            this.DOM.appendChild(this.textarea);
+
+            this.clear = {
+                DOM: document.createElement("button"),
+                parent: this,
+                handleEvent: function () {
+                    this.parent.parent.deleteComment(this.parent);
+                }
+            }
+            this.clear.DOM.textContent = "Delete";
+            this.clear.DOM.addEventListener("click", this.clear);
+            titleHolder.appendChild(this.clear.DOM);
+
+            this.resize = function () {
+                var w = window.innerWidth;
+                w = Math.min(w, 800);
+                w = Math.max(w, 200);
+                var elem_w = w / 2.5;
+                elem_w = Math.max(elem_w, 190);
+                this.DOM.style.width = elem_w + "px";
+                this.textarea.style.width = (elem_w - 5) + "px";
+            }
+            this.buildXML = function (root) {
+                //storage.document.createElement();
+                var node = storage.document.createElement("comment");
+                var question = storage.document.createElement("question");
+                var comment = storage.document.createElement("response");
+                node.setAttribute("time", this.time);
+                question.textContent = this.title.textContent;
+                comment.textContent = this.textarea.value;
+                node.appendChild(question);
+                node.appendChild(comment);
+                root.appendChild(node);
+            }
+            this.resize();
+        },
+        newComment: function (time) {
+            var node = new this.Comment(this, time);
+            this.list.push(node);
+            commentHolder.appendChild(node.DOM);
+            return node;
+        },
+        deleteComment: function (comment) {
+            var index = this.list.findIndex(function (element, index, array) {
+                if (element == comment) {
+                    return true;
+                }
+                return false;
+            }, comment);
+            if (index == -1) {
+                return false;
+            }
+            var node = this.list.splice(index, 1);
+            comment.DOM.remove();
+            this.parent.canvas.drawMarkers();
+            return true;
+        },
+        clearList: function () {
+            while (this.list.length > 0) {
+                this.deleteComment(this.list[0]);
+            }
+        }
+    }
+
+    this.canvas = {
+        parent: this,
+        comments: this.comments,
+        layer1: document.createElement("canvas"),
+        layer2: document.createElement("canvas"),
+        layer3: document.createElement("canvas"),
+        layer4: document.createElement("canvas"),
+        resize: function (w) {
+            this.layer1.width = w;
+            this.layer2.width = w;
+            this.layer3.width = w;
+            this.layer4.width = w;
+            this.layer1.style.width = w + "px";
+            this.layer2.style.width = w + "px";
+            this.layer3.style.width = w + "px";
+            this.layer4.style.width = w + "px";
+            this.drawWaveform();
+            this.drawMarkers();
+        },
+        handleEvent: function (event) {
+            switch (event.currentTarget) {
+                case this.layer1:
+                    switch (event.type) {
+                        case "mousemove":
+                            this.drawMouse(event);
+                            break;
+                        case "mouseleave":
+                            this.clearCanvas(this.layer1);
+                            break;
+                        case "click":
+                            var rect = this.layer1.getBoundingClientRect();
+                            var pixX = event.clientX - rect.left;
+                            var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
+                            this.comments.newComment(pixX * tpp);
+                            this.drawMarkers();
+                            break;
+                    }
+                    break;
+            }
+        },
+        drawWaveform: function () {
+            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
+                return;
+            }
+            var buffer = this.parent.parent.buffer.buffer;
+            var context = this.layer4.getContext("2d");
+            context.lineWidth = 1;
+            context.strokeStyle = "#888";
+            context.clearRect(0, 0, this.layer4.width, this.layer4.height);
+            var data = buffer.getChannelData(0);
+            var t_per_pixel = buffer.duration / this.layer4.width;
+            var s_per_pixel = data.length / this.layer4.width;
+            var pixX = 0;
+            while (pixX < this.layer4.width) {
+                var start = Math.floor(s_per_pixel * pixX);
+                var end = Math.min(Math.ceil(s_per_pixel * (pixX + 1)), data.length);
+                var frame = data.subarray(start, end);
+                var min = frame[0];
+                var max = min;
+                for (var n = 0; n < frame.length; n++) {
+                    if (frame[n] < min) {
+                        min = frame[n];
+                    }
+                    if (frame[n] > max) {
+                        max = frame[n];
+                    }
+                }
+                // Assuming min/max normalised between [-1, 1] to map to [150, 0]
+                context.beginPath();
+                context.moveTo(pixX + 0.5, (min + 1) * -75 + 150);
+                context.lineTo(pixX + 0.5, (max + 1) * -75 + 150);
+                context.stroke();
+                pixX++;
+            }
+        },
+        drawMouse: function (event) {
+            var context = this.layer1.getContext("2d");
+            context.clearRect(0, 0, this.layer1.width, this.layer1.height);
+            var rect = this.layer1.getBoundingClientRect();
+            var pixX = event.clientX - rect.left;
+            pixX = Math.floor(pixX) - 0.5;
+            context.strokeStyle = "#800";
+            context.beginPath();
+            context.moveTo(pixX, 0);
+            context.lineTo(pixX, this.layer1.height);
+            context.stroke();
+        },
+        drawTicker: function () {
+            var context = this.layer2.getContext("2d");
+            context.clearRect(0, 0, this.layer2.width, this.layer2.height);
+            var time = this.parent.parent.getCurrentPosition();
+            var ratio = time / this.parent.parent.buffer.buffer.duration;
+            var pixX = Math.floor(ratio * this.layer2.width) + 0.5;
+            context.strokeStyle = "#080";
+            context.beginPath();
+            context.moveTo(pixX, 0);
+            context.lineTo(pixX, this.layer2.height);
+            context.stroke();
+        },
+        drawMarkers: function () {
+            if (this.parent.parent == undefined || this.parent.parent.buffer == undefined) {
+                return;
+            }
+            var context = this.layer3.getContext("2d");
+            context.clearRect(0, 0, this.layer3.width, this.layer3.height);
+            context.strokeStyle = "#008";
+            var tpp = this.parent.parent.buffer.buffer.duration / this.layer1.width;
+            for (var i = 0; i < this.comments.list.length; i++) {
+                var comment = this.comments.list[i];
+                var pixX = Math.floor(comment.time / tpp) + 0.5;
+                context.beginPath();
+                context.moveTo(pixX, 0);
+                context.lineTo(pixX, this.layer3.height);
+                context.stroke();
+            }
+        },
+        clearCanvas: function (canvas) {
+            var context = canvas.getContext("2d");
+            context.clearRect(0, 0, canvas.width, canvas.height);
+        }
+    }
+    this.canvas.layer1.className = "timeline-element-canvas canvas-layer1 canvas-disabled";
+    this.canvas.layer2.className = "timeline-element-canvas canvas-layer2";
+    this.canvas.layer3.className = "timeline-element-canvas canvas-layer3";
+    this.canvas.layer4.className = "timeline-element-canvas canvas-layer3";
+    this.canvas.layer1.height = "150";
+    this.canvas.layer2.height = "150";
+    this.canvas.layer3.height = "150";
+    this.canvas.layer4.height = "150";
+    canvasHolder.appendChild(this.canvas.layer1);
+    canvasHolder.appendChild(this.canvas.layer2);
+    canvasHolder.appendChild(this.canvas.layer3);
+    canvasHolder.appendChild(this.canvas.layer4);
+    this.canvas.layer1.addEventListener("mousemove", this.canvas);
+    this.canvas.layer1.addEventListener("mouseleave", this.canvas);
+    this.canvas.layer1.addEventListener("click", this.canvas);
+
+    var canvasIntervalID = null;
+
+    this.playButton = {
+        parent: this,
+        DOM: document.createElement("button"),
+        handleEvent: function (event) {
+            var id = this.parent.parent.id;
+            var str = this.DOM.textContent;
+            if (str == "Play") {
+                audioEngineContext.play(id);
+            } else if (str == "Stop") {
+                audioEngineContext.stop();
+            }
+        }
+    }
+    this.playButton.DOM.addEventListener("click", this.playButton);
+    this.playButton.DOM.className = "timeline-button timeline-button-disabled";
+    this.playButton.DOM.disabled = true;
+    this.playButton.DOM.textContent = "Wait";
+
+    buttonHolder.appendChild(this.playButton.DOM);
+
+    this.resize = function () {
+        var w = window.innerWidth;
+        w = Math.min(w, 800);
+        w = Math.max(w, 200);
+        root.style.width = w + "px";
+        var c_w = w - 100;
+        this.canvas.resize(c_w);
+    }
+
+    this.enable = function () {
+        // This is used to tell the interface object that playback of this node is ready
+        this.canvas.layer1.addEventListener("click", this.canvas);
+        this.canvas.layer1.className = "timeline-element-canvas canvas-layer1";
+        this.playButton.DOM.className = "timeline-button timeline-button-play";
+        this.playButton.DOM.textContent = "Play";
+        this.playButton.DOM.disabled = false;
+
+        this.canvas.drawWaveform();
+    };
+    this.updateLoading = function (progress) {
+        // progress is a value from 0 to 100 indicating the current download state of media files
+        progress = String(progress);
+        progress = progress.substr(0, 5);
+        this.playButton.DOM.textContent = "Loading: " + progress + '%';
+    };
+    this.startPlayback = function () {
+        // Called when playback has begun
+        canvasIntervalID = window.setInterval(this.canvas.drawTicker.bind(this.canvas), 100);
+        this.playButton.DOM.textContent = "Stop";
+    };
+    this.stopPlayback = function () {
+        // Called when playback has stopped. This gets called even if playback never started!
+        window.clearInterval(canvasIntervalID);
+        this.canvas.clearCanvas(this.canvas.layer2);
+        this.playButton.DOM.textContent = "Play";
+    };
+    this.getValue = function () {
+        // Return the current value of the object. If there is no value, return 0
+        return 0;
+    };
+    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 labelSpan.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.
+        return null;
+    };
+    this.error = function () {
+        // If there is an error with the audioObject, this will be called to indicate a failure
+    }
+};
+
+function resizeWindow(event) {
+    // Called on every window resize event, use this to scale your page properly
+    for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
+        audioEngineContext.audioObjects[i].interfaceDOM.resize();
+    }
+}
+
+function buttonSubmitClick() {
+    if (audioEngineContext.timer.testStarted == false) {
+        interfaceContext.lightbox.post("Warning", 'You have not started the test! Please click play on a sample to begin the test!');
+        return;
+    }
+    var checks = [];
+    checks = checks.concat(testState.currentStateMap.interfaces[0].options);
+    checks = checks.concat(specification.interfaces.options);
+    var canContinue = true;
+    for (var i = 0; i < checks.length; i++) {
+        var checkState = true;
+        if (checks[i].type == 'check') {
+            switch (checks[i].name) {
+                case 'fragmentPlayed':
+                    //Check if all fragments have been played
+                    checkState = interfaceContext.checkAllPlayed();
+                    break;
+                case 'fragmentFullPlayback':
+                    //Check if all fragments have played to their full length
+                    checkState = interfaceContext.checkFragmentsFullyPlayed();
+                    break;
+                case 'fragmentComments':
+                    checkState = interfaceContext.checkAllCommented();
+                    break;
+                default:
+                    console.log("WARNING - Check option " + checks[i].check + " is not supported on this interface");
+                    break;
+            }
+            if (checkState == false) {
+                canContinue == false;
+            }
+        }
+        if (!canContinue) {
+            return;
+        }
+    }
+
+    if (canContinue) {
+        if (audioEngineContext.status == 1) {
+            var playback = document.getElementById('playback-button');
+            playback.click();
+            // This function is called when the submit button is clicked. Will check for any further tests to perform, or any post-test options
+        }
+        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();
+
+    for (var i = 0; i < audioEngineContext.audioObjects.length; i++) {
+        var id = audioEngineContext.audioObjects[i].specification.id;
+        var commentsList = audioEngineContext.audioObjects[i].interfaceDOM.comments.list;
+        var root = audioEngineContext.audioObjects[i].storeDOM;
+        for (var j = 0; j < commentsList.length; j++) {
+            commentsList[j].buildXML(root);
+        }
+    }
+}
--- a/js/core.js	Tue Aug 02 17:21:14 2016 +0100
+++ b/js/core.js	Thu Aug 04 12:20:54 2016 +0100
@@ -419,6 +419,15 @@
 
             document.getElementsByTagName("head")[0].appendChild(css);
             break;
+        case "timeline":
+            interfaceJS.setAttribute("src","interfaces/timeline.js");
+            var css = document.createElement('link');
+            css.rel = 'stylesheet';
+            css.type = 'text/css';
+            css.href = 'interfaces/timeline.css';
+
+            document.getElementsByTagName("head")[0].appendChild(css);
+            break;
 	}
 	document.getElementsByTagName("head")[0].appendChild(interfaceJS);
     
--- a/test_create/interface-specs.xml	Tue Aug 02 17:21:14 2016 +0100
+++ b/test_create/interface-specs.xml	Thu Aug 04 12:20:54 2016 +0100
@@ -205,6 +205,36 @@
                 <outsidereference min="0" max="0"/>
             </elements>
         </interface>
+        <interface name="timeline">
+            <metrics>
+                <entry name="testTimer" support="optional" default="on"/>
+                <entry name="elementTimer" support="optional" default="on"/>
+                <entry name="elementInitialPosition" support="none"/>
+                <entry name="elementTracker" support="none" default="off"/>
+                <entry name="elementFlagListenedTo" support="optional" default="on"/>
+                <entry name="elementFlagMoved" support="none"/>
+                <entry name="elementListenTracker" support="optional" default="on"/>
+            </metrics>
+            <checks>
+                <entry name="fragmentMoved" support="none"/>
+                <entry name="fragmentPlayed" support="optional" default="off"/>
+                <entry name="fragmentFullPlayback" support="optional" default="off"/>
+                <entry name="fragmentComments" support="optional" default="off"/>
+                <entry name="scalerange" support="none"/>
+            </checks>
+            <show>
+                <entry name="volume" support="optional" default="off"/>
+                <entry name="page-count" support="optional" default="off"/>
+                <entry name="playhead" support="none"/>
+                <entry name="comments" support="optional" default="off"/>
+            </show>
+            <elements>
+                <number min="1" max="undefined"/>
+                <anchor min="0" max="0"/>
+                <reference min="0" max="0"/>
+                <outsidereference min="0" max="undefined"/>
+            </elements>
+        </interface>
     </interfaces>
     <scaledefinitions>
         <scale name="(Blank)">
@@ -488,6 +518,15 @@
                 <outsidereference min="0" max="1"/>
             </elements>
         </test>
-        <test name="ABX" interface="ABX"/>
+        <test name="ABX" interface="ABX">
+            <descriptions>
+                <description lang="en">Each page has two audio fragments presented as A and B. The test duplicates one of the fragments and presents it as X. The user must choose which, out of A or B, is closest to X.</description>
+            </descriptions>
+        </test>
+        <test name="timeline" interface="timeline">
+            <descriptions>
+                <description lang="en">Each fragment is displayed with a clickable waveform of itself. The user must click on the waveform at the location that a specific event occured. Users can then enter in information about this event. This test is unit-/value-less.</description>
+            </descriptions>
+        </test>
     </tests>
 </root>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/examples/timeline.xml	Thu Aug 04 12:20:54 2016 +0100
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<waet xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="test-schema.xsd">
+	<setup interface="timeline" projectReturn="save.php">
+		<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="fragmentPlayed"/>
+			<interfaceoption type="check" name="scalerange" min="25" max="75"/>
+            <interfaceoption type="show" name="volume"/>
+			<interfaceoption type="show" name='playhead'/>
+			<interfaceoption type="show" name="page-count"/>
+            <interfaceoption type="show" name="comments"/>
+		</interface>
+	</setup>
+	<page id='test-0' hostURL="media/example/" randomiseOrder='true' repeatCount='4' loop='true' loudness="-23">
+        <title>My Test</title>
+		<interface>
+			<scales>
+				<scalelabel position="0">(1) Very Annoying</scalelabel>
+				<scalelabel position="25">(2) Annoying</scalelabel>
+				<scalelabel position="50">(3) Slightly Annoying</scalelabel>
+				<scalelabel position="75">(4) Audible but not Annoying</scalelabel>
+				<scalelabel position="100">(5) Inaudible</scalelabel>
+			</scales>
+		</interface>
+		<audioelement url="0.wav" id="track-1"/>
+		<audioelement url="1.wav" id="track-2"/>
+        <commentquestion id="preference" type="radio">
+			<statement>Please enter your overall preference</statement>
+			<option name="worst">Very Bad</option>
+			<option name="bad"></option>
+			<option name="OK">OK</option>
+			<option name="Good"></option>
+			<option name="Great">Great</option>
+		</commentquestion>
+		<commentquestion id="character" type="checkbox">
+			<statement>Please describe the overall character</statement>
+			<option name="funky">Funky</option>
+			<option name="mellow">Mellow</option>
+			<option name="laidback">Laid back</option>
+			<option name="heavy">Heavy</option>
+		</commentquestion>
+	</page>
+    <page id='test-1' hostURL="media/example/" randomiseOrder='true' repeatCount='4' loop='true' loudness="-23">
+        <title>My Test</title>
+		<interface>
+			<scales>
+				<scalelabel position="0">(1) Very Annoying</scalelabel>
+				<scalelabel position="25">(2) Annoying</scalelabel>
+				<scalelabel position="50">(3) Slightly Annoying</scalelabel>
+				<scalelabel position="75">(4) Audible but not Annoying</scalelabel>
+				<scalelabel position="100">(5) Inaudible</scalelabel>
+			</scales>
+		</interface>
+		<audioelement url="0.wav" id="track-3"/>
+		<audioelement url="1.wav" id="track-4"/>
+        <commentquestion id="preference1" type="radio">
+			<statement>Please enter your overall preference</statement>
+			<option name="worst">Very Bad</option>
+			<option name="bad"></option>
+			<option name="OK">OK</option>
+			<option name="Good"></option>
+			<option name="Great">Great</option>
+		</commentquestion>
+		<commentquestion id="character1" type="checkbox">
+			<statement>Please describe the overall character</statement>
+			<option name="funky">Funky</option>
+			<option name="mellow">Mellow</option>
+			<option name="laidback">Laid back</option>
+			<option name="heavy">Heavy</option>
+		</commentquestion>
+	</page>
+</waet>