Update project
This commit is contained in:
+192
-90
@@ -217,7 +217,6 @@
|
||||
const NOTE_BODY_PLAIN_FIELD = 'body_plain';
|
||||
const NOTE_USERNAME_FIELD = 'Username';
|
||||
let mediaRecorder = null;
|
||||
let recordedChunks = [];
|
||||
let recordedBlob = null;
|
||||
let titleRecognition = null;
|
||||
let noteRecognition = null;
|
||||
@@ -647,105 +646,172 @@
|
||||
ensureUserLogged();
|
||||
}
|
||||
|
||||
// --- Audio recording setup with auto-stop on silence ---
|
||||
// --- Audio recording setup with segmented silence handling ---
|
||||
let audioContext = null;
|
||||
let analyser = null;
|
||||
let silenceStart = null;
|
||||
let recordingStream = null;
|
||||
let silenceCheckInterval = null;
|
||||
let lastSoundAt = 0;
|
||||
let stoppingSegment = false;
|
||||
const SILENCE_THRESHOLD = 0.01; // Volume threshold for silence
|
||||
const SILENCE_DURATION = 3000; // 3 seconds
|
||||
|
||||
function checkSilence() {
|
||||
if (!analyser) return;
|
||||
const SEGMENT_SILENCE_MS = 2000; // pause a segment after 2s of silence
|
||||
const SESSION_SILENCE_MS = 3500; // end entire session after 3.5s of silence
|
||||
let recordingSessionActive = false;
|
||||
let segmentActive = false;
|
||||
let currentSegmentChunks = [];
|
||||
let recordedSegments = [];
|
||||
recordedBlob = null; // reuse existing variable for merged preview
|
||||
let hasHeardSpeech = false;
|
||||
let sessionStartAt = 0;
|
||||
|
||||
function computeRms() {
|
||||
if (!analyser) return 0;
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
|
||||
// Calculate average volume
|
||||
let sum = 0;
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const normalized = (dataArray[i] - 128) / 128;
|
||||
sum += normalized * normalized;
|
||||
}
|
||||
const rms = Math.sqrt(sum / bufferLength);
|
||||
|
||||
if (rms < SILENCE_THRESHOLD) {
|
||||
if (!silenceStart) {
|
||||
silenceStart = Date.now();
|
||||
} else if (Date.now() - silenceStart >= SILENCE_DURATION) {
|
||||
stopRecording();
|
||||
}
|
||||
} else {
|
||||
silenceStart = null;
|
||||
}
|
||||
return Math.sqrt(sum / bufferLength);
|
||||
}
|
||||
|
||||
async function setupRecorder() {
|
||||
let stream;
|
||||
async function ensureStream() {
|
||||
if (recordingStream) return recordingStream;
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
recordingStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
autoGainControl: true
|
||||
}
|
||||
autoGainControl: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Microphone access failed:', err);
|
||||
document.getElementById('recordStatus').textContent = 'Microphone access denied or unavailable.';
|
||||
throw err;
|
||||
}
|
||||
recordedChunks = [];
|
||||
recordedBlob = null;
|
||||
|
||||
// Setup audio context for silence detection
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
analyser = audioContext.createAnalyser();
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
const source = audioContext.createMediaStreamSource(recordingStream);
|
||||
source.connect(analyser);
|
||||
analyser.fftSize = 2048;
|
||||
return recordingStream;
|
||||
}
|
||||
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
function startSilenceMonitor() {
|
||||
if (silenceCheckInterval) return;
|
||||
silenceCheckInterval = setInterval(checkSilence, 120);
|
||||
}
|
||||
|
||||
function stopSilenceMonitor() {
|
||||
if (silenceCheckInterval) {
|
||||
clearInterval(silenceCheckInterval);
|
||||
silenceCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupStream() {
|
||||
stopSilenceMonitor();
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
mediaRecorder = null;
|
||||
if (recordingStream) {
|
||||
recordingStream.getTracks().forEach((t) => t.stop());
|
||||
recordingStream = null;
|
||||
}
|
||||
analyser = null;
|
||||
}
|
||||
|
||||
function updateCombinedPreview() {
|
||||
const audioEl = document.getElementById('audioPreview');
|
||||
if (!recordedSegments.length) {
|
||||
recordedBlob = null;
|
||||
audioEl.src = '';
|
||||
audioEl.classList.add('hidden');
|
||||
document.getElementById('audioPlayer').classList.add('hidden');
|
||||
document.getElementById('recordStatus').textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const type = recordedSegments[0]?.blob?.type || 'audio/webm';
|
||||
recordedBlob = new Blob(recordedSegments.map((s) => s.blob), { type });
|
||||
audioEl.src = URL.createObjectURL(recordedBlob);
|
||||
audioEl.volume = 1.0;
|
||||
audioEl.classList.remove('hidden');
|
||||
document.getElementById('audioPlayer').classList.remove('hidden');
|
||||
document.getElementById('recordStatus').textContent = `Segments recorded: ${recordedSegments.length}`;
|
||||
}
|
||||
|
||||
function startSegment() {
|
||||
if (!recordingStream) return;
|
||||
currentSegmentChunks = [];
|
||||
mediaRecorder = new MediaRecorder(recordingStream);
|
||||
const recorderRef = mediaRecorder;
|
||||
segmentActive = true;
|
||||
stoppingSegment = false;
|
||||
lastSoundAt = Date.now();
|
||||
mediaRecorder.ondataavailable = (e) => {
|
||||
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
|
||||
if (e.data && e.data.size > 0) currentSegmentChunks.push(e.data);
|
||||
};
|
||||
mediaRecorder.onstop = () => {
|
||||
if (silenceCheckInterval) {
|
||||
clearInterval(silenceCheckInterval);
|
||||
silenceCheckInterval = null;
|
||||
if (currentSegmentChunks.length) {
|
||||
const blob = new Blob(currentSegmentChunks, { type: recorderRef.mimeType });
|
||||
recordedSegments.push({ blob, name: `segment-${recordedSegments.length + 1}.webm` });
|
||||
updateCombinedPreview();
|
||||
}
|
||||
silenceStart = null;
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
segmentActive = false;
|
||||
if (mediaRecorder === recorderRef) {
|
||||
mediaRecorder = null;
|
||||
}
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
recordedBlob = new Blob(recordedChunks, { type: mediaRecorder.mimeType });
|
||||
const audioEl = document.getElementById('audioPreview');
|
||||
audioEl.src = URL.createObjectURL(recordedBlob);
|
||||
audioEl.volume = 1.0;
|
||||
audioEl.classList.remove('hidden');
|
||||
document.getElementById('audioPlayer').classList.remove('hidden');
|
||||
document.getElementById('recordStatus').textContent = '';
|
||||
mediaRecorder = null; // allow fresh recorder next start
|
||||
stoppingSegment = false;
|
||||
};
|
||||
mediaRecorder.start();
|
||||
document.getElementById('recordStatus').textContent = `Recording segment ${recordedSegments.length + 1}…`;
|
||||
}
|
||||
|
||||
function stopSegment() {
|
||||
return new Promise((resolve) => {
|
||||
if (stoppingSegment) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
stoppingSegment = true;
|
||||
const onStop = () => {
|
||||
mediaRecorder.removeEventListener('stop', onStop);
|
||||
resolve();
|
||||
};
|
||||
mediaRecorder.addEventListener('stop', onStop);
|
||||
mediaRecorder.stop();
|
||||
} else {
|
||||
segmentActive = false;
|
||||
stoppingSegment = false;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function startRecording() {
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
try {
|
||||
if (!mediaRecorder) await setupRecorder();
|
||||
silenceStart = null;
|
||||
recordedChunks = [];
|
||||
if (recordingSessionActive) return;
|
||||
await ensureStream();
|
||||
recordedSegments = [];
|
||||
recordedBlob = null;
|
||||
mediaRecorder.start();
|
||||
document.getElementById('recordStatus').textContent = 'Recording…';
|
||||
segmentActive = false;
|
||||
hasHeardSpeech = false;
|
||||
sessionStartAt = Date.now();
|
||||
recordingSessionActive = true;
|
||||
lastSoundAt = sessionStartAt;
|
||||
startSegment();
|
||||
startSilenceMonitor();
|
||||
recordBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
stopBtn.classList.remove('hidden');
|
||||
|
||||
// Start silence detection
|
||||
silenceCheckInterval = setInterval(checkSilence, 100);
|
||||
} catch (err) {
|
||||
console.error('Failed to start recording:', err);
|
||||
document.getElementById('recordStatus').textContent = 'Recording not available.';
|
||||
@@ -755,29 +821,60 @@
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
if (silenceCheckInterval) {
|
||||
clearInterval(silenceCheckInterval);
|
||||
silenceCheckInterval = null;
|
||||
async function endRecordingSession(reason) {
|
||||
recordingSessionActive = false;
|
||||
stopSilenceMonitor();
|
||||
await stopSegment();
|
||||
cleanupStream();
|
||||
const recordBtn = document.getElementById('recordBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
recordBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
stopBtn.classList.add('hidden');
|
||||
if (reason) {
|
||||
document.getElementById('recordStatus').textContent = reason;
|
||||
}
|
||||
hasHeardSpeech = false;
|
||||
}
|
||||
|
||||
async function stopRecording() {
|
||||
await endRecordingSession('Recording stopped.');
|
||||
}
|
||||
|
||||
function checkSilence() {
|
||||
if (!recordingSessionActive || !analyser) return;
|
||||
const now = Date.now();
|
||||
const rms = computeRms();
|
||||
|
||||
if (rms >= SILENCE_THRESHOLD) {
|
||||
hasHeardSpeech = true;
|
||||
lastSoundAt = now;
|
||||
if (!segmentActive) {
|
||||
startSegment();
|
||||
}
|
||||
document.getElementById('recordBtn').disabled = false;
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
stopBtn.disabled = true;
|
||||
stopBtn.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have not yet heard speech, allow up to SESSION_SILENCE_MS before ending
|
||||
if (!hasHeardSpeech) {
|
||||
if (now - sessionStartAt >= SESSION_SILENCE_MS) {
|
||||
endRecordingSession('Stopped after silence.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (segmentActive && !stoppingSegment && now - lastSoundAt >= SEGMENT_SILENCE_MS) {
|
||||
stopSegment();
|
||||
}
|
||||
|
||||
if (!segmentActive && now - lastSoundAt >= SESSION_SILENCE_MS) {
|
||||
endRecordingSession('Stopped after silence.');
|
||||
}
|
||||
}
|
||||
|
||||
function clearAudio() {
|
||||
if (silenceCheckInterval) {
|
||||
clearInterval(silenceCheckInterval);
|
||||
silenceCheckInterval = null;
|
||||
}
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
recordedChunks = [];
|
||||
async function clearAudio() {
|
||||
await endRecordingSession('Audio cleared.');
|
||||
recordedSegments = [];
|
||||
recordedBlob = null;
|
||||
const audioEl = document.getElementById('audioPreview');
|
||||
audioEl.src = '';
|
||||
@@ -788,15 +885,13 @@
|
||||
document.getElementById('currentTime').textContent = '0:00';
|
||||
document.getElementById('durationTime').textContent = '0:00';
|
||||
document.getElementById('playPauseBtn').textContent = '▶';
|
||||
document.getElementById('recordBtn').disabled = false;
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
stopBtn.disabled = true;
|
||||
stopBtn.classList.add('hidden');
|
||||
document.getElementById('recordStatus').textContent = '';
|
||||
}
|
||||
|
||||
document.getElementById('recordBtn').addEventListener('click', startRecording);
|
||||
document.getElementById('stopBtn').addEventListener('click', stopRecording);
|
||||
document.getElementById('stopBtn').addEventListener('click', () => {
|
||||
stopRecording();
|
||||
});
|
||||
// Audio options menu
|
||||
const audioMenuContainer = document.getElementById('audioMenuContainer');
|
||||
const audioMenuBtn = document.getElementById('audioMenuBtn');
|
||||
@@ -853,10 +948,10 @@
|
||||
e.preventDefault();
|
||||
audioMenu.classList.toggle('hidden');
|
||||
});
|
||||
removeAudioBtn.addEventListener('click', (e) => {
|
||||
removeAudioBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
audioMenu.classList.add('hidden');
|
||||
clearAudio();
|
||||
await clearAudio();
|
||||
});
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!audioMenuContainer.contains(e.target)) {
|
||||
@@ -1217,16 +1312,23 @@
|
||||
}
|
||||
|
||||
const noteRecord = await pb.collection(NOTES_COLLECTION).create(payload);
|
||||
let attachmentRecord = null;
|
||||
if (recordedBlob) {
|
||||
const file = new File([recordedBlob], 'note-audio.webm', { type: recordedBlob.type || 'audio/webm' });
|
||||
const fd = new FormData();
|
||||
fd.append(ATTACHMENTS_FILE_FIELD, file);
|
||||
fd.append(ATTACHMENTS_PBID_FIELD, noteRecord.id);
|
||||
attachmentRecord = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
|
||||
const attachmentRecords = [];
|
||||
if (recordedSegments && recordedSegments.length) {
|
||||
for (let i = 0; i < recordedSegments.length; i++) {
|
||||
const seg = recordedSegments[i];
|
||||
const blobToUpload = seg.blob || seg;
|
||||
const fileName = seg.name || `note-audio-seg-${i + 1}.webm`;
|
||||
const file = new File([blobToUpload], fileName, { type: blobToUpload.type || 'audio/webm' });
|
||||
const fd = new FormData();
|
||||
fd.append(ATTACHMENTS_FILE_FIELD, file);
|
||||
fd.append(ATTACHMENTS_PBID_FIELD, noteRecord.id);
|
||||
const createdAtt = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
|
||||
attachmentRecords.push(createdAtt);
|
||||
}
|
||||
}
|
||||
statusEl.textContent = `Saved note ${noteRecord.id}` + (attachmentRecord ? ` with audio ${attachmentRecord.id}` : '');
|
||||
clearAudio();
|
||||
const attachmentMsg = attachmentRecords.length ? ` with ${attachmentRecords.length} audio segment(s)` : '';
|
||||
statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`;
|
||||
await clearAudio();
|
||||
document.getElementById('noteText').value = '';
|
||||
document.getElementById('noteTitle').value = '';
|
||||
document.getElementById('jobNote').value = 'no';
|
||||
|
||||
Reference in New Issue
Block a user