Files
work-calls-corpus/scripts/record-call.js

305 lines
14 KiB
JavaScript

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 = `
<div style="text-align: center; padding: 20px;">
<p style="color: red;">Microphone access is required for this experiment.</p>
<p>Please refresh the page and allow microphone access when prompted.</p>
<button onclick="location.reload()" style="
padding: 10px 20px;
font-size: 14px;
background-color: #007cba;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
">Refresh Page</button>
</div>
`;
return;
}
display_element.innerHTML = `
<div style="text-align: center; padding: 20px;">
<div style="background-color: #f0f0f0; padding: 15px; margin-bottom: 20px; border-radius: 8px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto;">
<p style="margin: 0; font-weight: bold; font-size: 16px;">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!</p>
</div>
${trial.stimulus ? `<p>${trial.stimulus}</p>` : ''}
<button id="record-button" style="
padding: 15px 30px;
font-size: 16px;
background-color: #007cba;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
margin: 10px;
">Hold to Record</button>
<div id="recording-status" style="margin: 10px; font-weight: bold;"></div>
<div id="recording-controls" style="display: none; margin: 20px;">
<audio id="playback-audio" controls style="display: block; margin: 10px auto;"></audio>
<button id="re-record-button" style="
padding: 10px 20px;
font-size: 14px;
background-color: #f44336;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
">Re-record</button>
<button id="accept-recording-button" style="
padding: 10px 20px;
font-size: 14px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
">Accept Recording</button>
</div>
</div>
`;
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;