@jspsych/plugin-audio-keyboard-response 1.1.3 → 2.1.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":";;AAEA,MAAM,IAAI,GAAU;AAClB,IAAA,IAAI,EAAE,yBAAyB;AAC/B,IAAA,UAAU,EAAE;;AAEV,QAAA,QAAQ,EAAE;YACR,IAAI,EAAE,aAAa,CAAC,KAAK;AACzB,YAAA,WAAW,EAAE,UAAU;AACvB,YAAA,OAAO,EAAE,SAAS;AACnB,SAAA;;AAED,QAAA,OAAO,EAAE;YACP,IAAI,EAAE,aAAa,CAAC,IAAI;AACxB,YAAA,WAAW,EAAE,SAAS;AACtB,YAAA,OAAO,EAAE,UAAU;AACpB,SAAA;;AAED,QAAA,MAAM,EAAE;YACN,IAAI,EAAE,aAAa,CAAC,WAAW;AAC/B,YAAA,WAAW,EAAE,QAAQ;AACrB,YAAA,OAAO,EAAE,IAAI;AACd,SAAA;;AAED,QAAA,cAAc,EAAE;YACd,IAAI,EAAE,aAAa,CAAC,GAAG;AACvB,YAAA,WAAW,EAAE,gBAAgB;AAC7B,YAAA,OAAO,EAAE,IAAI;AACd,SAAA;;AAED,QAAA,mBAAmB,EAAE;YACnB,IAAI,EAAE,aAAa,CAAC,IAAI;AACxB,YAAA,WAAW,EAAE,qBAAqB;AAClC,YAAA,OAAO,EAAE,IAAI;AACd,SAAA;;AAED,QAAA,sBAAsB,EAAE;YACtB,IAAI,EAAE,aAAa,CAAC,IAAI;AACxB,YAAA,WAAW,EAAE,wBAAwB;AACrC,YAAA,OAAO,EAAE,KAAK;AACf,SAAA;;AAED,QAAA,8BAA8B,EAAE;YAC9B,IAAI,EAAE,aAAa,CAAC,IAAI;AACxB,YAAA,WAAW,EAAE,gCAAgC;AAC7C,YAAA,OAAO,EAAE,IAAI;AACd,SAAA;AACF,KAAA;CACF,CAAC;AAIF;;;;;;;AAOG;AACH,MAAM,2BAA2B,CAAA;AAI/B,IAAA,WAAA,CAAoB,OAAgB,EAAA;QAAhB,IAAO,CAAA,OAAA,GAAP,OAAO,CAAS;KAAI;AAExC,IAAA,KAAK,CAAC,eAA4B,EAAE,KAAsB,EAAE,OAAmB,EAAA;;AAE7E,QAAA,IAAI,cAAc,CAAC;;QAGnB,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;;AAGpD,QAAA,IAAI,QAAQ,GAAG;AACb,YAAA,EAAE,EAAE,IAAI;AACR,YAAA,GAAG,EAAE,IAAI;SACV,CAAC;;AAGF,QAAA,IAAI,SAAS,CAAC;;QAGd,IAAI,CAAC,OAAO,CAAC,SAAS;AACnB,aAAA,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC;AAC9B,aAAA,IAAI,CAAC,CAAC,MAAM,KAAI;YACf,IAAI,OAAO,KAAK,IAAI,EAAE;AACpB,gBAAA,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;AAC1C,gBAAA,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;gBAC3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;AACzC,aAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;AACpB,gBAAA,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC;AAC5B,aAAA;AACD,YAAA,UAAU,EAAE,CAAC;AACf,SAAC,CAAC;AACD,aAAA,KAAK,CAAC,CAAC,GAAG,KAAI;YACb,OAAO,CAAC,KAAK,CACX,CAAA,2BAAA,EAA8B,KAAK,CAAC,QAAQ,CAA2F,yFAAA,CAAA,CACxI,CAAC;AACF,YAAA,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACrB,SAAC,CAAC,CAAC;QAEL,MAAM,UAAU,GAAG,MAAK;;YAEtB,IAAI,KAAK,CAAC,sBAAsB,EAAE;gBAChC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACjD,aAAA;;AAGD,YAAA,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE;AACzB,gBAAA,eAAe,CAAC,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;AAC1C,aAAA;;YAGD,IAAI,OAAO,KAAK,IAAI,EAAE;AACpB,gBAAA,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;AAChC,gBAAA,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;AAC7B,aAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;AACnB,aAAA;;YAGD,IAAI,KAAK,CAAC,8BAA8B,EAAE;AACxC,gBAAA,uBAAuB,EAAE,CAAC;AAC3B,aAAA;AAAM,iBAAA,IAAI,CAAC,KAAK,CAAC,sBAAsB,EAAE;gBACxC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;AAC/D,aAAA;;AAGD,YAAA,IAAI,KAAK,CAAC,cAAc,KAAK,IAAI,EAAE;gBACjC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,MAAK;AACrC,oBAAA,SAAS,EAAE,CAAC;AACd,iBAAC,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;AAC1B,aAAA;AAED,YAAA,OAAO,EAAE,CAAC;AACZ,SAAC,CAAC;;QAGF,MAAM,SAAS,GAAG,MAAK;;AAErB,YAAA,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,gBAAgB,EAAE,CAAC;;;YAI1C,IAAI,OAAO,KAAK,IAAI,EAAE;AACpB,gBAAA,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;AACnB,aAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;AACpB,aAAA;YAED,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACnD,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;;AAGjE,YAAA,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,0BAA0B,EAAE,CAAC;;AAGpD,YAAA,IAAI,UAAU,GAAG;gBACf,EAAE,EAAE,QAAQ,CAAC,EAAE;gBACf,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,QAAQ,EAAE,QAAQ,CAAC,GAAG;aACvB,CAAC;;AAGF,YAAA,eAAe,CAAC,SAAS,GAAG,EAAE,CAAC;;AAG/B,YAAA,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;AAErC,YAAA,cAAc,EAAE,CAAC;AACnB,SAAC,CAAC;;QAGF,SAAS,cAAc,CAAC,IAAI,EAAA;;AAE1B,YAAA,IAAI,QAAQ,CAAC,GAAG,IAAI,IAAI,EAAE;gBACxB,QAAQ,GAAG,IAAI,CAAC;AACjB,aAAA;YAED,IAAI,KAAK,CAAC,mBAAmB,EAAE;AAC7B,gBAAA,SAAS,EAAE,CAAC;AACb,aAAA;SACF;QAED,MAAM,uBAAuB,GAAG,MAAK;;YAEnC,IAAI,OAAO,KAAK,IAAI,EAAE;AACpB,gBAAA,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC;AACzC,oBAAA,iBAAiB,EAAE,cAAc;oBACjC,eAAe,EAAE,KAAK,CAAC,OAAO;AAC9B,oBAAA,SAAS,EAAE,OAAO;AAClB,oBAAA,OAAO,EAAE,KAAK;AACd,oBAAA,cAAc,EAAE,KAAK;AACrB,oBAAA,aAAa,EAAE,OAAO;AACtB,oBAAA,wBAAwB,EAAE,SAAS;AACpC,iBAAA,CAAC,CAAC;AACJ,aAAA;AAAM,iBAAA;AACL,gBAAA,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC;AACzC,oBAAA,iBAAiB,EAAE,cAAc;oBACjC,eAAe,EAAE,KAAK,CAAC,OAAO;AAC9B,oBAAA,SAAS,EAAE,aAAa;AACxB,oBAAA,OAAO,EAAE,KAAK;AACd,oBAAA,cAAc,EAAE,KAAK;AACtB,iBAAA,CAAC,CAAC;AACJ,aAAA;AACH,SAAC,CAAC;AAEF,QAAA,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,KAAI;YAC7B,cAAc,GAAG,OAAO,CAAC;AAC3B,SAAC,CAAC,CAAC;KACJ;AAED,IAAA,QAAQ,CACN,KAAsB,EACtB,eAAe,EACf,kBAAuB,EACvB,aAAyB,EAAA;QAEzB,IAAI,eAAe,IAAI,WAAW,EAAE;AAClC,YAAA,aAAa,EAAE,CAAC;AAChB,YAAA,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;AACpD,SAAA;QACD,IAAI,eAAe,IAAI,QAAQ,EAAE;YAC/B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,kBAAkB,EAAE,aAAa,CAAC,CAAC;AAChE,SAAA;KACF;IAEO,kBAAkB,CAAC,KAAsB,EAAE,kBAAkB,EAAA;QACnE,MAAM,IAAI,GAAG,IAAI,CAAC,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;AAEpE,QAAA,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;KAChC;AAEO,IAAA,eAAe,CAAC,KAAsB,EAAE,kBAAkB,EAAE,aAAyB,EAAA;QAC3F,MAAM,IAAI,GAAG,IAAI,CAAC,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC;QAEpE,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;QAEzD,MAAM,OAAO,GAAG,MAAK;AACnB,YAAA,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE;AACpB,gBAAA,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;AACzD,aAAA;AACH,SAAC,CAAC;QAEF,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,MAAK;AACtC,YAAA,aAAa,EAAE,CAAC;AAChB,YAAA,IAAI,CAAC,KAAK,CAAC,8BAA8B,EAAE;gBACzC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAC/C,aAAA;AAAM,iBAAA;AACL,gBAAA,OAAO,EAAE,CAAC;AACX,aAAA;AACH,SAAC,CAAC,CAAC;KACJ;IAEO,sBAAsB,CAAC,KAAsB,EAAE,kBAAkB,EAAA;AACvE,QAAA,MAAM,YAAY,GAAG;YACnB,QAAQ,EAAE,KAAK,CAAC,QAAQ;AACxB,YAAA,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,gBAAgB,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,EAAE,IAAI,CAAC;AACvE,YAAA,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC;SAC5D,CAAC;AAEF,QAAA,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,mBAAmB,CAAC,YAAY,EAAE,kBAAkB,CAAC,CAAC;QAE1F,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,+BAA+B,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AAEpE,QAAA,OAAO,IAAI,CAAC;KACb;;AA/MM,2BAAI,CAAA,IAAA,GAAG,IAAI;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-audio-keyboard-response\",\n \"version\": \"2.1.0\",\n \"description\": \"jsPsych plugin for playing an audio file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-audio-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/audio-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","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 // prettier-ignore\n citations: '__CITATIONS__',\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":[],"mappings":";;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECmFA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
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.1.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.2.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,79 @@ 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
+ },
85
+ // prettier-ignore
86
+ citations: '__CITATIONS__',
49
87
  };
