diff --git a/configs/complete.yaml b/configs/complete.yaml index 433682c9..85adf409 100644 --- a/configs/complete.yaml +++ b/configs/complete.yaml @@ -284,6 +284,20 @@ pages: label: Male - value: other label: Other + - type: dropdown + name: referral + label: How have you heard of us? + response: + - value: searchEngine + label: Google/internet search + - value: friends + label: Friends or colleagues + - value: participant + label: Previous experiment + - value: paper + label: Seen in published paper + - value: other + label: Other - type: long_text label: Feedback name: feedback diff --git a/configs/pref.yaml b/configs/pref.yaml new file mode 100644 index 00000000..fa018a4b --- /dev/null +++ b/configs/pref.yaml @@ -0,0 +1,30 @@ +# test config preference test + + +testname: Preference test +testId: pref +bufferSize: 2048 +stopOnErrors: true +showButtonPreviousPage: true +remoteService: service/write.php + +pages: + - type: preference_test + id: trialpref + name: Preference test + content: | +

Which sound do you prefer?

+ considerOrder: true + mustPlayback: ended + stimuli: + C1: configs/resources/audio/mono_c1.wav + C2: configs/resources/audio/mono_c2.wav + C3: configs/resources/audio/mono_c3.wav + + - type: finish + name: Thank you + content: Thank you for attending + showResults: true + writeResults: true + generateSubjectId: true + confirmationCode: your_code_here diff --git a/configs/pref_grouped.yaml b/configs/pref_grouped.yaml new file mode 100644 index 00000000..a715f6c0 --- /dev/null +++ b/configs/pref_grouped.yaml @@ -0,0 +1,28 @@ +# test config preference test + + +testname: Preference test +testId: pref_grouped +bufferSize: 2048 +stopOnErrors: true +showButtonPreviousPage: true +remoteService: service/write.php + +pages: + - type: preference_test + id: trialpref + name: Preference test + content: | +

Which sound do you prefer?

