diff --git a/index.html b/index.html
index 16c5088..9ff87d8 100644
--- a/index.html
+++ b/index.html
@@ -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';