# HG changeset patch # User Nicholas Jillings # Date 1478699316 0 # Node ID c0854362d09da5ef13d4323dbcd69543b9a83c01 # Parent 278e6e54703bd60e14bcbc4f6ba37f386e378e67# Parent 21110fddb0af7d7ea59c59fe6cd69bdd8b483a85 Merge branch 'master' into vnext diff -r 278e6e54703b -r c0854362d09d js/core.js --- a/js/core.js Wed Nov 09 13:22:50 2016 +0000 +++ b/js/core.js Wed Nov 09 13:48:36 2016 +0000 @@ -1611,22 +1611,21 @@ } else { interfaceContext.playhead.setTimePerPixel(this.audioObjects[id]); } + var setTime = audioContext.currentTime; if (this.synchPlayback && this.loopPlayback) { // Traditional looped playback - var setTime = audioContext.currentTime + specification.crossFade; for (var i = 0; i < this.audioObjects.length; i++) { this.audioObjects[i].play(audioContext.currentTime); if (id == i) { this.audioObjects[i].loopStart(setTime); } else { - this.audioObjects[i].loopStop(setTime); + this.audioObjects[i].loopStop(setTime + specification.crossFade); } } } else { - var setTime = audioContext.currentTime + specification.crossFade; for (var i = 0; i < this.audioObjects.length; i++) { if (i != id) { - this.audioObjects[i].stop(setTime); + this.audioObjects[i].stop(setTime + specification.crossFade); } else if (i == id) { this.audioObjects[id].play(setTime); } @@ -1871,10 +1870,10 @@ this.outputGain.gain.cancelScheduledValues(audioContext.currentTime); if (!audioEngineContext.loopPlayback || !audioEngineContext.synchPlayback) { this.metric.startListening(audioEngineContext.timer.getTestTime()); - this.outputGain.gain.setValueAtTime(this.onplayGain, startTime); + this.outputGain.gain.linearRampToValueAtTime(this.onplayGain, startTime + specification.crossFade); this.interfaceDOM.startPlayback(); } else { - this.outputGain.gain.setValueAtTime(0.0, startTime); + this.outputGain.gain.linearRampToValueAtTime(0.0, startTime); } if (audioEngineContext.loopPlayback) { this.bufferNode.loopStart = this.specification.startTime || 0; @@ -1894,7 +1893,7 @@ this.bufferNode.stop(stopTime); this.bufferNode = undefined; } - this.outputGain.gain.setValueAtTime(0.0, stopTime); + this.outputGain.gain.linearRampToValueAtTime(0.0, stopTime); this.interfaceDOM.stopPlayback(); }; diff -r 278e6e54703b -r c0854362d09d python/generate_report.py --- a/python/generate_report.py Wed Nov 09 13:22:50 2016 +0000 +++ b/python/generate_report.py Wed Nov 09 13:48:36 2016 +0000 @@ -64,6 +64,10 @@ gender = [] age = [] +# diagnostics +browser = [] +platform = [] + # get username if available for name in ('LOGNAME', 'USER', 'LNAME', 'USERNAME'): user = os.environ.get(name) @@ -162,6 +166,20 @@ individual_table = '\n' # table with stats for this individual test file timeline_plots = '' # plots of timeline (movements and plays) + # diagnostics: browser + vendor = root.find("./navigator/vendor") + if vendor is not None and vendor.text is not None: + browser.append(vendor.text.replace(',','')) + else: + browser.append('UNAVAILABLE') + + # diagnostics: platform + platform_tag = root.find("./navigator/platform") + if platform_tag is not None and platform_tag.text is not None: + platform.append(platform_tag.text.replace('_','\_')) + else: + platform_tag.append('UNAVAILABLE') + # DEMO survey stats # get gender post_survey = root.find("./survey/[@location='post']") @@ -535,6 +553,104 @@ ''' # problem: some people entered twice? + +# pie chart of browser usage +browsers = ['Google Inc.', 'Apple Computer Inc.', 'UNAVAILABLE'] +# TODO: get the above automatically +browser_distribution = '' +for item in browsers: + number = browser.count(item) + if number>0: + browser_distribution += str("{:.2f}".format((100.0*number)/len(browser)))+\ + '/'+item.capitalize()+' ('+str(number)+'),\n' + +body += r''' + % Pie chart of browser distribution + \def\angle{0} + \def\radius{3} + \def\cyclelist{{"orange","blue","red","green"}} + \newcount\cyclecount \cyclecount=-1 + \newcount\ind \ind=-1 + \begin{figure}[htbp] + \begin{center}\begin{tikzpicture}[nodes = {font=\sffamily}] + \foreach \percent/\name in {'''+\ + browser_distribution+\ + r'''} {\ifx\percent\empty\else % If \percent is empty, do nothing + \global\advance\cyclecount by 1 % Advance cyclecount + \global\advance\ind by 1 % Advance list index + \ifnum6<\cyclecount % If cyclecount is larger than list + \global\cyclecount=0 % reset cyclecount and + \global\ind=0 % reset list index + \fi + \pgfmathparse{\cyclelist[\the\ind]} % Get color from cycle list + \edef\color{\pgfmathresult} % and store as \color + % Draw angle and set labels + \draw[fill={\color!50},draw={\color}] (0,0) -- (\angle:\radius) + arc (\angle:\angle+\percent*3.6:\radius) -- cycle; + \node at (\angle+0.5*\percent*3.6:0.7*\radius) {\percent\,\%}; + \node[pin=\angle+0.5*\percent*3.6:\name] + at (\angle+0.5*\percent*3.6:\radius) {}; + \pgfmathparse{\angle+\percent*3.6} % Advance angle + \xdef\angle{\pgfmathresult} % and store in \angle + \fi + }; + \end{tikzpicture} + \caption{Representation of browsers across subjects} + \label{default} + \end{center} + \end{figure} + + ''' + +# pie chart of platform usage +platforms = ['Win32', 'Win64', 'MacIntel', 'Linux i686', 'Linux x86\_64', 'UNAVAILABLE'] +# TODO: get the above automatically # order alphabetically +platform_distribution = '' +for item in platforms: + number = platform.count(item) + if number>0: + platform_distribution += str("{:.2f}".format((100.0*number)/len(platform)))+\ + '/'+item.capitalize()+' ('+str(number)+'),\n' + +body += r''' + % Pie chart of browser distribution + \def\angle{0} + \def\radius{3} + \def\cyclelist{{"orange","blue","red","green","cyan"}} + \newcount\cyclecount \cyclecount=-1 + \newcount\ind \ind=-1 + \begin{figure}[htbp] + \begin{center}\begin{tikzpicture}[nodes = {font=\sffamily}] + \foreach \percent/\name in {'''+\ + platform_distribution+\ + r'''} {\ifx\percent\empty\else % If \percent is empty, do nothing + \global\advance\cyclecount by 1 % Advance cyclecount + \global\advance\ind by 1 % Advance list index + \ifnum6<\cyclecount % If cyclecount is larger than list + \global\cyclecount=0 % reset cyclecount and + \global\ind=0 % reset list index + \fi + \pgfmathparse{\cyclelist[\the\ind]} % Get color from cycle list + \edef\color{\pgfmathresult} % and store as \color + % Draw angle and set labels + \draw[fill={\color!50},draw={\color}] (0,0) -- (\angle:\radius) + arc (\angle:\angle+\percent*3.6:\radius) -- cycle; + \node at (\angle+0.5*\percent*3.6:0.7*\radius) {\percent\,\%}; + \node[pin=\angle+0.5*\percent*3.6:\name] + at (\angle+0.5*\percent*3.6:\radius) {}; + \pgfmathparse{\angle+\percent*3.6} % Advance angle + \xdef\angle{\pgfmathresult} % and store in \angle + \fi + }; + \end{tikzpicture} + \caption{Representation of platforms across subjects} + \label{default} + \end{center} + \end{figure} + + ''' + + #TODO # time per page in function of number of fragments (plot) # time per participant in function of number of pages diff -r 278e6e54703b -r c0854362d09d python/score_parser.py --- a/python/score_parser.py Wed Nov 09 13:22:50 2016 +0000 +++ b/python/score_parser.py Wed Nov 09 13:48:36 2016 +0000 @@ -26,26 +26,28 @@ elif not os.access(os.path.dirname(folder_name), os.W_OK): #the file does exist but write privileges are not given print("No write privileges in folder '"+folder_name+"'.") + - # CODE -# remember which files have been opened this time -file_history = [] +storage = {} -# get every XML file in folder +# create folder 'ratings' if not yet created +if not os.path.exists(folder_name + '/ratings'): + os.makedirs(folder_name + '/ratings') + +# Get every XML file in the folder for file_name in os.listdir(folder_name): - if file_name.endswith(".xml"): + if (file_name.endswith(".xml")): tree = ET.parse(folder_name + '/' + file_name) root = tree.getroot() - - # get subject ID from XML file - subject_id = file_name[:-4] # file name (without extension) as subject ID - - # get list of all pages this subject evaluated - for page in root.findall("./page"): # iterate over pages - page_name = page.get('ref') # get page reference ID - + + subject_id = root.get('key'); + + # get the list of the pages this subject evaluated + for page in root.findall("./page"): # iterate over pages + page_name = page.get('ref') # get page ID + if page_name is None: # ignore 'empty' audio_holders print("WARNING: " + file_name + " contains empty audio holder. (score_parser.py)") break @@ -53,84 +55,64 @@ if page.get('state') != "complete": print("WARNING: " + file_name + " contains incomplete page " +page_name+ ". (score_parser.py)") break; - - file_name = folder_name+'/ratings/'+page_name+'-ratings.csv' # score file name - - # create folder 'ratings' if not yet created - if not os.path.exists(folder_name + '/ratings'): - os.makedirs(folder_name + '/ratings') - + + # Check if page in the store + if storage.get(page_name) == None: + storage[page_name] = {'header':[], 'axis':{}} # add to the store + + # Get the axis names + pageConfig = root.find('./waet/page/[@id="'+page_name+'"]') + for interface in pageConfig.findall('./interface'): # Get the noeds + interfaceName = interface.get("name"); # Get the axis name + if interfaceName == None: + interfaceName = "default" # If name not set, make name 'default' + if storage[page_name]['axis'].get(interfaceName) == None: + storage[page_name]['axis'][interfaceName] = {} # If not in store for page, add empty dict + storage[page_name]['axis'][interfaceName][subject_id] = [] # Add the store for the session + # header: fragment IDs in 'alphabetical' order # go to fragment column, or create new column if it doesn't exist yet - - # get array of audio elements and number of audio elements - audiolist = page.findall("./audioelement") - n_fragments = len(audiolist) - + # get alphabetical array of fragment IDs from this subject's XML fragmentnamelist = [] # make empty list - for audioelement in audiolist: # iterate over all audioelements + for audioelement in page.findall("./audioelement"): # iterate over all audioelements fragmentnamelist.append(audioelement.get('ref')) # add to list + + fragmentnamelist = sorted(fragmentnamelist); # Sort the list + storage[page_name]['header'] = fragmentnamelist; + + for fragmentname in fragmentnamelist: + audioElement = page.find("./audioelement/[@ref='"+ fragmentname+ "']") # Get the element + for value in audioElement.findall('./value'): + axisName = value.get('interface-name') + if axisName == None: + axisName = 'default' + axisStore = storage[page_name]['axis'][axisName] + if hasattr(value, 'text'): + axisStore[subject_id].append(value.text) + else: + axisStore[subject_id].append('') - - # if file exists, get header and add any 'new' fragments not yet in the header - if os.path.isfile(file_name): - with open(file_name, 'r') as readfile: - filereader = csv.reader(readfile, delimiter=',') - headerrow = next(filereader) - - # If file hasn't been opened yet this time, remove all rows except header - if file_name not in file_history: - with open(file_name, 'w') as writefile: - filewriter = csv.writer(writefile, delimiter=',') - headerrow = sorted(headerrow) - filewriter.writerow(headerrow) - file_history.append(file_name) - - # Which of the fragments are in fragmentnamelist but not in headerrow? - newfragments = list(set(fragmentnamelist)-set(headerrow)) - newfragments = sorted(newfragments) # new fragments in alphabetical order - # If not empty, read file and rewrite adding extra columns - if newfragments: # if not empty - with open('temp.csv', 'w') as writefile: - filewriter = csv.writer(writefile, delimiter=',') - filewriter.writerow(headerrow + newfragments) # write new header - with open(file_name, 'r') as readfile: - filereader = csv.reader(readfile, delimiter=',') - next(filereader) # skip header - for row in filereader: # rewrite row plus empty cells for every new fragment name - filewriter.writerow(row + ['']*len(newfragments)) - os.rename('temp.csv', file_name) # replace old file with temp file - headerrow = headerrow + newfragments - - - # if file does not exist yet, create file and make header - else: - headerrow = sorted(fragmentnamelist) # sort alphabetically - headerrow.insert(0,'') - fragmentnamelist = fragmentnamelist[1:] #HACKY FIX inserting in firstrow also affects fragmentnamelist - with open(file_name, 'w') as writefile: - filewriter = csv.writer(writefile, delimiter=',') - filewriter.writerow(headerrow) - file_history.append(file_name) - - # open file to write for this page - writefile = open(file_name, 'a') +# Now create the individual files +for page_name in storage: + for axis_name in storage[page_name]['axis']: + + file_name = folder_name+'/ratings/'+page_name+'-'+axis_name+'-ratings.csv' # score file name + + # I'm not as elegant, I say burn the files and start again + headerrow = list(storage[page_name]['header']) # Extract the element IDs + headerrow.insert(0,'file_keys') + with open(file_name, 'w') as writefile: filewriter = csv.writer(writefile, delimiter=',') - - # prepare row to be written for this subject for this page - ratingrow = [subject_id] - - # get scores related to fragment [id] - for fragmentname in headerrow[1:]: # iterate over fragments in header (skip first empty column) - elementvalue = page.find("./audioelement/[@ref='" - + fragmentname - + "']/value") - if hasattr(elementvalue, 'text'): # if rating for this fragment exists - ratingrow.append(elementvalue.text) # add to rating row - else: # if this subject has not rated this fragment - ratingrow.append('') # append empty cell - - # write row: [subject ID, rating fragment ID 1, ..., rating fragment ID M] - if any(ratingrow[1:]): # append to file if row non-empty (except subject name) - filewriter.writerow(ratingrow) + filewriter.writerow(headerrow) + + # open file to write the page + writefile = open(file_name, 'a') + filewriter = csv.writer(writefile, delimiter=',') + + for subject_id in storage[page_name]['axis'][axis_name]: + entry = [subject_id] + for value in storage[page_name]['axis'][axis_name][subject_id]: + entry.append(value) + filewriter.writerow(entry) + writefile.close() \ No newline at end of file diff -r 278e6e54703b -r c0854362d09d tests/examples/AB_example.xml --- a/tests/examples/AB_example.xml Wed Nov 09 13:22:50 2016 +0000 +++ b/tests/examples/AB_example.xml Wed Nov 09 13:48:36 2016 +0000 @@ -14,7 +14,7 @@ - This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'. + This is an example of an 'AB'-style test, with two pages, using the test stimuli in 'example_eval/'. The 'playOne' configuration option means a fragment has to be finished playing before another fragment can be auditioned. @@ -53,7 +53,7 @@ - + Comment on fragment Depth diff -r 278e6e54703b -r c0854362d09d tests/examples/APE_example.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/examples/APE_example.xml Wed Nov 09 13:48:36 2016 +0000 @@ -0,0 +1,143 @@ + + + + + + Please enter your name. + + + + Please select with which activities you have any experience (example checkbox question) + + + + + + + + What instrument did you play + + + This is an example of an 'APE'-style test, with two pages, using the test stimuli in 'example_eval/'. + + + + + Please enter your location. (example mandatory text question) + + + Please enter your age (example non-mandatory number question) + + + Please rate this interface (example radio button question) + + + + + + + Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder. + + + + testTimer + elementTimer + elementInitialPosition + elementTracker + elementFlagListenedTo + elementFlagMoved + elementListenTracker + + + + + + + + + + + + Comment on fragment + + Preference + + Min + Max + Middle + 20 + + + + Depth + + Low + High + Middle + Middle + + + + + + + + + + Example of an 'APE' style interface with hidden anchor 'zero' (which needs to be below 20%), looping of the samples, randomisation of marker labels, mandatory moving of every sample, and a forced scale usage of at least 25%-75%. + + + + + Please enter the genre. + + + + + Comment on fragment + + Example Test Question + + Min + Max + Middle + 20 + + + + + + + + + + + What is your general experience with numbers? + + + Please enter your overall preference + + + + + + + + Please describe the overall character + + + + + + + + Example of an 'APE' style interface with hidden anchor 'zero' (which needs to be below 20%), looping of the samples, randomisation of marker labels, mandatory moving of every sample, and a forced scale usage of at least 25%-75%. + + + + + Please enter the genre. + + + + \ No newline at end of file diff -r 278e6e54703b -r c0854362d09d tests/examples/mushra_example.xml --- a/tests/examples/mushra_example.xml Wed Nov 09 13:22:50 2016 +0000 +++ b/tests/examples/mushra_example.xml Wed Nov 09 13:48:36 2016 +0000 @@ -94,13 +94,13 @@ 20 - + - + What is your general experience with numbers? diff -r 278e6e54703b -r c0854362d09d tests/examples/project.xml --- a/tests/examples/project.xml Wed Nov 09 13:22:50 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ - - - - - - Please enter your name. - - - - Please select with which activities you have any experience (example checkbox question) - - - - - - - - What instrument did you play - - - This is an example of an 'APE'-style test, with two pages, using the test stimuli in 'example_eval/'. - - - - - Please enter your location. (example mandatory text question) - - - Please enter your age (example non-mandatory number question) - - - Please rate this interface (example radio button question) - - - - - - - Thank you for taking this listening test. Please click 'submit' and your results will appear in the 'saves/' folder. - - - - testTimer - elementTimer - elementInitialPosition - elementTracker - elementFlagListenedTo - elementFlagMoved - elementListenTracker - - - - - - - - - - - - Comment on fragment - - Preference - - Min - Max - Middle - 20 - - - - Depth - - Low - High - Middle - Middle - - - - - - - - - - Example of an 'APE' style interface with hidden anchor 'zero' (which needs to be below 20%), looping of the samples, randomisation of marker labels, mandatory moving of every sample, and a forced scale usage of at least 25%-75%. - - - - - Please enter the genre. - - - - - Comment on fragment - - Example Test Question - - Min - Max - Middle - 20 - - - - - - - - - - - What is your general experience with numbers? - - - Please enter your overall preference - - - - - - - - Please describe the overall character - - - - - - - - Example of an 'APE' style interface with hidden anchor 'zero' (which needs to be below 20%), looping of the samples, randomisation of marker labels, mandatory moving of every sample, and a forced scale usage of at least 25%-75%. - - - - - Please enter the genre. - - - - \ No newline at end of file