50
88
 
51
89
  type Info = typeof info;
52
90
 
53
91
  /**
54
- * **audio-keyboard-response**
92
+ * This plugin plays audio files and records responses generated with the keyboard.
93
+ *
94
+ * If the browser supports it, audio files are played using the WebAudio API. This allows for reasonably precise timing of the
95
+ * playback. The timing of responses generated is measured against the WebAudio specific clock, improving the measurement of
96
+ * response times. If the browser does not support the WebAudio API, then the audio file is played with HTML5 audio.
97
+ *
98
+ * Audio files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using
99
+ * 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
100
  *
56
- * jsPsych plugin for playing an audio file and getting a keyboard response
101
+ * The trial can end when the participant responds, when the audio file has finished playing, or if the participant has
102
+ * failed to respond within a fixed length of time. You can also prevent a keyboard response from being recorded before
103
+ * the audio has finished playing.
57
104
  *
58
105
  * @author Josh de Leeuw
59
- * @see {@link https://www.jspsych.org/plugins/jspsych-audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}
106
+ * @see {@link https://www.jspsych.org/latest/plugins/audio-keyboard-response/ audio-keyboard-response plugin documentation on jspsych.org}
60
107
  */
61
108
  class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
62
109
  static info = info;
63
- private audio;
64
-
65
- constructor(private jsPsych: JsPsych) {}
110
+ private audio: AudioPlayerInterface;
111
+ private params: TrialType<Info>;
112
+ private display: HTMLElement;
113
+ private response: { rt: number; key: string } = { rt: null, key: null };
114
+ private startTime: number;
115
+ private finish: ({}: { rt: number; response: string; stimulus: string }) => void;
116
+
117
+ constructor(private jsPsych: JsPsych) {
118
+ autoBind(this);
119
+ }
66
120
 
