diff --git a/packages/plugin-audio-keyboard-response/src/index.ts b/packages/plugin-audio-keyboard-response/src/index.ts index aaae336b27..36395ba84c 100644 --- a/packages/plugin-audio-keyboard-response/src/index.ts +++ b/packages/plugin-audio-keyboard-response/src/index.ts @@ -1,3 +1,4 @@ +import autoBind from "auto-bind"; import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych"; const info = { @@ -61,50 +62,28 @@ type Info = typeof info; class AudioKeyboardResponsePlugin implements JsPsychPlugin { static info = info; private audio; - - constructor(private jsPsych: JsPsych) {} + private params: TrialType; + private display: HTMLElement; + private response: { rt: number; key: string } = { rt: null, key: null }; + private startTime: number; + private finish: ({}: { rt: number; response: string; stimulus: string }) => void; + + constructor(private jsPsych: JsPsych) { + autoBind(this); + } trial(display_element: HTMLElement, trial: TrialType, on_load: () => void) { - // hold the .resolve() function from the Promise that ends the trial - let trial_complete; + return new Promise(async (resolve) => { + this.finish = resolve; + this.params = trial; + this.display = display_element; - // setup stimulus - var context = this.jsPsych.pluginAPI.audioContext(); - - // store response - var response = { - rt: null, - key: null, - }; + // load audio file + this.audio = await this.jsPsych.pluginAPI.getAudioPlayer(trial.stimulus); - // record webaudio context start time - var startTime; - - // load audio file - this.jsPsych.pluginAPI - .getAudioBuffer(trial.stimulus) - .then((buffer) => { - if (context !== null) { - this.audio = context.createBufferSource(); - this.audio.buffer = buffer; - this.audio.connect(context.destination); - } else { - this.audio = buffer; - this.audio.currentTime = 0; - } - setupTrial(); - }) - .catch((err) => { - console.error( - `Failed to load audio file "${trial.stimulus}". Try checking the file path. We recommend using the preload plugin to load audio files.` - ); - console.error(err); - }); - - const setupTrial = () => { // set up end event if trial needs it if (trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", end_trial); + this.audio.addEventListener("ended", this.end_trial); } // show prompt if there is one @@ -112,104 +91,88 @@ class AudioKeyboardResponsePlugin implements JsPsychPlugin { display_element.innerHTML = trial.prompt; } - // start audio - if (context !== null) { - startTime = context.currentTime; - this.audio.start(startTime); - } else { - this.audio.play(); - } + // start playing audio here to record time + // use this for offsetting RT measurement in + // setup_keyboard_listener + this.startTime = this.jsPsych.pluginAPI.audioContext()?.currentTime; // start keyboard listener when trial starts or sound ends if (trial.response_allowed_while_playing) { - setup_keyboard_listener(); + this.setup_keyboard_listener(); } else if (!trial.trial_ends_after_audio) { - this.audio.addEventListener("ended", setup_keyboard_listener); + this.audio.addEventListener("ended", this.setup_keyboard_listener); } // end trial if time limit is set if (trial.trial_duration !== null) { this.jsPsych.pluginAPI.setTimeout(() => { - end_trial(); + this.end_trial(); }, trial.trial_duration); } + // call trial on_load method because we are done with all loading setup on_load(); - }; - // function to end trial when it is time - const end_trial = () => { - // kill any remaining setTimeout handlers - this.jsPsych.pluginAPI.clearAllTimeouts(); - - // stop the audio file if it is playing - // remove end event listeners if they exist - if (context !== null) { - this.audio.stop(); - } else { - this.audio.pause(); - } - - this.audio.removeEventListener("ended", end_trial); - this.audio.removeEventListener("ended", setup_keyboard_listener); + this.audio.play(); + }); + } - // kill keyboard listeners - this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); + private end_trial() { + // kill any remaining setTimeout handlers + this.jsPsych.pluginAPI.clearAllTimeouts(); - // gather the data to store for the trial - var trial_data = { - rt: response.rt, - stimulus: trial.stimulus, - response: response.key, - }; + // stop the audio file if it is playing + this.audio.stop(); - // clear the display - display_element.innerHTML = ""; + // remove end event listeners if they exist + this.audio.removeEventListener("ended", this.end_trial); + this.audio.removeEventListener("ended", this.setup_keyboard_listener); - // move on to the next trial - this.jsPsych.finishTrial(trial_data); + // kill keyboard listeners + this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); - trial_complete(); + // gather the data to store for the trial + var trial_data = { + rt: this.response.rt, + response: this.response.key, + stimulus: this.params.stimulus, }; - // function to handle responses by the subject - function after_response(info) { - // only record the first response - if (response.key == null) { - response = info; - } + // clear the display + this.display.innerHTML = ""; - if (trial.response_ends_trial) { - end_trial(); - } - } + // move on to the next trial + this.finish(trial_data); + } - const setup_keyboard_listener = () => { - // start the response listener - if (context !== null) { - this.jsPsych.pluginAPI.getKeyboardResponse({ - callback_function: after_response, - valid_responses: trial.choices, - rt_method: "audio", - persist: false, - allow_held_key: false, - audio_context: context, - audio_context_start_time: startTime, - }); - } else { - this.jsPsych.pluginAPI.getKeyboardResponse({ - callback_function: after_response, - valid_responses: trial.choices, - rt_method: "performance", - persist: false, - allow_held_key: false, - }); - } - }; + private after_response(info: { key: string; rt: number }) { + this.response = info; + if (this.params.response_ends_trial) { + this.end_trial(); + } + } - return new Promise((resolve) => { - trial_complete = resolve; - }); + private setup_keyboard_listener() { + // start the response listener + if (this.jsPsych.pluginAPI.useWebaudio) { + this.jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: this.after_response, + valid_responses: this.params.choices, + rt_method: "audio", + persist: false, + allow_held_key: false, + audio_context: this.jsPsych.pluginAPI.audioContext(), + audio_context_start_time: this.startTime, + }); + } else { + this.jsPsych.pluginAPI.getKeyboardResponse({ + callback_function: this.after_response, + valid_responses: this.params.choices, + rt_method: "performance", + persist: false, + allow_held_key: false, + }); + } } simulate(