import { ParameterType } from 'jspsych'; const info = { name: "record-call", parameters: { stimulus: { type: ParameterType.STRING, default: null, }, }, }; class jsPsychRecordCall { constructor(jsPsych) { this.jsPsych = jsPsych; this.recorded_data_chunks = []; this.recording = false; this.recorder = null; this.current_recording = null; this.audio_duration = null; } static { this.info = info; } trial(display_element, trial) { try { this.recorder = this.jsPsych.pluginAPI.getMicrophoneRecorder(); if (!this.recorder) { throw new Error("Microphone not initialized"); } this.setupRecordingEvents(); } catch (error) { console.error("Microphone setup failed:", error); display_element.innerHTML = `

Microphone access is required for this experiment.

Please refresh the page and allow microphone access when prompted.

`; return; } display_element.innerHTML = `

Imagine two people lifting something heavy together. In your culture, is there a typical phrase, word or utterance that is used in such a situation? Please record one such a phrase, word or utterance!

${trial.stimulus ? `

${trial.stimulus}

` : ''}
`; this.setupButtonEvents(display_element, trial); } setupRecordingEvents() { this.data_available_handler = (e) => { if (e.data.size > 0) { this.recorded_data_chunks.push(e.data); } }; this.stop_event_handler = () => { const data = new Blob(this.recorded_data_chunks, {type: "audio/ogg"}); this.current_recording = data; const audioUrl = URL.createObjectURL(data); const audioElement = document.getElementById("playback-audio"); audioElement.src = audioUrl; // Force playback to load metadata and get exact duration document.getElementById("recording-status").textContent = "Processing recording..."; document.getElementById("record-button").style.display = "none"; const loadAudioMetadata = () => { let metadataLoaded = false; const showControls = () => { if (metadataLoaded) return; metadataLoaded = true; // Clean up audioElement.muted = false; audioElement.pause(); audioElement.currentTime = 0; // Show controls document.getElementById("recording-controls").style.display = "block"; document.getElementById("record-button").style.display = "none"; document.getElementById("recording-status").textContent = "Recording complete. Listen and choose:"; console.log('Final audio duration:', this.audio_duration); }; // Multiple event handlers to catch metadata loading const onMetadataEvent = () => { if (audioElement.duration && isFinite(audioElement.duration) && audioElement.duration > 0) { this.audio_duration = audioElement.duration; console.log('Audio duration loaded via metadata:', this.audio_duration); showControls(); } }; const onCanPlayEvent = () => { if (audioElement.duration && isFinite(audioElement.duration) && audioElement.duration > 0) { this.audio_duration = audioElement.duration; console.log('Audio duration loaded via canplay:', this.audio_duration); showControls(); } }; audioElement.addEventListener('loadedmetadata', onMetadataEvent); audioElement.addEventListener('loadeddata', onMetadataEvent); audioElement.addEventListener('canplay', onCanPlayEvent); // Mute and try to load audioElement.muted = true; audioElement.currentTime = 0; audioElement.load(); // Fallback: if metadata doesn't load within 2 seconds, continue anyway setTimeout(() => { if (!metadataLoaded) { console.warn('Metadata loading timeout, continuing without duration'); showControls(); } }, 2000); // Try playing after a short delay if metadata isn't loaded setTimeout(() => { if (!metadataLoaded && !this.audio_duration) { console.log('Attempting to play audio to force metadata loading'); audioElement.play().catch(e => { console.log('Auto-play prevented:', e); // If play fails, just continue if (!metadataLoaded) { showControls(); } }); } }, 500); }; loadAudioMetadata(); }; this.start_event_handler = (e) => { this.recorded_data_chunks.length = 0; this.recorder_start_time = e.timeStamp; }; this.recorder.addEventListener("dataavailable", this.data_available_handler); this.recorder.addEventListener("stop", this.stop_event_handler); this.recorder.addEventListener("start", this.start_event_handler); } setupButtonEvents(display_element, trial) { const recordButton = document.getElementById("record-button"); const reRecordButton = document.getElementById("re-record-button"); const acceptButton = document.getElementById("accept-recording-button"); const statusDiv = document.getElementById("recording-status"); recordButton.addEventListener("mousedown", () => { if (!this.recording) { this.startRecording(); recordButton.textContent = "Recording... (Release to stop)"; recordButton.style.backgroundColor = "#f44336"; statusDiv.textContent = "Recording in progress..."; } }); recordButton.addEventListener("mouseup", () => { if (this.recording) { this.stopRecording(); recordButton.textContent = "Hold to Record"; recordButton.style.backgroundColor = "#007cba"; statusDiv.textContent = "Processing recording..."; } }); recordButton.addEventListener("mouseleave", () => { if (this.recording) { this.stopRecording(); recordButton.textContent = "Hold to Record"; recordButton.style.backgroundColor = "#007cba"; statusDiv.textContent = "Processing recording..."; } }); reRecordButton.addEventListener("click", () => { document.getElementById("recording-controls").style.display = "none"; document.getElementById("record-button").style.display = "inline-block"; statusDiv.textContent = ""; this.current_recording = null; }); acceptButton.addEventListener("click", () => { this.endTrial(trial); }); } startRecording() { try { this.recording = true; this.recorder.start(); this.stimulus_start_time = Date.now(); } catch (error) { console.error("Failed to start recording:", error); document.getElementById("recording-status").textContent = "Recording failed. Please refresh and try again."; this.recording = false; } } stopRecording() { if (this.recording) { this.recording = false; // Add a small buffer to prevent clipping setTimeout(() => { this.recorder.stop(); }, 200); // 200ms buffer } } endTrial(trial) { this.recorder.removeEventListener("dataavailable", this.data_available_handler); this.recorder.removeEventListener("start", this.start_event_handler); this.recorder.removeEventListener("stop", this.stop_event_handler); this.jsPsych.pluginAPI.clearAllTimeouts(); const reader = new FileReader(); reader.addEventListener("load", () => { const response = reader.result.split(",")[1]; let trial_data = { rt: this.stimulus_start_time ? Date.now() - this.stimulus_start_time : null, stimulus: trial.stimulus, response: response, estimated_stimulus_onset: this.recorder_start_time ? Math.round(this.stimulus_start_time - this.recorder_start_time) : null, audio_duration: this.audio_duration }; this.jsPsych.finishTrial(trial_data); }); if (this.current_recording) { reader.readAsDataURL(this.current_recording); } else { this.jsPsych.finishTrial({ rt: null, stimulus: trial.stimulus, response: null, estimated_stimulus_onset: null, audio_duration: this.audio_duration }); } } } export default jsPsychRecordCall;