Compare commits
3 Commits
jspsych@6.
...
23ec2b02b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23ec2b02b9 | ||
|
|
2a130f79b8 | ||
|
|
1cd4dd8a95 |
@@ -11,8 +11,12 @@ We've implemented some generic methods for audio testing with jsPsych:
|
||||
|
||||
* A plugin to do alternative forced choice with sounds: [jspsych-audio-sequence-button-response](docs/jspsych-audio-sequence-button-response.md).
|
||||
* A plugin to do extend `audio-keyboard-response`: [jspsych-audio-keyboard-response-wait](docs/jspsych-audio-keyboard-response-wait.md).
|
||||
* A plugin to do extend `audio-keyboard-response`: [jspsych-audio-keyboard-response-clickable](plugins/jspsych-audio-keyboard-response-clickable.js).
|
||||
* A plugin to display a Coordinate Response Measure interface: [jspsych-crm](docs/jspsych-crm.md).
|
||||
* A plugin to do display a loading spinner while waiting for a (possible async) function to complete: [jspsych-waitfor-function](docs/jspsych-waitfor-function.md).
|
||||
* A plugin where html can be clickable: [jspsych-html-keyboard-response-clickable](plugins/jspsych-html-keyboard-response-clickable.js).
|
||||
* A plugin where images can be clickable: [jspsych-image-keyboard-response-clickable](plugins/jspsych-image-keyboard-response-clickable.js).
|
||||
* A plugin to deal with Safari's quirky behaviour: [jspsych-audio-safari-init](plugins/jspsych-audio-safari-init.js).
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ The trial can end when the subject responds, when the audio file has finished pl
|
||||
|
||||
Note that the buttons are disabled during playing so the subject cannot press any button during that time.
|
||||
|
||||
Make sure to define a CSS style for `.jspsych-audio-sequence-button-response button.highlighted` to see the button light up, and you may have to use `!important` for it to show up.
|
||||
|
||||
## Parameters
|
||||
|
||||
Parameters with a default value of *undefined* must be specified. Other parameters can be left unspecified if the default value is acceptable.
|
||||
@@ -72,6 +74,18 @@ The animation of the visual feedback can make use of [Semantic UI's transitions]
|
||||
|
||||
#### Three alternative forced choice (3AFC)
|
||||
|
||||
CSS:
|
||||
```css
|
||||
.jspsych-audio-sequence-button-response button {
|
||||
min-width: 5em;
|
||||
min-height: 4em;
|
||||
}
|
||||
.jspsych-audio-sequence-button-response button.highlighted {
|
||||
background-color: #ffff00 !important;
|
||||
}
|
||||
```
|
||||
|
||||
Javascript:
|
||||
```javascript
|
||||
var trial = {
|
||||
type: 'audio-button-response',
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* jspsych-audio-sequence-button-response
|
||||
* Etienne Gaudrain <etienne.gaudrain@cnrs.fr>
|
||||
* jspsych-audio-sequence-button-response for jsPsych v6.3
|
||||
* Etienne Gaudrain <etienne.gaudrain@cnrs.fr> 2021-10-15
|
||||
*
|
||||
* Plugin for playing a sequence of audio files and getting an HTML button response
|
||||
*
|
||||
* Based on jspsych-audio-button-response.
|
||||
*
|
||||
* 2022-03-19: Fixed bug that ISI was applied also to last item.
|
||||
**/
|
||||
|
||||
jsPsych.plugins["audio-sequence-button-response"] = (function() {
|
||||
@@ -103,11 +105,7 @@ jsPsych.plugins["audio-sequence-button-response"] = (function() {
|
||||
plugin.trial = function(display_element, trial) {
|
||||
|
||||
var context = jsPsych.pluginAPI.audioContext();
|
||||
if(context !== null) {
|
||||
var source;
|
||||
} else {
|
||||
var audio;
|
||||
}
|
||||
var audio;
|
||||
|
||||
if(trial.visual_feedback===true && trial.i_correct===null)
|
||||
throw "'i_correct' has to be defined if visual feedback is requested.";
|
||||
@@ -125,12 +123,6 @@ jsPsych.plugins["audio-sequence-button-response"] = (function() {
|
||||
play_next_audio.i = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
// We un-highlight the previous button
|
||||
if(play_next_audio.i>0)
|
||||
$(display_element).find('#jspsych-audio-sequence-button-response-' + (play_next_audio.i-1) +' button').toggleClass('highlighted');
|
||||
*/
|
||||
|
||||
// Is it the last stimulus, do we need to end trial?
|
||||
if(play_next_audio.i >= trial.stimuli.length) {
|
||||
$(display_element).find(".jspsych-audio-sequence-button-response button").removeClass("disabled").prop('disabled', false);
|
||||
@@ -141,34 +133,37 @@ jsPsych.plugins["audio-sequence-button-response"] = (function() {
|
||||
}
|
||||
|
||||
// Prepare the next sound to play
|
||||
if(context !== null) {
|
||||
source = context.createBufferSource();
|
||||
source.buffer = jsPsych.pluginAPI.getAudioBuffer(trial.stimuli[play_next_audio.i]);
|
||||
source.connect(context.destination);
|
||||
source.onended = function(){
|
||||
jsPsych.pluginAPI.getAudioBuffer(trial.stimuli[play_next_audio.i]).then(function(buffer){
|
||||
if(context !== null) {
|
||||
audio = context.createBufferSource();
|
||||
audio.buffer = buffer;
|
||||
audio.connect(context.destination);
|
||||
} else {
|
||||
audio = buffer;
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
audio.addEventListener('ended', function _audio_ended(){
|
||||
$(display_element).find('.jspsych-audio-sequence-button-response button.highlighted').removeClass('highlighted');
|
||||
setTimeout(play_next_audio, trial.isi);
|
||||
};
|
||||
} else {
|
||||
audio = jsPsych.pluginAPI.getAudioBuffer(trial.stimuli[play_next_audio.i]);
|
||||
audio.currentTime = 0;
|
||||
audio.addEventListener('ended', function(){
|
||||
$(display_element).find('.jspsych-audio-sequence-button-response button.highlighted').removeClass('highlighted');
|
||||
setTimeout(play_next_audio, trial.isi);
|
||||
if(play_next_audio.i<trial.stimuli.length){
|
||||
setTimeout(play_next_audio, trial.isi);
|
||||
} else {
|
||||
setTimeout(play_next_audio, 0);
|
||||
}
|
||||
audio.removeEventListener('ended', _audio_ended);
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight the current button
|
||||
$(display_element).find('#jspsych-audio-sequence-button-response-' + play_next_audio.i +' button').addClass('highlighted');
|
||||
// Highlight the current button
|
||||
$(display_element).find('#jspsych-audio-sequence-button-response-' + play_next_audio.i +' button').addClass('highlighted');
|
||||
|
||||
if(context !== null) {
|
||||
startTime = context.currentTime;
|
||||
source.start(startTime);
|
||||
} else {
|
||||
audio.play();
|
||||
}
|
||||
if(context !== null) {
|
||||
startTime = context.currentTime;
|
||||
audio.start(startTime);
|
||||
} else {
|
||||
audio.play();
|
||||
}
|
||||
|
||||
play_next_audio.i++;
|
||||
play_next_audio.i++;
|
||||
});
|
||||
}
|
||||
|
||||
//display buttons
|
||||
@@ -288,12 +283,11 @@ jsPsych.plugins["audio-sequence-button-response"] = (function() {
|
||||
// stop the audio file if it is playing
|
||||
// remove end event listeners if they exist
|
||||
if(context !== null) {
|
||||
source.stop();
|
||||
source.onended = function() {}
|
||||
audio.stop();
|
||||
} else {
|
||||
audio.pause();
|
||||
audio.removeEventListener('ended', end_trial);
|
||||
}
|
||||
audio.removeEventListener('ended', end_trial);
|
||||
|
||||
// kill any remaining setTimeout handlers
|
||||
jsPsych.pluginAPI.clearAllTimeouts();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Etienne Gaudrain
|
||||
*
|
||||
* Based on:
|
||||
* jspsych-image-keyboard-response @6.1.0
|
||||
* jspsych-image-keyboard-response @6.2.0
|
||||
* Josh de Leeuw
|
||||
*
|
||||
* plugin for displaying a stimulus and getting a keyboard response or click on
|
||||
@@ -21,7 +21,7 @@ jsPsych.plugins["image-keyboard-response-clickable"] = (function() {
|
||||
jsPsych.pluginAPI.registerPreload('image-keyboard-response-clickable', 'stimulus', 'image');
|
||||
|
||||
plugin.info = {
|
||||
name: 'image-keyboard-response',
|
||||
name: 'image-keyboard-response-clickable',
|
||||
description: '',
|
||||
parameters: {
|
||||
stimulus: {
|
||||
@@ -85,37 +85,102 @@ jsPsych.plugins["image-keyboard-response-clickable"] = (function() {
|
||||
default: true,
|
||||
description: 'If true, trial will end when subject makes a response.'
|
||||
},
|
||||
render_on_canvas: {
|
||||
type: jsPsych.plugins.parameterType.BOOL,
|
||||
pretty_name: 'Render on canvas',
|
||||
default: true,
|
||||
description: 'If true, the image will be drawn onto a canvas element (prevents blank screen between consecutive images in some browsers).' +
|
||||
'If false, the image will be shown via an img element.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugin.trial = function(display_element, trial) {
|
||||
|
||||
var start_time;
|
||||
|
||||
// display stimulus
|
||||
var html = '<img src="' + trial.stimulus + '" id="jspsych-image-keyboard-response-stimulus" style="';
|
||||
if(trial.stimulus_height !== null) {
|
||||
html += 'height:' + trial.stimulus_height + 'px; '
|
||||
if(trial.stimulus_width == null && trial.maintain_aspect_ratio) {
|
||||
html += 'width: auto; ';
|
||||
var height, width, start_time;
|
||||
if(trial.render_on_canvas) {
|
||||
// first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML)
|
||||
if(display_element.hasChildNodes()) {
|
||||
// can't loop through child list because the list will be modified by .removeChild()
|
||||
while(display_element.firstChild) {
|
||||
display_element.removeChild(display_element.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(trial.stimulus_width !== null) {
|
||||
html += 'width:' + trial.stimulus_width + 'px; '
|
||||
if(trial.stimulus_height == null && trial.maintain_aspect_ratio) {
|
||||
html += 'height: auto; ';
|
||||
// create canvas element and image
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.id = "jspsych-image-keyboard-response-stimulus";
|
||||
canvas.style.margin = 0;
|
||||
canvas.style.padding = 0;
|
||||
var img = new Image();
|
||||
img.src = trial.stimulus;
|
||||
// determine image height and width
|
||||
if(trial.stimulus_height !== null) {
|
||||
height = trial.stimulus_height;
|
||||
if(trial.stimulus_width == null && trial.maintain_aspect_ratio) {
|
||||
width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);
|
||||
}
|
||||
} else {
|
||||
height = img.naturalHeight;
|
||||
}
|
||||
if(trial.stimulus_width !== null) {
|
||||
width = trial.stimulus_width;
|
||||
if(trial.stimulus_height == null && trial.maintain_aspect_ratio) {
|
||||
height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth);
|
||||
}
|
||||
} else if(!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) {
|
||||
// if stimulus width is null, only use the image's natural width if the width value wasn't set
|
||||
// in the if statement above, based on a specified height and maintain_aspect_ratio = true
|
||||
width = img.naturalWidth;
|
||||
}
|
||||
canvas.height = height;
|
||||
canvas.width = width;
|
||||
// add canvas and draw image
|
||||
display_element.insertBefore(canvas, null);
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
// add prompt if there is one
|
||||
if(trial.prompt !== null) {
|
||||
display_element.insertAdjacentHTML('beforeend', trial.prompt);
|
||||
}
|
||||
}
|
||||
html += '"></img>';
|
||||
|
||||
// add prompt
|
||||
if(trial.prompt !== null) {
|
||||
html += trial.prompt;
|
||||
}
|
||||
start_time = performance.now();
|
||||
|
||||
// render
|
||||
display_element.innerHTML = html;
|
||||
start_time = performance.now();
|
||||
} else {
|
||||
|
||||
// display stimulus as an image element
|
||||
var html = '<img src="' + trial.stimulus + '" id="jspsych-image-keyboard-response-stimulus">';
|
||||
// add prompt
|
||||
if(trial.prompt !== null) {
|
||||
html += trial.prompt;
|
||||
}
|
||||
// update the page content
|
||||
display_element.innerHTML = html;
|
||||
|
||||
// set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth)
|
||||
var img = display_element.querySelector('#jspsych-image-keyboard-response-stimulus');
|
||||
if(trial.stimulus_height !== null) {
|
||||
height = trial.stimulus_height;
|
||||
if(trial.stimulus_width == null && trial.maintain_aspect_ratio) {
|
||||
width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);
|
||||
}
|
||||
} else {
|
||||
height = img.naturalHeight;
|
||||
}
|
||||
if(trial.stimulus_width !== null) {
|
||||
width = trial.stimulus_width;
|
||||
if(trial.stimulus_height == null && trial.maintain_aspect_ratio) {
|
||||
height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth);
|
||||
}
|
||||
} else if(!(trial.stimulus_height !== null & trial.maintain_aspect_ratio)) {
|
||||
// if stimulus width is null, only use the image's natural width if the width value wasn't set
|
||||
// in the if statement above, based on a specified height and maintain_aspect_ratio = true
|
||||
width = img.naturalWidth;
|
||||
}
|
||||
img.style.height = height.toString() + "px";
|
||||
img.style.width = width.toString() + "px";
|
||||
|
||||
start_time = performance.now();
|
||||
}
|
||||
|
||||
// store response
|
||||
var response = {
|
||||
@@ -165,17 +230,6 @@ jsPsych.plugins["image-keyboard-response-clickable"] = (function() {
|
||||
}
|
||||
};
|
||||
|
||||
// start the response listener
|
||||
if(trial.choices != jsPsych.NO_KEYS) {
|
||||
var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({
|
||||
callback_function: after_response,
|
||||
valid_responses: trial.choices,
|
||||
rt_method: 'performance',
|
||||
persist: false,
|
||||
allow_held_key: false
|
||||
});
|
||||
}
|
||||
|
||||
if(trial.clickable){
|
||||
display_element.querySelectorAll(".clickable").forEach(function(e){
|
||||
var clickHandler = function(event){
|
||||
@@ -188,6 +242,17 @@ jsPsych.plugins["image-keyboard-response-clickable"] = (function() {
|
||||
});
|
||||
}
|
||||
|
||||
// start the response listener
|
||||
if(trial.choices != jsPsych.NO_KEYS) {
|
||||
var keyboardListener = jsPsych.pluginAPI.getKeyboardResponse({
|
||||
callback_function: after_response,
|
||||
valid_responses: trial.choices,
|
||||
rt_method: 'performance',
|
||||
persist: false,
|
||||
allow_held_key: false
|
||||
});
|
||||
}
|
||||
|
||||
// hide stimulus if stimulus_duration is set
|
||||
if(trial.stimulus_duration !== null) {
|
||||
jsPsych.pluginAPI.setTimeout(function() {
|
||||
@@ -200,8 +265,9 @@ jsPsych.plugins["image-keyboard-response-clickable"] = (function() {
|
||||
jsPsych.pluginAPI.setTimeout(function() {
|
||||
end_trial();
|
||||
}, trial.trial_duration);
|
||||
} else if(trial.response_ends_trial === false) {
|
||||
console.warn("The experiment may be deadlocked. Try setting a trial duration or set response_ends_trial to true.");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return plugin;
|
||||
|
||||
BIN
tests/res/eg-syllables-bi01.wav
Normal file
BIN
tests/res/eg-syllables-bi01.wav
Normal file
Binary file not shown.
BIN
tests/res/eg-syllables-bu01.wav
Normal file
BIN
tests/res/eg-syllables-bu01.wav
Normal file
Binary file not shown.
BIN
tests/res/eg-syllables-di01.wav
Normal file
BIN
tests/res/eg-syllables-di01.wav
Normal file
Binary file not shown.
BIN
tests/res/eg-syllables-du01.wav
Normal file
BIN
tests/res/eg-syllables-du01.wav
Normal file
Binary file not shown.
64
tests/test_jspsych-audio-sequence-button-response.html
Normal file
64
tests/test_jspsych-audio-sequence-button-response.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Test for jspsych-html-keyboard-response-clickable</title>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jquery/jquery/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@6.3.1/jspsych.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@6.3.1/plugins/jspsych-html-button-response.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@6.3.1/css/jspsych.css" rel="stylesheet" type="text/css">
|
||||
<script src="../plugins/jspsych-audio-sequence-button-response.js"></script>
|
||||
<style>
|
||||
.jspsych-audio-sequence-button-response button {
|
||||
min-width: 5em;
|
||||
min-height: 4em;
|
||||
}
|
||||
.jspsych-audio-sequence-button-response button.highlighted {
|
||||
background-color: #ffff00 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(event) {
|
||||
|
||||
var timeline = [];
|
||||
|
||||
timeline.push({
|
||||
type: 'html-button-response',
|
||||
choices: ['start'],
|
||||
stimulus: ""
|
||||
});
|
||||
|
||||
timeline.push({
|
||||
type: 'audio-sequence-button-response',
|
||||
choices: [' ', ' ', ' '],
|
||||
prompt: '<p>Prompt</p>',
|
||||
stimuli: ['res/eg-syllables-bi01.wav', 'res/eg-syllables-bu01.wav', 'res/eg-syllables-di01.wav'],
|
||||
trial_ends_after_audio: true,
|
||||
on_finish: function(){
|
||||
console.log("Trial is finished.");
|
||||
}
|
||||
});
|
||||
|
||||
timeline.push({
|
||||
type: 'audio-sequence-button-response',
|
||||
choices: [' ', ' ', ' '],
|
||||
prompt: '<p>Prompt</p>',
|
||||
stimuli: ['res/eg-syllables-bi01.wav', 'res/eg-syllables-bu01.wav', 'res/eg-syllables-di01.wav'],
|
||||
trial_ends_after_audio: true,
|
||||
on_finish: function(){
|
||||
console.log("Trial is finished.");
|
||||
}
|
||||
});
|
||||
|
||||
jsPsych.init({
|
||||
timeline: timeline
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user