305 lines
14 KiB
JavaScript
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; |