@jspsych/plugin-audio-keyboard-response 1.1.3 → 2.0.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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nconst info = <const>{\n name: \"audio-keyboard-response\",\n parameters: {\n /** The audio file to be played. */\n stimulus: {\n type: ParameterType.AUDIO,\n pretty_name: \"Stimulus\",\n default: undefined,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The maximum duration to wait for a response. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when user makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then the trial will end as soon as the audio file finishes playing. */\n trial_ends_after_audio: {\n type: ParameterType.BOOL,\n pretty_name: \"Trial ends after audio\",\n default: false,\n },\n /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n },\n};\n\ntype Info = typeof info;\n\n/**\n * **audio-keyboard-response**\n *\n * jsPsych plugin for playing an audio file and getting a keyboard response\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/plugins/jspsych-audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}\n */\nclass AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n private audio;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>, on_load: () => void) {\n // hold the .resolve() function from the Promise that ends the trial\n let trial_complete;\n\n // setup stimulus\n var context = this.jsPsych.pluginAPI.audioContext();\n\n // store response\n var response = {\n rt: null,\n key: null,\n };\n\n // record webaudio context start time\n var startTime;\n\n // load audio file\n this.jsPsych.pluginAPI\n .getAudioBuffer(trial.stimulus)\n .then((buffer) => {\n if (context !== null) {\n this.audio = context.createBufferSource();\n this.audio.buffer = buffer;\n this.audio.connect(context.destination);\n } else {\n this.audio = buffer;\n this.audio.currentTime = 0;\n }\n setupTrial();\n })\n .catch((err) => {\n console.error(\n `Failed to load audio file \"${trial.stimulus}\". Try checking the file path. We recommend using the preload plugin to load audio files.`\n );\n console.error(err);\n });\n\n const setupTrial = () => {\n // set up end event if trial needs it\n if (trial.trial_ends_after_audio) {\n this.audio.addEventListener(\"ended\", end_trial);\n }\n\n // show prompt if there is one\n if (trial.prompt !== null) {\n display_element.innerHTML = trial.prompt;\n }\n\n // start audio\n if (context !== null) {\n startTime = context.currentTime;\n this.audio.start(startTime);\n } else {\n this.audio.play();\n }\n\n // start keyboard listener when trial starts or sound ends\n if (trial.response_allowed_while_playing) {\n setup_keyboard_listener();\n } else if (!trial.trial_ends_after_audio) {\n this.audio.addEventListener(\"ended\", setup_keyboard_listener);\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(() => {\n end_trial();\n }, trial.trial_duration);\n }\n\n on_load();\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill any remaining setTimeout handlers\n this.jsPsych.pluginAPI.clearAllTimeouts();\n\n // stop the audio file if it is playing\n // remove end event listeners if they exist\n if (context !== null) {\n this.audio.stop();\n } else {\n this.audio.pause();\n }\n\n this.audio.removeEventListener(\"ended\", end_trial);\n this.audio.removeEventListener(\"ended\", setup_keyboard_listener);\n\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n };\n\n // clear the display\n display_element.innerHTML = \"\";\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n\n trial_complete();\n };\n\n // function to handle responses by the subject\n function after_response(info) {\n // only record the first response\n if (response.key == null) {\n response = info;\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n }\n\n const setup_keyboard_listener = () => {\n // start the response listener\n if (context !== null) {\n this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"audio\",\n persist: false,\n allow_held_key: false,\n audio_context: context,\n audio_context_start_time: startTime,\n });\n } else {\n this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n };\n\n return new Promise((resolve) => {\n trial_complete = resolve;\n });\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n this.trial(display_element, trial, () => {\n load_callback();\n if (!trial.response_allowed_while_playing) {\n this.audio.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n });\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default AudioKeyboardResponsePlugin;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import autoBind from \"auto-bind\";\nimport { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { AudioPlayerInterface } from \"../../jspsych/src/modules/plugin-api/AudioPlayer\";\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"audio-keyboard-response\",\n version: version,\n parameters: {\n /** The audio file to be played. */\n stimulus: {\n type: ParameterType.AUDIO,\n default: undefined,\n },\n /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus.\n * Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) -\n * see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values)\n * and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/)\n * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `\"ALL_KEYS\"`\n * means that all keys will be accepted as valid responses. Specifying `\"NO_KEYS\"` will mean that no responses are allowed.\n */\n choices: {\n type: ParameterType.KEYS,\n default: \"ALL_KEYS\",\n },\n /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that\n * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press).\n */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the\n * participant fails to make a response before this timer is reached, the participant's response will be\n * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the\n * trial will wait for a response indefinitely.\n */\n trial_duration: {\n type: ParameterType.INT,\n default: null,\n },\n /** If true, then the trial will end whenever the participant makes a response (assuming they make their\n * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will\n * continue until the value for `trial_duration` is reached. You can use set this parameter to `false` to\n * force the participant to listen to the stimulus for a fixed amount of time, even if they respond before the time is complete\n */\n response_ends_trial: {\n type: ParameterType.BOOL,\n default: true,\n },\n /** If true, then the trial will end as soon as the audio file finishes playing. */\n trial_ends_after_audio: {\n type: ParameterType.BOOL,\n pretty_name: \"Trial ends after audio\",\n default: false,\n },\n /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish\n * playing before a keyboard response is accepted. Once the audio has played all the way through, a valid\n * keyboard response is allowed (including while the audio is being re-played via on-screen playback controls).\n */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n default: true,\n },\n },\n data: {\n /** Indicates which key the participant pressed. If no key was pressed before the trial ended, then the value will be `null`. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus\n * first began playing until the participant made a key response. If no key was pressed before the trial ended, then the\n * value will be `null`.\n */\n rt: {\n type: ParameterType.INT,\n },\n /** Path to the audio file that played during the trial. */\n stimulus: {\n type: ParameterType.STRING,\n },\n },\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays audio files and records responses generated with the keyboard.\n *\n * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise timing of the\n * playback. The timing of responses generated is measured against the WebAudio specific clock, improving the measurement of\n * response times. If the browser does not support the WebAudio API, then the audio file is played with HTML5 audio.\n *\n * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using\n * timeline variables or another dynamic method to specify the audio stimulus, then you will need to [manually preload](../overview/media-preloading.md#manual-preloading) the audio.\n *\n * The trial can end when the participant responds, when the audio file has finished playing, or if the participant has\n * failed to respond within a fixed length of time. You can also prevent a keyboard response from being recorded before\n * the audio has finished playing.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}\n */\nclass AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n private audio: AudioPlayerInterface;\n private params: TrialType<Info>;\n private display: HTMLElement;\n private response: { rt: number; key: string } = { rt: null, key: null };\n private startTime: number;\n private finish: ({}: { rt: number; response: string; stimulus: string }) => void;\n\n constructor(private jsPsych: JsPsych) {\n autoBind(this);\n }\n\n trial(display_element: HTMLElement, trial: TrialType<Info>, on_load: () => void) {\n return new Promise(async (resolve) => {\n this.finish = resolve;\n this.params = trial;\n this.display = display_element;\n // load audio file\n this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus);\n\n // set up end event if trial needs it\n if (trial.trial_ends_after_audio) {\n this.audio.addEventListener(\"ended\", this.end_trial);\n }\n\n // show prompt if there is one\n if (trial.prompt !== null) {\n display_element.innerHTML = trial.prompt;\n }\n\n // start playing audio here to record time\n // use this for offsetting RT measurement in\n // setup_keyboard_listener\n this.startTime = this.jsPsych.pluginAPI.audioContext()?.currentTime;\n\n // start keyboard listener when trial starts or sound ends\n if (trial.response_allowed_while_playing) {\n this.setup_keyboard_listener();\n } else if (!trial.trial_ends_after_audio) {\n this.audio.addEventListener(\"ended\", this.setup_keyboard_listener);\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(() => {\n this.end_trial();\n }, trial.trial_duration);\n }\n\n // call trial on_load method because we are done with all loading setup\n on_load();\n\n this.audio.play();\n });\n }\n\n private end_trial() {\n // kill any remaining setTimeout handlers\n this.jsPsych.pluginAPI.clearAllTimeouts();\n\n // stop the audio file if it is playing\n this.audio.stop();\n\n // remove end event listeners if they exist\n this.audio.removeEventListener(\"ended\", this.end_trial);\n this.audio.removeEventListener(\"ended\", this.setup_keyboard_listener);\n\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // gather the data to store for the trial\n var trial_data = {\n rt: this.response.rt,\n response: this.response.key,\n stimulus: this.params.stimulus,\n };\n\n // clear the display\n this.display.innerHTML = \"\";\n\n // move on to the next trial\n this.finish(trial_data);\n }\n\n private after_response(info: { key: string; rt: number }) {\n this.response = info;\n if (this.params.response_ends_trial) {\n this.end_trial();\n }\n }\n\n private setup_keyboard_listener() {\n // start the response listener\n if (this.jsPsych.pluginAPI.useWebaudio) {\n this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: this.after_response,\n valid_responses: this.params.choices,\n rt_method: \"audio\",\n persist: false,\n allow_held_key: false,\n audio_context: this.jsPsych.pluginAPI.audioContext(),\n audio_context_start_time: this.startTime,\n });\n } else {\n this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: this.after_response,\n valid_responses: this.params.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n }\n\n async simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n return this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n return this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n return data;\n }\n\n private async simulate_visual(\n trial: TrialType<Info>,\n simulation_options,\n load_callback: () => void\n ) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n const result = await this.trial(display_element, trial, () => {\n load_callback();\n if (!trial.response_allowed_while_playing) {\n this.audio.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n });\n\n return result;\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default AudioKeyboardResponsePlugin;\n"],"names":["version","info"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspsych/plugin-audio-keyboard-response",
3
- "version": "1.1.3",
3
+ "version": "2.0.0",
4
4
  "description": "jsPsych plugin for playing an audio file and getting a keyboard response",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -37,7 +37,7 @@
37
37
  "jspsych": ">=7.1.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@jspsych/config": "^2.0.0",
41
- "@jspsych/test-utils": "^1.1.2"
40
+ "@jspsych/config": "^3.0.0",
41
+ "@jspsych/test-utils": "^1.2.0"
42
42
  }