+ considerOrder: true + stimuli: + - C1: configs/resources/audio/mono_c1.wav + C2: configs/resources/audio/mono_c2.wav + - C3: configs/resources/audio/mono_c3.wav + C2: configs/resources/audio/mono_c2.wav + + - type: finish + name: Thank you + content: Thank you for attending + showResults: true + writeResults: true diff --git a/doc/experimenter.md b/doc/experimenter.md index 5206b38b..373daf1c 100644 --- a/doc/experimenter.md +++ b/doc/experimenter.md @@ -105,6 +105,18 @@ A paired comparison page creates a forced or unforced paired comparison (AB/ABX/ * **reference** Filepath to the reference stimulus (WAV file). * **stimuli** A map of stimuli representing three conditions. The key is the name of the condition. The value is the filepath to the stimulus (WAV file). +#### `preference_test` page + +A preference test page creates a forced paired comparison between two stimuli. The main difference to `paired_comparison` is that there is no reference or ground truth. + +* **type** must be preference_test. +* **id** Identifier of the page. +* **name** Name of the page (is shown as title) +* **content** Content (HTML) of the page. The content is shown on the upper part of the page. +* **considerOrder** If set to true, a complete set of pairs is generated from the list of stimuli: A set of `{A, B}` would result in two pairs `(A, B)` and `(B, A)`. Otherwise only the pair `(A, B)` would be generated. +* **mustPlayback** If set to `ended`, the participant must fully play back all stimuli to the end. If set to `processUpdate`, the participant must start playing back all stimuli before responding becomes possible. +* **stimuli** Either a map of stimuli or an array of maps of stimuli. If it is a map, pairs will be generated from the list and then shuffled. If it is an array of maps, pairs will be generated for each map individually and then all the pairs from all maps are shuffled. + #### `likert_multi_stimulus` page A likert multi stimulus page creates a multi-stimulus likert rating. @@ -142,6 +154,8 @@ The finish page must be the last page of the experiment. * **content** Content (HTML) of the page. The content is shown on the upper part of the page. * **showResults** The results are shown to the participant. * **writeResults** The results are sent to the remote service (which writes the results into a file). +* **generateSubjectId** If set to true, a random subject ID is generated and appended to the results. +* **confirmationCode:** If set, this code is displayed after the results were sent. This can be used to confirm that subjects have completed the experiment and facilitates ## Results diff --git a/doc/participant.md b/doc/participant.md index 8b5eb9cf..24316989 100644 --- a/doc/participant.md +++ b/doc/participant.md @@ -41,3 +41,8 @@ * SPACE Play/pause the current selection * a Set begin of loop to current position * b Set end of loop to current position + +### Preference test + +* 1 Play item A +* 2 Play item B \ No newline at end of file diff --git a/index.html b/index.html index dab46eb0..27a000e3 100644 --- a/index.html +++ b/index.html @@ -81,6 +81,7 @@ + @@ -113,6 +114,8 @@ + + diff --git a/lib/webmushra/datamodel/PreferenceTestChoice.js b/lib/webmushra/datamodel/PreferenceTestChoice.js new file mode 100644 index 00000000..8b82b928 --- /dev/null +++ b/lib/webmushra/datamodel/PreferenceTestChoice.js @@ -0,0 +1,14 @@ +/************************************************************************* + (C) Copyright AudioLabs 2017 + +This source code is protected by copyright law and international treaties. This source code is made available to You subject to the terms and conditions of the Software License for the webMUSHRA.js Software. Said terms and conditions have been made available to You prior to Your download of this source code. By downloading this source code You agree to be bound by the above mentionend terms and conditions, which can also be found here: https://www.audiolabs-erlangen.de/resources/webMUSHRA. Any unauthorised use of this source code may result in severe civil and criminal penalties, and will be prosecuted to the maximum extent possible under law. + +**************************************************************************/ + +function PreferenceTestChoice() { + this.optionA = null; + this.optionB = null; + this.answer = null; + this.comment = null; + this.time = null; +} diff --git a/lib/webmushra/nls/nls.js b/lib/webmushra/nls/nls.js index 4eee50a4..64de3208 100644 --- a/lib/webmushra/nls/nls.js +++ b/lib/webmushra/nls/nls.js @@ -100,6 +100,10 @@ nls['es']['slightly'] = "Ligeramente molesto"; nls['es']['annoying'] = "Molesto"; nls['es']['very'] = "Muy molesto"; +// captions Preference Test +nls['en']['pref'] = "Which item do you prefer?"; +nls['de']['pref'] = "Welche Version bevorzugen Sie?"; +nls['fr']['pref'] = "Quel item préférez-vous?"; // captions Paired Comparison AB/ABN nls['en']['quest'] = "Which item is the reference?"; @@ -116,3 +120,9 @@ nls['en']['attending'] = "Thank you for your participation!"; nls['de']['attending'] = "Vielen Dank für die Teilnahme!"; nls['fr']['attending'] = "Merci pour votre participation!"; nls['es']['attending'] = "Gracias por participar!"; +nls['en']['response'] = "Response"; +nls['de']['response'] = "Antwort"; +nls['fr']['response'] = "Réponse"; +nls['en']['code'] = "Your confirmation code:"; +nls['de']['code'] = "Ihr Bestätigungscode:"; +nls['fr']['code'] = "Votre code de confirmation:"; diff --git a/lib/webmushra/pages/FinishPage.js b/lib/webmushra/pages/FinishPage.js index fd1e3ce1..d41412c3 100644 --- a/lib/webmushra/pages/FinishPage.js +++ b/lib/webmushra/pages/FinishPage.js @@ -32,8 +32,21 @@ FinishPage.prototype.getName = function () { return this.pageConfig.name; }; +function getId(){ + /* Returns an ID based on random number and the current timestampe */ + return Math.floor(Math.random() * Date.now()); +} + + FinishPage.prototype.storeParticipantData = function() { var i; + if (this.pageConfig.generateSubjectId){ + this.session.participant.name[this.session.participant.name.length] = "subjectId"; + this.session.participant.response[this.session.participant.response.length] = getId(); + } else { + this.session.participant.name[this.session.participant.name.length] = "subjectId"; + this.session.participant.response[this.session.participant.response.length] = null; + } for (i = 0; i < this.pageConfig.questionnaire.length; ++i) { var element = this.pageConfig.questionnaire[i]; @@ -64,20 +77,33 @@ FinishPage.prototype.render = function (_parent) { for (i = 0; i < this.pageConfig.questionnaire.length; ++i) { var element = this.pageConfig.questionnaire[i]; if (element.type === "text") { - table.append($(""+ element.label +"")); + table.append($(""+ element.label +"")); } else if (element.type === "number") { - table.append($(""+ element.label +"")); + table.append($(""+ element.label +"")); } else if(element.type === "likert") { this.likert = new LikertScale(element.response, element.name + "_"); var td = $(""); table.append($("").append( - $(""+ element.label +""), + $(""+ element.label +""), td )); this.likert.render(td); }else if (element.type === "long_text"){ - table.append($(""+ element.label +"")); - } + table.append($(""+ element.label +"")); + }else if (element.type === "dropdown"){ + var td = $(""); + table.append($("").append( + $(""+ element.label +""), + td)); + dropdown = $(""); + var opt = $(""); + dropdown.append(opt); + for (i = 0; i < element.response.length; ++i) { + opt = $("") + dropdown.append(opt); + } + td.append(dropdown); + } console.log(element); } var button = $(""); @@ -91,6 +117,14 @@ FinishPage.prototype.render = function (_parent) { $("#popHeader").text(this.pageManager.getLocalizer().getFragment(this.language, 'attending')); + if (typeof this.pageConfig.confirmationCode !== "undefined"){ + divCode = $("

" + this.pageManager.getLocalizer().getFragment(this.language, 'code') + "

"); + code = $("

" + this.pageConfig.confirmationCode + "

"); + code.css("font-size", "28px"); + divCode.append(code); + $("#popupResultsContent").append(divCode); + } + var table = $("
"); var trHeader = document.createElement("tr"); var trT; @@ -166,6 +200,38 @@ FinishPage.prototype.render = function (_parent) { trEmpty = $(""); $(table).append(trEmpty); + } else if (trial.type === "preference_test") { + + trPref = document.createElement("tr"); + thT = $(""); + $(thT).append(trial.id + " (Preference Test)" ); + $(trPref).append(thT); + $(table).append(trPref); + trLabel = document.createElement("tr"); + thT = $("AB" + this.pageManager.getLocalizer().getFragment(this.language, 'response') + ""); + $(trLabel).append(thT); + $(table).append(trLabel); + + var j; + for(j = 0; j < trial.responses.length; ++j){ + trPT = document.createElement("tr"); + tdPTOptionA = document.createElement("td"); + tdPTOptionB = document.createElement("td"); + tdPTAnswer = document.createElement("td"); + var response = trial.responses[j]; + + + $(tdPTOptionA).append(response.optionA); + $(tdPTOptionB).append(response.optionB); + $(tdPTAnswer).append(response.answer); + $(trPT).append(tdPTOptionA); + $(trPT).append(tdPTOptionB); + $(trPT).append(tdPTAnswer); + $(table).append(trPT); + } + trEmpty = $(""); + $(table).append(trEmpty); + } else if(trial.type ==="bs1116") { trPaired = document.createElement("tr"); thT = $(""); @@ -287,6 +353,10 @@ FinishPage.prototype.load = function() { if ($("#" + element.name).val() || element.optional == true) { ++counter; } + } else if(element.type === "dropdown") { + if ($("#" + element.name).val() || element.optional == true) { + ++counter; + } } if (counter == this.pageConfig.questionnaire.length) { $('#send_results').removeAttr('disabled'); diff --git a/lib/webmushra/pages/PreferenceTestPage.js b/lib/webmushra/pages/PreferenceTestPage.js new file mode 100644 index 00000000..6af7599b --- /dev/null +++ b/lib/webmushra/pages/PreferenceTestPage.js @@ -0,0 +1,184 @@ +/************************************************************************* + (C) Copyright AudioLabs 2017 + +This source code is protected by copyright law and international treaties. This source code is made available to You subject to the terms and conditions of the Software License for the webMUSHRA.js Software. Said terms and conditions have been made available to You prior to Your download of this source code. By downloading this source code You agree to be bound by the above mentionend terms and conditions, which can also be found here: https://www.audiolabs-erlangen.de/resources/webMUSHRA. Any unauthorised use of this source code may result in severe civil and criminal penalties, and will be prosecuted to the maximum extent possible under law. + +**************************************************************************/ + +function PreferenceTestPage(_pageManager, _pageTemplateRenderer, _audioContext, _bufferSize, _audioFileLoader, _stimuli, _session, _pageConfig, _errorHandler, _language) { + this.pageManager = _pageManager; + this.pageTemplateRenderer = _pageTemplateRenderer; + this.audioContext = _audioContext; + this.bufferSize = _bufferSize; + this.audioFileLoader = _audioFileLoader; + this.session = _session; + this.pageConfig = _pageConfig; + this.errorHandler = _errorHandler; + this.language = _language; + this.div = null; + this.fpc = null; + + this.currentItem = null; + + this.stimuli = _stimuli; + this.played = []; + for (var i = 0; i < this.stimuli.length; ++i) { + this.audioFileLoader.addFile(this.stimuli[i].getFilepath(), (function (_buffer, _stimulus) { _stimulus.setAudioBuffer(_buffer); }), this.stimuli[i]); + this.played.push(false); + } + + this.filePlayer = null; + this.choice = null; + this.time = 0; + this.startTimeOnPage = null; +} + + + +PreferenceTestPage.prototype.getName = function () { + return this.pageConfig.name; +}; + +PreferenceTestPage.prototype.init = function () { + this.filePlayer = new FilePlayer(this.audioContext, this.bufferSize, this.stimuli, this.errorHandler, this.language, this.pageManager.getLocalizer()); + + if (typeof this.pageConfig.mustPlayback !== "undefined") { + this.filePlayer.genericAudioControl.addEventListener((function (_event) { + if (_event.name == this.pageConfig.mustPlayback) { + this.played[_event.index] = true; + if (this.played.every((element) => element === true)){ + $('#radio-choice-a').checkboxradio('enable'); + $('#radio-choice-b').checkboxradio('enable'); + } + } + }).bind(this)); + } +}; + +PreferenceTestPage.prototype.render = function (_parent) { + var div = $("
"); + _parent.append(div); + + var content; + if(this.pageConfig.content === null){ + content =""; + } else { + content = this.pageConfig.content; + } + + var p = $("

" + content + "

"); + div.append(p); + + var outerTable = $("
"); + div.append(outerTable); + var trOuter = $(""); + outerTable.append(trOuter); + var tdLabels = $(""); + trOuter.append(tdLabels); + var innerTable = $("
"); + tdLabels.append(innerTable); + var trA = $(""); + innerTable.append(trA); + trA.append($("A:")); + var trB = $(""); + innerTable.append(trB); + trB.append($("B:")); + trA.height(tdLabels.height()); + trB.height(tdLabels.height()); + var tdPlayer = $(""); + trOuter.append(tdPlayer); + this.filePlayer.render(tdPlayer); + + var table = $("
"); + div.append(table); + + var trAB = $(""); + table.append(trAB); + var tdAB = $(""); + trAB.append(tdAB); + + var tableAB = $("
"); + tdAB.append(tableAB); + + var trPlays = $(""); + tableAB.append(trPlays); + + var trResponse = $(""); + tableAB.append(trResponse); + var tdResponse = $(""); + trResponse.append(tdResponse); + + var radioChoice = $("
\ + \ + \ + \ + \ +
"); + if (typeof this.pageConfig.mustPlayback !== "undefined"){ + radioChoice.find("input[type='radio']").attr("disabled", true);; + } + + radioChoice.find("input[type='radio']").bind("change", (function(){ + this.pageTemplateRenderer.unlockNextButton(); + } + ).bind(this)); + + tdResponse.append(radioChoice); + + this.fpc = new FilePlayerController(this.filePlayer); + this.fpc.bind(); + +}; + +PreferenceTestPage.prototype.load = function () { + this.startTimeOnPage = new Date(); + + if (this.choice === null) { + this.pageTemplateRenderer.lockNextButton(); + } + // audio + this.filePlayer.init(); + + //choice + if(this.choice === 'a') { + $('#radio-choice-a').prop('checked', true).checkboxradio('refresh'); + $('#radio-choice-b').prop('checked', false).checkboxradio('refresh'); + } else if (this.choice === 'b') { + $('#radio-choice-b').prop('checked', true).checkboxradio('refresh'); + $('#radio-choice-a').prop('checked', false).checkboxradio('refresh'); + } +}; + +PreferenceTestPage.prototype.save = function () { + this.fpc.unbind(); + this.time += (new Date() - this.startTimeOnPage); + // audio + this.filePlayer.free(); + // choice + var radio = $('#radio-choice :radio:checked'); + this.choice = (radio.length > 0) ? radio[0].value : null; +}; + + +PreferenceTestPage.prototype.store = function () { + var trial = this.session.getTrial(this.pageConfig.type, this.pageConfig.id); + if (trial === null) { + trial = new Trial(); + trial.type = this.pageConfig.type; + trial.id = this.pageConfig.id; + this.session.trials[this.session.trials.length] = trial; + } + var choice = new PreferenceTestChoice(); + choice.optionA = this.stimuli[0].getId(); + choice.optionB = this.stimuli[1].getId(); + choice.answer = (this.choice == 'a') ? choice.optionA : choice.optionB; + + if (this.choice === null) { + choice.answer = "unknown"; + } else if (this.choice === "n") { + choice.answer = "undecided"; + } + choice.time = this.time; + trial.responses[trial.responses.length] = choice; + +}; diff --git a/lib/webmushra/pages/PreferenceTestPageManager.js b/lib/webmushra/pages/PreferenceTestPageManager.js new file mode 100644 index 00000000..d7866c68 --- /dev/null +++ b/lib/webmushra/pages/PreferenceTestPageManager.js @@ -0,0 +1,64 @@ +/************************************************************************* + (C) Copyright AudioLabs 2017 + +This source code is protected by copyright law and international treaties. This source code is made available to You subject to the terms and conditions of the Software License for the webMUSHRA.js Software. Said terms and conditions have been made available to You prior to Your download of this source code. By downloading this source code You agree to be bound by the above mentionend terms and conditions, which can also be found here: https://www.audiolabs-erlangen.de/resources/webMUSHRA. Any unauthorised use of this source code may result in severe civil and criminal penalties, and will be prosecuted to the maximum extent possible under law. + +**************************************************************************/ + +function PreferenceTestPageManager() { + +} + +PreferenceTestPageManager.prototype.createPages = function (_pageManager, _pageTemplateRenderer, _pageConfig, _audioContext, _bufferSize, _audioFileLoader, _session, _errorHandler, _language) { + + /* Create a set of trials */ + this.stimuli = []; + this.trials = []; + /* If the stimuli are passed in groups, only create pairs from the items within each group */ + if (Array.isArray(_pageConfig.stimuli)){ + for (group of _pageConfig.stimuli){ + this.stimuli = []; + for (var key in group) { + this.stimuli[this.stimuli.length] = new Stimulus(key, group[key]); + } + for (pair of pairs(this.stimuli, _pageConfig.considerOrder)){ + this.trials.push(pair); + } + } + } else { /* Otherwise make pairs of all stimuli */ + for (var key in _pageConfig.stimuli) { + this.stimuli[this.stimuli.length] = new Stimulus(key, _pageConfig.stimuli[key]); + } + for (pair of pairs(this.stimuli, _pageConfig.considerOrder)){ + this.trials.push(pair); + } + } + + shuffle(this.trials); + + for (var i = 0; i < this.trials.length; ++i) { + var page = new PreferenceTestPage(_pageManager, _pageTemplateRenderer, _audioContext, _bufferSize, _audioFileLoader, this.trials[i], + _session, _pageConfig, _errorHandler, _language); + _pageManager.addPage(page); + } +}; + +function* pairs(array, fullSet = true) { +/* + * This generator returns the items from arrays in pairs. + * If fullSet is true, all pairs are returned (i.e. {A, B} and {B, A}). + * If it is false, only the first half of pairs is returned (i.e. only {A, B} and not {B, A}) + */ + if (array.length < 2) { + yield new Set(); + } else { + for (let i = 0; i < array.length; ++i){ + for (let j = fullSet ? 0 : i; j < array.length; ++j){ + if (i == j){continue;} + var pair = [array[i], array[j]]; + yield pair; + } + } + } +} + \ No newline at end of file diff --git a/service/write.php b/service/write.php index 9855cae8..08e21b13 100644 --- a/service/write.php +++ b/service/write.php @@ -137,6 +137,51 @@ function sanitize($string = '', $is_filename = FALSE) fclose($fp); } +// preference test + +$write_pt = false; +$ptCsvData = array(); + +$input = array("session_test_id"); +for($i =0; $i < $length; $i++){ + array_push($input, $session->participant->name[$i]); +} +array_push($input, "trial_id", "choice_option_A", "choice_option_B", "choice_answer", "choice_time", "choice_comment"); +array_push($ptCsvData, $input); + + + +foreach ($session->trials as $trial) { + if ($trial->type == "preference_test") { + foreach ($trial->responses as $response) { + $write_pc = true; + + + $results = array($session->testId); + for($i =0; $i < $length; $i++){ + array_push($results, $session->participant->response[$i]); + } + array_push($results, $trial->id, $response->optionA, $response->optionB, $response->answer, $response->time, $response->comment); + + array_push($ptCsvData, $results); + } + } +} + +if ($write_pc) { + $filename = $filepathPrefix."preference_test".$filepathPostfix; + $isFile = is_file($filename); + $fp = fopen($filename, 'a'); + foreach ($ptCsvData as $row) { + if ($isFile) { + $isFile = false; + } else { + fputcsv($fp, $row); + } + } + fclose($fp); +} + // bs1116 $write_bs1116 = false; diff --git a/startup.js b/startup.js index 6189246b..c44a08f7 100644 --- a/startup.js +++ b/startup.js @@ -91,7 +91,11 @@ function addPagesToPageManager(_pageManager, _pages) { var pcPageManager = new PairedComparisonPageManager(); pcPageManager.createPages(_pageManager, pageTemplateRenderer, pageConfig, audioContext, config.bufferSize, audioFileLoader, session, errorHandler, config.language); pcPageManager = null; - } else if (pageConfig.type == "bs1116") { + } else if (pageConfig.type == "preference_test") { + var prefPageManager = new PreferenceTestPageManager(); + prefPageManager.createPages(_pageManager, pageTemplateRenderer, pageConfig, audioContext, config.bufferSize, audioFileLoader, session, errorHandler, config.language); + prefPageManager = null; + }else if (pageConfig.type == "bs1116") { var bs1116PageManager = new BS1116PageManager(); bs1116PageManager.createPages(_pageManager, pageTemplateRenderer, pageConfig, audioContext, config.bufferSize, audioFileLoader, session, errorHandler, config.language); bs1116PageManager = null;