67
121
  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;
122
+ return new Promise(async (resolve) => {
123
+ this.finish = resolve;
124
+ this.params = trial;
125
+ this.display = display_element;
126
+ // load audio file
127
+ this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus);
70
128
 
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
129
  // set up end event if trial needs it
106
130
  if (trial.trial_ends_after_audio) {
107
- this.audio.addEventListener("ended", end_trial);
131
+ this.audio.addEventListener("ended", this.end_trial);
108
132
  }
109
133
 
110
134
  // show prompt if there is one
@@ -112,107 +136,91 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
112
136
  display_element.innerHTML = trial.prompt;
113
137
  }
114
138
 
115
- // start audio
116
- if (context !== null) {
117
- startTime = context.currentTime;
118
- this.audio.start(startTime);
119
- } else {
120
- this.audio.play();
121
- }
139
+ // start playing audio here to record time
140
+ // use this for offsetting RT measurement in
141
+ // setup_keyboard_listener
142
+ this.startTime = this.jsPsych.pluginAPI.audioContext()?.currentTime;
122
143
 
123
144
  // start keyboard listener when trial starts or sound ends
124
145
  if (trial.response_allowed_while_playing) {
125
- setup_keyboard_listener();
146
+ this.setup_keyboard_listener();
126
147
  } else if (!trial.trial_ends_after_audio) {
127
- this.audio.addEventListener("ended", setup_keyboard_listener);
148
+ this.audio.addEventListener("ended", this.setup_keyboard_listener);
128
149
  }
129
150
 
130
151
  // end trial if time limit is set