43
43
  }
package/src/index.spec.ts CHANGED
@@ -1,10 +1,131 @@
1
- import { pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
1
+ jest.mock("../../jspsych/src/modules/plugin-api/AudioPlayer");
2
+
3
+ import { flushPromises, pressKey, simulateTimeline, startTimeline } from "@jspsych/test-utils";
2
4
  import { initJsPsych } from "jspsych";
3
5
 
6
+ //@ts-expect-error mock
7
+ import { mockStop } from "../../jspsych/src/modules/plugin-api/AudioPlayer";
4
8
  import audioKeyboardResponse from ".";
5
9
 
6
10
  jest.useFakeTimers();
7
11
 
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ describe("audio-keyboard-response", () => {
17
+ // this relies on AudioContext, which we haven't mocked yet
18
+ it.skip("works with all defaults", async () => {
19
+ const { expectFinished, expectRunning } = await startTimeline([
20
+ {
21
+ type: audioKeyboardResponse,
22
+ stimulus: "foo.mp3",
23
+ },
24
+ ]);
25
+
26
+ expectRunning();
27
+
28
+ pressKey("a");
29
+
30
+ expectFinished();
31
+
32
+ await flushPromises();
33
+ });
34
+
35
+ it("works with use_webaudio:false", async () => {
36
+ const jsPsych = initJsPsych({ use_webaudio: false });
37
+
38
+ const { expectFinished, expectRunning } = await startTimeline(
39
+ [
40
+ {
41
+ type: audioKeyboardResponse,
42
+ stimulus: "foo.mp3",
43
+ },
44
+ ],
45
+ jsPsych
46
+ );
47
+
48
+ await expectRunning();
49
+ pressKey("a");
50
+ await expectFinished();
51
+ });
52
+
53
+ it("ends when trial_ends_after_audio is true and audio finishes", async () => {
54
+ const jsPsych = initJsPsych({ use_webaudio: false });
55
+
56
+ const { expectFinished, expectRunning } = await startTimeline(
57
+ [
58
+ {
59
+ type: audioKeyboardResponse,
60
+ stimulus: "foo.mp3",
61
+ trial_ends_after_audio: true,
62
+ },
63
+ ],
64
+ jsPsych
65
+ );
66
+
67
+ await expectRunning();
68
+
69
+ jest.runAllTimers();
70
+
71
+ await expectFinished();
72
+ });
73
+
74
+ it("prevents responses when response_allowed_while_playing is false", async () => {
75
+ const jsPsych = initJsPsych({ use_webaudio: false });
76
+
77
+ const { expectFinished, expectRunning } = await startTimeline(
78
+ [
79
+ {
80
+ type: audioKeyboardResponse,
81
+ stimulus: "foo.mp3",
82
+ response_allowed_while_playing: false,
83
+ },
84
+ ],
85
+ jsPsych
86
+ );
87
+
88
+ await expectRunning();
89
+
90
+ pressKey("a");
91
+
92
+ await expectRunning();
93
+
94
+ jest.runAllTimers();
95
+
96
+ await expectRunning();
97
+
98
+ pressKey("a");
99
+
100
+ await expectFinished();
101
+ });
102
+
103
+ it("ends when trial_duration is shorter than the audio duration, stopping the audio", async () => {
104
+ const jsPsych = initJsPsych({ use_webaudio: false });
105
+
106
+ const { expectFinished, expectRunning } = await startTimeline(
107
+ [
108
+ {
109
+ type: audioKeyboardResponse,
110
+ stimulus: "foo.mp3",
111
+ trial_duration: 500,
112
+ },
113
+ ],
114
+ jsPsych
115
+ );
116
+
117
+ await expectRunning();
118
+
119
+ expect(mockStop).not.toHaveBeenCalled();
120
+
121
+ jest.advanceTimersByTime(500);
122
+
123
+ expect(mockStop).toHaveBeenCalled();
124
+
125
+ await expectFinished();
126
+ });
127
+ });
128
+
8
129
  describe("audio-keyboard-response simulation", () => {
9
130
  test("data mode works", async () => {
10
131
  const timeline = [
@@ -22,8 +143,7 @@ describe("audio-keyboard-response simulation", () => {
22
143
  expect(typeof getData().values()[0].response).toBe("string");
23
144
  });
24
145
 
25
- // can't run this until we mock Audio elements.
26
- test.skip("visual mode works", async () => {
146
+ test("visual mode works", async () => {
27
147
  const jsPsych = initJsPsych({ use_webaudio: false });
28
148
 
29
149
  const timeline = [
package/src/index.ts CHANGED
@@ -1,36 +1,53 @@
1
+ import autoBind from "auto-bind";
1
2
  import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
2
3
 
4
+ import { AudioPlayerInterface } from "../../jspsych/src/modules/plugin-api/AudioPlayer";
5
+ import { version } from "../package.json";
6
+
3
7
  const info = <const>{
4
8
  name: "audio-keyboard-response",
9
+ version: version,
5
10
  parameters: {
6
11
  /** The audio file to be played. */
7
12
  stimulus: {
8
13
  type: ParameterType.AUDIO,
9
- pretty_name: "Stimulus",
10
14
  default: undefined,
11
15
  },
12
- /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */
16
+ /** This array contains the key(s) that the participant is allowed to press in order to respond to the stimulus.
17
+ * Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) -
18
+ * see [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values)
19
+ * and [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/)
20
+ * for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"`
21
+ * means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed.
22
+ */
13
23
  choices: {
14
24
  type: ParameterType.KEYS,
15
- pretty_name: "Choices",
16
25
  default: "ALL_KEYS",
17
26
  },
18
- /** Any content here will be displayed below the stimulus. */
27
+ /** This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that
28
+ * it can be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press).
29
+ */
19
30
  prompt: {
20
31
  type: ParameterType.HTML_STRING,
21
32
  pretty_name: "Prompt",
22
33
  default: null,
23
34
  },
24
- /** The maximum duration to wait for a response. */
35
+ /** How long to wait for the participant to make a response before ending the trial in milliseconds. If the
36
+ * participant fails to make a response before this timer is reached, the participant's response will be
37
+ * recorded as null for the trial and the trial will end. If the value of this parameter is null, then the
38
+ * trial will wait for a response indefinitely.
39
+ */
25
40
  trial_duration: {
26
41
  type: ParameterType.INT,
27
- pretty_name: "Trial duration",
28
42
  default: null,
29
43
  },
30
- /** If true, the trial will end when user makes a response. */
44
+ /** If true, then the trial will end whenever the participant makes a response (assuming they make their
45
+ * response before the cutoff specified by the `trial_duration` parameter). If false, then the trial will
46
+ * continue until the value for `trial_duration` is reached. You can use set this parameter to `false` to
47
+ * force the participant to listen to the stimulus for a fixed amount of time, even if they respond before the time is complete
48
+ */
31
49
  response_ends_trial: {
32
50
  type: ParameterType.BOOL,
33
- pretty_name: "Response ends trial",
34
51
  default: true,
35
52
  },
36
53
  /** If true, then the trial will end as soon as the audio file finishes playing. */
@@ -39,72 +56,77 @@ const info = <const>{
39
56
  pretty_name: "Trial ends after audio",
40
57
  default: false,
41
58
  },
42
- /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish playing before a response is accepted. */
59
+ /** If true, then responses are allowed while the audio is playing. If false, then the audio must finish
60
+ * playing before a keyboard response is accepted. Once the audio has played all the way through, a valid
61
+ * keyboard response is allowed (including while the audio is being re-played via on-screen playback controls).
62
+ */
43
63
  response_allowed_while_playing: {
44
64
  type: ParameterType.BOOL,
45
- pretty_name: "Response allowed while playing",
46
65
  default: true,
47
66
  },
48
67
  },
68
+ data: {
69
+ /** Indicates which key the participant pressed. If no key was pressed before the trial ended, then the value will be `null`. */
70
+ response: {
71
+ type: ParameterType.STRING,
72
+ },
73
+ /** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus
74
+ * first began playing until the participant made a key response. If no key was pressed before the trial ended, then the
75
+ * value will be `null`.
76
+ */
77
+ rt: {
78
+ type: ParameterType.INT,
79
+ },
80
+ /** Path to the audio file that played during the trial. */
81
+ stimulus: {
82
+ type: ParameterType.STRING,
83
+ },
84
+ },
49
85
  };
50
86
 
51
87
  type Info = typeof info;
52
88
 
53
89
  /**
54
- * **audio-keyboard-response**
90
+ * This plugin plays audio files and records responses generated with the keyboard.
91
+ *
92
+ * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise timing of the
93
+ * playback. The timing of responses generated is measured against the WebAudio specific clock, improving the measurement of
94
+ * response times. If the browser does not support the WebAudio API, then the audio file is played with HTML5 audio.
95
+ *
96
+ * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using
97
+ * timeline variables or another dynamic method to specify the audio stimulus, then you will need to [manually preload](../overview/media-preloading.md#manual-preloading) the audio.
55
98
  *
56
- * jsPsych plugin for playing an audio file and getting a keyboard response
99
+ * The trial can end when the participant responds, when the audio file has finished playing, or if the participant has
100
+ * failed to respond within a fixed length of time. You can also prevent a keyboard response from being recorded before
101
+ * the audio has finished playing.
57
102
  *
58
103
  * @author Josh de Leeuw
59
- * @see {@link https://www.jspsych.org/plugins/jspsych-audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}
104
+ * @see {@link https://www.jspsych.org/latest/plugins/audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}
60
105
  */
61
106
  class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
62
107
  static info = info;
63
- private audio;
64
-
65
- constructor(private jsPsych: JsPsych) {}
108
+ private audio: AudioPlayerInterface;
109
+ private params: TrialType<Info>;
110
+ private display: HTMLElement;
111
+ private response: { rt: number; key: string } = { rt: null, key: null };
112
+ private startTime: number;
113
+ private finish: ({}: { rt: number; response: string; stimulus: string }) => void;
114
+
115
+ constructor(private jsPsych: JsPsych) {
116
+ autoBind(this);
117
+ }
66
118
 
67
119
  trial(display_element: HTMLElement, trial: TrialType<Info>, on_load: () => void) {
68
- // hold the .resolve() function from the Promise that ends the trial
69
- let trial_complete;
120
+ return new Promise(async (resolve) => {
121
+ this.finish = resolve;
122
+ this.params = trial;
123
+ this.display = display_element;
124
+ // load audio file
125
+ this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus);
70
126
 
71
- // setup stimulus
72
- var context = this.jsPsych.pluginAPI.audioContext();
73
-
74
- // store response
75
- var response = {
76
- rt: null,
77
- key: null,
78
- };
79
-
80
- // record webaudio context start time
81
- var startTime;
82
-
83
- // load audio file
84
- this.jsPsych.pluginAPI
85
- .getAudioBuffer(trial.stimulus)
86
- .then((buffer) => {
87
- if (context !== null) {
88
- this.audio = context.createBufferSource();
89
- this.audio.buffer = buffer;
90
- this.audio.connect(context.destination);
91
- } else {
92
- this.audio = buffer;
93
- this.audio.currentTime = 0;
94
- }
95
- setupTrial();
96
- })
97
- .catch((err) => {
98
- console.error(
99
- `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.`
100
- );
101
- console.error(err);
102
- });
103
-
104
- const setupTrial = () => {
105
127
  // set up end event if trial needs it
106
128
  if (trial.trial_ends_after_audio) {
107
- this.audio.addEventListener("ended", end_trial);
129
+ this.audio.addEventListener("ended", this.end_trial);
108
130
  }
109
131
 
110
132
  // show prompt if there is one
@@ -112,107 +134,91 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
112
134
  display_element.innerHTML = trial.prompt;
113
135
  }
114
136
 
115
- // start audio
116
- if (context !== null) {
117
- startTime = context.currentTime;
118
- this.audio.start(startTime);
119
- } else {
120
- this.audio.play();
121
- }
137
+ // start playing audio here to record time
138
+ // use this for offsetting RT measurement in
139
+ // setup_keyboard_listener
140
+ this.startTime = this.jsPsych.pluginAPI.audioContext()?.currentTime;
122
141
 
123
142
  // start keyboard listener when trial starts or sound ends
124
143
  if (trial.response_allowed_while_playing) {
125
- setup_keyboard_listener();
144
+ this.setup_keyboard_listener();
126
145
  } else if (!trial.trial_ends_after_audio) {
127
- this.audio.addEventListener("ended", setup_keyboard_listener);
146
+ this.audio.addEventListener("ended", this.setup_keyboard_listener);
128
147
  }
129
148
 
130
149
  // end trial if time limit is set
131
150
  if (trial.trial_duration !== null) {
132
151
  this.jsPsych.pluginAPI.setTimeout(() => {
133
- end_trial();
152
+ this.end_trial();
134
153
  }, trial.trial_duration);
135
154
  }
136
155
 
156
+ // call trial on_load method because we are done with all loading setup
137
157
  on_load();
138
- };
139
-
140
- // function to end trial when it is time
141
- const end_trial = () => {
142
- // kill any remaining setTimeout handlers
143
- this.jsPsych.pluginAPI.clearAllTimeouts();
144
158
 
145
- // stop the audio file if it is playing
146
- // remove end event listeners if they exist
147
- if (context !== null) {
148
- this.audio.stop();
149
- } else {
150
- this.audio.pause();
151
- }
152
-
153
- this.audio.removeEventListener("ended", end_trial);
154
- this.audio.removeEventListener("ended", setup_keyboard_listener);
159
+ this.audio.play();
160
+ });
161
+ }
155
162
 
156
- // kill keyboard listeners
157
- this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
163
+ private end_trial() {
164
+ // kill any remaining setTimeout handlers
165
+ this.jsPsych.pluginAPI.clearAllTimeouts();
158
166
 
159
- // gather the data to store for the trial
160
- var trial_data = {
161
- rt: response.rt,
162
- stimulus: trial.stimulus,
163
- response: response.key,
164
- };
167
+ // stop the audio file if it is playing
168
+ this.audio.stop();
165
169
 
166
- // clear the display
167
- display_element.innerHTML = "";
170
+ // remove end event listeners if they exist
171
+ this.audio.removeEventListener("ended", this.end_trial);
172
+ this.audio.removeEventListener("ended", this.setup_keyboard_listener);
168
173
 
169
- // move on to the next trial
170
- this.jsPsych.finishTrial(trial_data);
174
+ // kill keyboard listeners
175
+ this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
171
176
 
172
- trial_complete();
177
+ // gather the data to store for the trial
178
+ var trial_data = {
179
+ rt: this.response.rt,
180
+ response: this.response.key,
181
+ stimulus: this.params.stimulus,
173
182
  };
174
183
 
175
- // function to handle responses by the subject
176
- function after_response(info) {
177
- // only record the first response
178
- if (response.key == null) {
179
- response = info;
180
- }
184
+ // clear the display
185
+ this.display.innerHTML = "";
181
186
 
182
- if (trial.response_ends_trial) {
183
- end_trial();
184
- }
185
- }
187
+ // move on to the next trial
188
+ this.finish(trial_data);
189
+ }
186
190
 
187
- const setup_keyboard_listener = () => {
188
- // start the response listener
189
- if (context !== null) {
190
- this.jsPsych.pluginAPI.getKeyboardResponse({
191
- callback_function: after_response,
192
- valid_responses: trial.choices,
193
- rt_method: "audio",
194
- persist: false,
195
- allow_held_key: false,
196
- audio_context: context,
197
- audio_context_start_time: startTime,
198
- });
199
- } else {
200
- this.jsPsych.pluginAPI.getKeyboardResponse({
201
- callback_function: after_response,
202
- valid_responses: trial.choices,
203
- rt_method: "performance",
204
- persist: false,
205
- allow_held_key: false,
206
- });
207
- }
208
- };
191
+ private after_response(info: { key: string; rt: number }) {
192
+ this.response = info;
193
+ if (this.params.response_ends_trial) {
194
+ this.end_trial();
195
+ }
196
+ }
209
197
 
210
- return new Promise((resolve) => {
211
- trial_complete = resolve;
212
- });
198
+ private setup_keyboard_listener() {
199
+ // start the response listener
200
+ if (this.jsPsych.pluginAPI.useWebaudio) {
201
+ this.jsPsych.pluginAPI.getKeyboardResponse({
202
+ callback_function: this.after_response,
203
+ valid_responses: this.params.choices,
204
+ rt_method: "audio",
205
+ persist: false,
206
+ allow_held_key: false,
207
+ audio_context: this.jsPsych.pluginAPI.audioContext(),
208
+ audio_context_start_time: this.startTime,
209
+ });
210
+ } else {
211
+ this.jsPsych.pluginAPI.getKeyboardResponse({
212
+ callback_function: this.after_response,
213
+ valid_responses: this.params.choices,
214
+ rt_method: "performance",
215
+ persist: false,
216
+ allow_held_key: false,
217
+ });
218
+ }
213
219
  }
214
220
 
215
- simulate(
221
+ async simulate(
216
222
  trial: TrialType<Info>,
217
223
  simulation_mode,
218
224
  simulation_options: any,
@@ -220,20 +226,24 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
220
226
  ) {
221
227
  if (simulation_mode == "data-only") {
222
228
  load_callback();
223
- this.simulate_data_only(trial, simulation_options);
229
+ return this.simulate_data_only(trial, simulation_options);
224
230
  }
225
231
  if (simulation_mode == "visual") {
226
- this.simulate_visual(trial, simulation_options, load_callback);
232
+ return this.simulate_visual(trial, simulation_options, load_callback);
227
233
  }
228
234
  }
229
235
 
230
236
  private simulate_data_only(trial: TrialType<Info>, simulation_options) {
231
237
  const data = this.create_simulation_data(trial, simulation_options);
232
238
 
233
- this.jsPsych.finishTrial(data);
239
+ return data;
234
240
  }
235
241
 
236
- private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {
242
+ private async simulate_visual(
243
+ trial: TrialType<Info>,
244
+ simulation_options,
245
+ load_callback: () => void
246
+ ) {
237
247
  const data = this.create_simulation_data(trial, simulation_options);
238
248
 
239
249
  const display_element = this.jsPsych.getDisplayElement();
@@ -244,7 +254,7 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
244
254
  }
245
255
  };
246
256
 
247
- this.trial(display_element, trial, () => {
257
+ const result = await this.trial(display_element, trial, () => {
248
258
  load_callback();
249
259
  if (!trial.response_allowed_while_playing) {
250
260
  this.audio.addEventListener("ended", respond);
@@ -252,6 +262,8 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
252
262
  respond();
253
263
  }
254
264
  });
265
+
266
+ return result;
255
267
  }
256
268
 
257
269
  private create_simulation_data(trial: TrialType<Info>, simulation_options) {