@jspsych/plugin-audio-button-response 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,302 +2,321 @@
2
2
 
3
3
  var jspsych = require('jspsych');
4
4
 
5
- const info = {
6
- name: "audio-button-response",
7
- parameters: {
8
- /** The audio to be played. */
9
- stimulus: {
10
- type: jspsych.ParameterType.AUDIO,
11
- pretty_name: "Stimulus",
12
- default: undefined,
13
- },
14
- /** Array containing the label(s) for the button(s). */
15
- choices: {
16
- type: jspsych.ParameterType.STRING,
17
- pretty_name: "Choices",
18
- default: undefined,
19
- array: true,
20
- },
21
- /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */
22
- button_html: {
23
- type: jspsych.ParameterType.HTML_STRING,
24
- pretty_name: "Button HTML",
25
- default: '<button class="jspsych-btn">%choice%</button>',
26
- array: true,
27
- },
28
- /** Any content here will be displayed below the stimulus. */
29
- prompt: {
30
- type: jspsych.ParameterType.HTML_STRING,
31
- pretty_name: "Prompt",
32
- default: null,
33
- },
34
- /** The maximum duration to wait for a response. */
35
- trial_duration: {
36
- type: jspsych.ParameterType.INT,
37
- pretty_name: "Trial duration",
38
- default: null,
39
- },
40
- /** Vertical margin of button. */
41
- margin_vertical: {
42
- type: jspsych.ParameterType.STRING,
43
- pretty_name: "Margin vertical",
44
- default: "0px",
45
- },
46
- /** Horizontal margin of button. */
47
- margin_horizontal: {
48
- type: jspsych.ParameterType.STRING,
49
- pretty_name: "Margin horizontal",
50
- default: "8px",
51
- },
52
- /** If true, the trial will end when user makes a response. */
53
- response_ends_trial: {
54
- type: jspsych.ParameterType.BOOL,
55
- pretty_name: "Response ends trial",
56
- default: true,
57
- },
58
- /** If true, then the trial will end as soon as the audio file finishes playing. */
59
- trial_ends_after_audio: {
60
- type: jspsych.ParameterType.BOOL,
61
- pretty_name: "Trial ends after audio",
62
- default: false,
63
- },
64
- /**
65
- * If true, then responses are allowed while the audio is playing.
66
- * If false, then the audio must finish playing before a response is accepted.
67
- */
68
- response_allowed_while_playing: {
69
- type: jspsych.ParameterType.BOOL,
70
- pretty_name: "Response allowed while playing",
71
- default: true,
72
- },
73
- },
74
- };
75
- /**
76
- * **audio-button-response**
77
- *
78
- * jsPsych plugin for playing an audio file and getting a button response
79
- *
80
- * @author Kristin Diep
81
- * @see {@link https://www.jspsych.org/plugins/jspsych-audio-button-response/ audio-button-response plugin documentation on jspsych.org}
82
- */
83
- class AudioButtonResponsePlugin {
84
- constructor(jsPsych) {
85
- this.jsPsych = jsPsych;
86
- }
87
- trial(display_element, trial, on_load) {
88
- // hold the .resolve() function from the Promise that ends the trial
89
- let trial_complete;
90
- // setup stimulus
91
- var context = this.jsPsych.pluginAPI.audioContext();
92
- // store response
93
- var response = {
94
- rt: null,
95
- button: null,
96
- };
97
- // record webaudio context start time
98
- var startTime;
99
- // load audio file
100
- this.jsPsych.pluginAPI
101
- .getAudioBuffer(trial.stimulus)
102
- .then((buffer) => {
103
- if (context !== null) {
104
- this.audio = context.createBufferSource();
105
- this.audio.buffer = buffer;
106
- this.audio.connect(context.destination);
107
- }
108
- else {
109
- this.audio = buffer;
110
- this.audio.currentTime = 0;
111
- }
112
- setupTrial();
113
- })
114
- .catch((err) => {
115
- console.error(`Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.`);
116
- console.error(err);
117
- });
118
- const setupTrial = () => {
119
- // set up end event if trial needs it
120
- if (trial.trial_ends_after_audio) {
121
- this.audio.addEventListener("ended", end_trial);
122
- }
123
- // enable buttons after audio ends if necessary
124
- if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
125
- this.audio.addEventListener("ended", enable_buttons);
126
- }
127
- //display buttons
128
- var buttons = [];
129
- if (Array.isArray(trial.button_html)) {
130
- if (trial.button_html.length == trial.choices.length) {
131
- buttons = trial.button_html;
132
- }
133
- else {
134
- console.error("Error in audio-button-response plugin. The length of the button_html array does not equal the length of the choices array");
135
- }
136
- }
137
- else {
138
- for (var i = 0; i < trial.choices.length; i++) {
139
- buttons.push(trial.button_html);
140
- }
141
- }
142
- var html = '<div id="jspsych-audio-button-response-btngroup">';
143
- for (var i = 0; i < trial.choices.length; i++) {
144
- var str = buttons[i].replace(/%choice%/g, trial.choices[i]);
145
- html +=
146
- '<div class="jspsych-audio-button-response-button" style="cursor: pointer; display: inline-block; margin:' +
147
- trial.margin_vertical +
148
- " " +
149
- trial.margin_horizontal +
150
- '" id="jspsych-audio-button-response-button-' +
151
- i +
152
- '" data-choice="' +
153
- i +
154
- '">' +
155
- str +
156
- "</div>";
157
- }
158
- html += "</div>";
159
- //show prompt if there is one
160
- if (trial.prompt !== null) {
161
- html += trial.prompt;
162
- }
163
- display_element.innerHTML = html;
164
- if (trial.response_allowed_while_playing) {
165
- enable_buttons();
166
- }
167
- else {
168
- disable_buttons();
169
- }
170
- // start time
171
- startTime = performance.now();
172
- // start audio
173
- if (context !== null) {
174
- startTime = context.currentTime;
175
- this.audio.start(startTime);
176
- }
177
- else {
178
- this.audio.play();
179
- }
180
- // end trial if time limit is set
181
- if (trial.trial_duration !== null) {
182
- this.jsPsych.pluginAPI.setTimeout(() => {
183
- end_trial();
184
- }, trial.trial_duration);
185
- }
186
- on_load();
187
- };
188
- // function to handle responses by the subject
189
- function after_response(choice) {
190
- // measure rt
191
- var endTime = performance.now();
192
- var rt = Math.round(endTime - startTime);
193
- if (context !== null) {
194
- endTime = context.currentTime;
195
- rt = Math.round((endTime - startTime) * 1000);
196
- }
197
- response.button = parseInt(choice);
198
- response.rt = rt;
199
- // disable all the buttons after a response
200
- disable_buttons();
201
- if (trial.response_ends_trial) {
202
- end_trial();
203
- }
204
- }
205
- // function to end trial when it is time
206
- const end_trial = () => {
207
- // kill any remaining setTimeout handlers
208
- this.jsPsych.pluginAPI.clearAllTimeouts();
209
- // stop the audio file if it is playing
210
- // remove end event listeners if they exist
211
- if (context !== null) {
212
- this.audio.stop();
213
- }
214
- else {
215
- this.audio.pause();
216
- }
217
- this.audio.removeEventListener("ended", end_trial);
218
- this.audio.removeEventListener("ended", enable_buttons);
219
- // gather the data to store for the trial
220
- var trial_data = {
221
- rt: response.rt,
222
- stimulus: trial.stimulus,
223
- response: response.button,
224
- };
225
- // clear the display
226
- display_element.innerHTML = "";
227
- // move on to the next trial
228
- this.jsPsych.finishTrial(trial_data);
229
- trial_complete();
230
- };
231
- function button_response(e) {
232
- var choice = e.currentTarget.getAttribute("data-choice"); // don't use dataset for jsdom compatibility
233
- after_response(choice);
234
- }
235
- function disable_buttons() {
236
- var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
237
- for (var i = 0; i < btns.length; i++) {
238
- var btn_el = btns[i].querySelector("button");
239
- if (btn_el) {
240
- btn_el.disabled = true;
241
- }
242
- btns[i].removeEventListener("click", button_response);
243
- }
244
- }
245
- function enable_buttons() {
246
- var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
247
- for (var i = 0; i < btns.length; i++) {
248
- var btn_el = btns[i].querySelector("button");
249
- if (btn_el) {
250
- btn_el.disabled = false;
251
- }
252
- btns[i].addEventListener("click", button_response);
253
- }
254
- }
255
- return new Promise((resolve) => {
256
- trial_complete = resolve;
257
- });
258
- }
259
- simulate(trial, simulation_mode, simulation_options, load_callback) {
260
- if (simulation_mode == "data-only") {
261
- load_callback();
262
- this.simulate_data_only(trial, simulation_options);
263
- }
264
- if (simulation_mode == "visual") {
265
- this.simulate_visual(trial, simulation_options, load_callback);
266
- }
267
- }
268
- create_simulation_data(trial, simulation_options) {
269
- const default_data = {
270
- stimulus: trial.stimulus,
271
- rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
272
- response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1),
273
- };
274
- const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
275
- this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
276
- return data;
277
- }
278
- simulate_data_only(trial, simulation_options) {
279
- const data = this.create_simulation_data(trial, simulation_options);
280
- this.jsPsych.finishTrial(data);
281
- }
282
- simulate_visual(trial, simulation_options, load_callback) {
283
- const data = this.create_simulation_data(trial, simulation_options);
284
- const display_element = this.jsPsych.getDisplayElement();
285
- const respond = () => {
286
- if (data.rt !== null) {
287
- this.jsPsych.pluginAPI.clickTarget(display_element.querySelector(`div[data-choice="${data.response}"] button`), data.rt);
288
- }
289
- };
290
- this.trial(display_element, trial, () => {
291
- load_callback();
292
- if (!trial.response_allowed_while_playing) {
293
- this.audio.addEventListener("ended", respond);
294
- }
295
- else {
296
- respond();
297
- }
298
- });
299
- }
300
- }
5
+ const info = {
6
+ name: "audio-button-response",
7
+ parameters: {
8
+ /** The audio to be played. */
9
+ stimulus: {
10
+ type: jspsych.ParameterType.AUDIO,
11
+ pretty_name: "Stimulus",
12
+ default: undefined,
13
+ },
14
+ /** Array containing the label(s) for the button(s). */
15
+ choices: {
16
+ type: jspsych.ParameterType.STRING,
17
+ pretty_name: "Choices",
18
+ default: undefined,
19
+ array: true,
20
+ },
21
+ /** The HTML for creating button. Can create own style. Use the "%choice%" string to indicate where the label from the choices parameter should be inserted. */
22
+ button_html: {
23
+ type: jspsych.ParameterType.HTML_STRING,
24
+ pretty_name: "Button HTML",
25
+ default: '<button class="jspsych-btn">%choice%</button>',
26
+ array: true,
27
+ },
28
+ /** Any content here will be displayed below the stimulus. */
29
+ prompt: {
30
+ type: jspsych.ParameterType.HTML_STRING,
31
+ pretty_name: "Prompt",
32
+ default: null,
33
+ },
34
+ /** The maximum duration to wait for a response. */
35
+ trial_duration: {
36
+ type: jspsych.ParameterType.INT,
37
+ pretty_name: "Trial duration",
38
+ default: null,
39
+ },
40
+ /** Vertical margin of button. */
41
+ margin_vertical: {
42
+ type: jspsych.ParameterType.STRING,
43
+ pretty_name: "Margin vertical",
44
+ default: "0px",
45
+ },
46
+ /** Horizontal margin of button. */
47
+ margin_horizontal: {
48
+ type: jspsych.ParameterType.STRING,
49
+ pretty_name: "Margin horizontal",
50
+ default: "8px",
51
+ },
52
+ /** If true, the trial will end when user makes a response. */
53
+ response_ends_trial: {
54
+ type: jspsych.ParameterType.BOOL,
55
+ pretty_name: "Response ends trial",
56
+ default: true,
57
+ },
58
+ /** If true, then the trial will end as soon as the audio file finishes playing. */
59
+ trial_ends_after_audio: {
60
+ type: jspsych.ParameterType.BOOL,
61
+ pretty_name: "Trial ends after audio",
62
+ default: false,
63
+ },
64
+ /**
65
+ * If true, then responses are allowed while the audio is playing.
66
+ * If false, then the audio must finish playing before a response is accepted.
67
+ */
68
+ response_allowed_while_playing: {
69
+ type: jspsych.ParameterType.BOOL,
70
+ pretty_name: "Response allowed while playing",
71
+ default: true,
72
+ },
73
+ /** The delay of enabling button */
74
+ enable_button_after: {
75
+ type: jspsych.ParameterType.INT,
76
+ pretty_name: "Enable button after",
77
+ default: 0,
78
+ },
79
+ },
80
+ };
81
+ /**
82
+ * **audio-button-response**
83
+ *
84
+ * jsPsych plugin for playing an audio file and getting a button response
85
+ *
86
+ * @author Kristin Diep
87
+ * @see {@link https://www.jspsych.org/plugins/jspsych-audio-button-response/ audio-button-response plugin documentation on jspsych.org}
88
+ */
89
+ class AudioButtonResponsePlugin {
90
+ constructor(jsPsych) {
91
+ this.jsPsych = jsPsych;
92
+ }
93
+ trial(display_element, trial, on_load) {
94
+ // hold the .resolve() function from the Promise that ends the trial
95
+ let trial_complete;
96
+ // setup stimulus
97
+ var context = this.jsPsych.pluginAPI.audioContext();
98
+ // store response
99
+ var response = {
100
+ rt: null,
101
+ button: null,
102
+ };
103
+ // record webaudio context start time
104
+ var startTime;
105
+ // load audio file
106
+ this.jsPsych.pluginAPI
107
+ .getAudioBuffer(trial.stimulus)
108
+ .then((buffer) => {
109
+ if (context !== null) {
110
+ this.audio = context.createBufferSource();
111
+ this.audio.buffer = buffer;
112
+ this.audio.connect(context.destination);
113
+ }
114
+ else {
115
+ this.audio = buffer;
116
+ this.audio.currentTime = 0;
117
+ }
118
+ setupTrial();
119
+ })
120
+ .catch((err) => {
121
+ console.error(`Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.`);
122
+ console.error(err);
123
+ });
124
+ const setupTrial = () => {
125
+ // set up end event if trial needs it
126
+ if (trial.trial_ends_after_audio) {
127
+ this.audio.addEventListener("ended", end_trial);
128
+ }
129
+ // enable buttons after audio ends if necessary
130
+ if (!trial.response_allowed_while_playing && !trial.trial_ends_after_audio) {
131
+ this.audio.addEventListener("ended", enable_buttons);
132
+ }
133
+ //display buttons
134
+ var buttons = [];
135
+ if (Array.isArray(trial.button_html)) {
136
+ if (trial.button_html.length == trial.choices.length) {
137
+ buttons = trial.button_html;
138
+ }
139
+ else {
140
+ console.error("Error in audio-button-response plugin. The length of the button_html array does not equal the length of the choices array");
141
+ }
142
+ }
143
+ else {
144
+ for (var i = 0; i < trial.choices.length; i++) {
145
+ buttons.push(trial.button_html);
146
+ }
147
+ }
148
+ var html = '<div id="jspsych-audio-button-response-btngroup">';
149
+ for (var i = 0; i < trial.choices.length; i++) {
150
+ var str = buttons[i].replace(/%choice%/g, trial.choices[i]);
151
+ html +=
152
+ '<div class="jspsych-audio-button-response-button" style="cursor: pointer; display: inline-block; margin:' +
153
+ trial.margin_vertical +
154
+ " " +
155
+ trial.margin_horizontal +
156
+ '" id="jspsych-audio-button-response-button-' +
157
+ i +
158
+ '" data-choice="' +
159
+ i +
160
+ '">' +
161
+ str +
162
+ "</div>";
163
+ }
164
+ html += "</div>";
165
+ //show prompt if there is one
166
+ if (trial.prompt !== null) {
167
+ html += trial.prompt;
168
+ }
169
+ display_element.innerHTML = html;
170
+ if (trial.response_allowed_while_playing) {
171
+ disable_buttons();
172
+ enable_buttons();
173
+ }
174
+ else {
175
+ disable_buttons();
176
+ }
177
+ // start time
178
+ startTime = performance.now();
179
+ // start audio
180
+ if (context !== null) {
181
+ startTime = context.currentTime;
182
+ this.audio.start(startTime);
183
+ }
184
+ else {
185
+ this.audio.play();
186
+ }
187
+ // end trial if time limit is set
188
+ if (trial.trial_duration !== null) {
189
+ this.jsPsych.pluginAPI.setTimeout(() => {
190
+ end_trial();
191
+ }, trial.trial_duration);
192
+ }
193
+ on_load();
194
+ };
195
+ // function to handle responses by the subject
196
+ function after_response(choice) {
197
+ // measure rt
198
+ var endTime = performance.now();
199
+ var rt = Math.round(endTime - startTime);
200
+ if (context !== null) {
201
+ endTime = context.currentTime;
202
+ rt = Math.round((endTime - startTime) * 1000);
203
+ }
204
+ response.button = parseInt(choice);
205
+ response.rt = rt;
206
+ // disable all the buttons after a response
207
+ disable_buttons();
208
+ if (trial.response_ends_trial) {
209
+ end_trial();
210
+ }
211
+ }
212
+ // function to end trial when it is time
213
+ const end_trial = () => {
214
+ // kill any remaining setTimeout handlers
215
+ this.jsPsych.pluginAPI.clearAllTimeouts();
216
+ // stop the audio file if it is playing
217
+ // remove end event listeners if they exist
218
+ if (context !== null) {
219
+ this.audio.stop();
220
+ }
221
+ else {
222
+ this.audio.pause();
223
+ }
224
+ this.audio.removeEventListener("ended", end_trial);
225
+ this.audio.removeEventListener("ended", enable_buttons);
226
+ // gather the data to store for the trial
227
+ var trial_data = {
228
+ rt: response.rt,
229
+ stimulus: trial.stimulus,
230
+ response: response.button,
231
+ };
232
+ // clear the display
233
+ display_element.innerHTML = "";
234
+ // move on to the next trial
235
+ this.jsPsych.finishTrial(trial_data);
236
+ trial_complete();
237
+ };
238
+ const enable_buttons_with_delay = (delay) => {
239
+ this.jsPsych.pluginAPI.setTimeout(enable_buttons_without_delay, delay);
240
+ };
241
+ function button_response(e) {
242
+ var choice = e.currentTarget.getAttribute("data-choice"); // don't use dataset for jsdom compatibility
243
+ after_response(choice);
244
+ }
245
+ function disable_buttons() {
246
+ var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
247
+ for (var i = 0; i < btns.length; i++) {
248
+ var btn_el = btns[i].querySelector("button");
249
+ if (btn_el) {
250
+ btn_el.disabled = true;
251
+ }
252
+ btns[i].removeEventListener("click", button_response);
253
+ }
254
+ }
255
+ function enable_buttons_without_delay() {
256
+ var btns = document.querySelectorAll(".jspsych-audio-button-response-button");
257
+ for (var i = 0; i < btns.length; i++) {
258
+ var btn_el = btns[i].querySelector("button");
259
+ if (btn_el) {
260
+ btn_el.disabled = false;
261
+ }
262
+ btns[i].addEventListener("click", button_response);
263
+ }
264
+ }
265
+ function enable_buttons() {
266
+ if (trial.enable_button_after > 0) {
267
+ enable_buttons_with_delay(trial.enable_button_after);
268
+ }
269
+ else {
270
+ enable_buttons_without_delay();
271
+ }
272
+ }
273
+ return new Promise((resolve) => {
274
+ trial_complete = resolve;
275
+ });
276
+ }
277
+ simulate(trial, simulation_mode, simulation_options, load_callback) {
278
+ if (simulation_mode == "data-only") {
279
+ load_callback();
280
+ this.simulate_data_only(trial, simulation_options);
281
+ }
282
+ if (simulation_mode == "visual") {
283
+ this.simulate_visual(trial, simulation_options, load_callback);
284
+ }
285
+ }
286
+ create_simulation_data(trial, simulation_options) {
287
+ const default_data = {
288
+ stimulus: trial.stimulus,
289
+ rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true) +
290
+ trial.enable_button_after,
291
+ response: this.jsPsych.randomization.randomInt(0, trial.choices.length - 1),
292
+ };
293
+ const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
294
+ this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
295
+ return data;
296
+ }
297
+ simulate_data_only(trial, simulation_options) {
298
+ const data = this.create_simulation_data(trial, simulation_options);
299
+ this.jsPsych.finishTrial(data);
300
+ }
301
+ simulate_visual(trial, simulation_options, load_callback) {
302
+ const data = this.create_simulation_data(trial, simulation_options);
303
+ const display_element = this.jsPsych.getDisplayElement();
304
+ const respond = () => {
305
+ if (data.rt !== null) {
306
+ this.jsPsych.pluginAPI.clickTarget(display_element.querySelector(`div[data-choice="${data.response}"] button`), data.rt);
307
+ }
308
+ };
309
+ this.trial(display_element, trial, () => {
310
+ load_callback();
311
+ if (!trial.response_allowed_while_playing) {
312
+ this.audio.addEventListener("ended", respond);
313
+ }
314
+ else {
315
+ respond();
316
+ }
317
+ });
318
+ }
319
+ }
301
320
  AudioButtonResponsePlugin.info = info;
302
321
 
303
322
  module.exports = AudioButtonResponsePlugin;