131
152
  if (trial.trial_duration !== null) {
132
153
  this.jsPsych.pluginAPI.setTimeout(() => {
133
- end_trial();
154
+ this.end_trial();
134
155
  }, trial.trial_duration);
135
156
  }
136
157
 
158
+ // call trial on_load method because we are done with all loading setup
137
159
  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
160
 
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);
161
+ this.audio.play();
162
+ });
163
+ }
155
164
 
156
- // kill keyboard listeners
157
- this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
165
+ private end_trial() {
166
+ // kill any remaining setTimeout handlers
167
+ this.jsPsych.pluginAPI.clearAllTimeouts();
158
168
 
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
- };
169
+ // stop the audio file if it is playing
170
+ this.audio.stop();
165
171
 
166
- // clear the display
167
- display_element.innerHTML = "";
172
+ // remove end event listeners if they exist
173
+ this.audio.removeEventListener("ended", this.end_trial);
174
+ this.audio.removeEventListener("ended", this.setup_keyboard_listener);
168
175
 
169
- // move on to the next trial
170
- this.jsPsych.finishTrial(trial_data);
176
+ // kill keyboard listeners
177
+ this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
171
178
 
172
- trial_complete();
179
+ // gather the data to store for the trial
180
+ var trial_data = {
181
+ rt: this.response.rt,
182
+ response: this.response.key,
183
+ stimulus: this.params.stimulus,
173
184
  };
174
185
 
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
- }
186
+ // clear the display
187
+ this.display.innerHTML = "";
181
188
 
182
- if (trial.response_ends_trial) {
183
- end_trial();
184
- }
185
- }
189
+ // move on to the next trial
190
+ this.finish(trial_data);
191
+ }
186
192
 
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
- };
193
+ private after_response(info: { key: string; rt: number }) {
194
+ this.response = info;
195
+ if (this.params.response_ends_trial) {
196
+ this.end_trial();
197
+ }
198
+ }
209
199
 
210
- return new Promise((resolve) => {
211
- trial_complete = resolve;
212
- });
200
+ private setup_keyboard_listener() {
201
+ // start the response listener
202
+ if (this.jsPsych.pluginAPI.useWebaudio) {
203
+ this.jsPsych.pluginAPI.getKeyboardResponse({
204
+ callback_function: this.after_response,
205
+ valid_responses: this.params.choices,
206
+ rt_method: "audio",
207
+ persist: false,
208
+ allow_held_key: false,
209
+ audio_context: this.jsPsych.pluginAPI.audioContext(),
210
+ audio_context_start_time: this.startTime,
211
+ });
212
+ } else {
213
+ this.jsPsych.pluginAPI.getKeyboardResponse({
214
+ callback_function: this.after_response,
215
+ valid_responses: this.params.choices,
216
+ rt_method: "performance",
217
+ persist: false,
218
+ allow_held_key: false,
219
+ });
220
+ }
213
221
  }
214
222
 
215
- simulate(
223
+ async simulate(
216
224
  trial: TrialType<Info>,
217
225
  simulation_mode,
218
226
  simulation_options: any,
@@ -220,20 +228,24 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
220
228
  ) {
221
229
  if (simulation_mode == "data-only") {
222
230
  load_callback();
223
- this.simulate_data_only(trial, simulation_options);
231
+ return this.simulate_data_only(trial, simulation_options);
224
232
  }
225
233
  if (simulation_mode == "visual") {
226
- this.simulate_visual(trial, simulation_options, load_callback);
234
+ return this.simulate_visual(trial, simulation_options, load_callback);
227
235
  }
228
236
  }
229
237
 
230
238
  private simulate_data_only(trial: TrialType<Info>, simulation_options) {
231
239
  const data = this.create_simulation_data(trial, simulation_options);
232
240
 
233
- this.jsPsych.finishTrial(data);
241
+ return data;
234
242
  }
235
243
 
236
- private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {
244
+ private async simulate_visual(
245
+ trial: TrialType<Info>,
246
+ simulation_options,
247
+ load_callback: () => void
248
+ ) {
237
249
  const data = this.create_simulation_data(trial, simulation_options);
238
250
 
239
251
  const display_element = this.jsPsych.getDisplayElement();
@@ -244,7 +256,7 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
244
256
  }
245
257
  };
246
258
 
247
- this.trial(display_element, trial, () => {
259
+ const result = await this.trial(display_element, trial, () => {
248
260
  load_callback();
249
261
  if (!trial.response_allowed_while_playing) {
250
262
  this.audio.addEventListener("ended", respond);
@@ -252,6 +264,8 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin<Info> {
252
264
  respond();
253
265
  }
254
266
  });
267
+
268
+ return result;
255
269
  }
256
270
 
257
271
  private create_simulation_data(trial: TrialType<Info>, simulation_options) {