diff --git a/assets/js/voice.js b/assets/js/voice.js index abbb492..1b86879 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -36,9 +36,15 @@ class VoiceChannel { // Ignore if in input field if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; - if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) { + if (this.settings.mode !== 'ptt') return; + + const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() || + (e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) || + (this.settings.pttKey === '0' && e.code === 'Numpad0'); + + if (isMatch) { if (!this.pttPressed) { - console.log('PTT Key Pressed:', e.key); + console.log('PTT Key Pressed:', e.key, e.code, 'Expected:', this.settings.pttKey); this.pttPressed = true; this.updateMuteState(); } @@ -46,8 +52,14 @@ class VoiceChannel { }); window.addEventListener('keyup', (e) => { - if (this.settings.mode === 'ptt' && e.key.toLowerCase() === this.settings.pttKey.toLowerCase()) { - console.log('PTT Key Released:', e.key); + if (this.settings.mode !== 'ptt') return; + + const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() || + (e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) || + (this.settings.pttKey === '0' && e.code === 'Numpad0'); + + if (isMatch) { + console.log('PTT Key Released:', e.key, e.code, 'Expected:', this.settings.pttKey); this.pttPressed = false; this.updateMuteState(); } @@ -253,38 +265,59 @@ class VoiceChannel { } setupVOX() { - if (this.audioContext) this.audioContext.close(); - this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - this.analyser = this.audioContext.createAnalyser(); - this.microphone = this.audioContext.createMediaStreamSource(this.localStream); - this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1); - this.microphone.connect(this.analyser); - this.analyser.connect(this.scriptProcessor); - this.scriptProcessor.connect(this.audioContext.destination); - - this.scriptProcessor.onaudioprocess = () => { - const array = new Uint8Array(this.analyser.frequencyBinCount); - this.analyser.getByteFrequencyData(array); - let values = 0; - for (let i = 0; i < array.length; i++) values += array[i]; - const average = values / array.length; - - // Log sometimes for debugging VOX - if (Math.random() < 0.01) console.log('VOX Avg:', average, 'Threshold:', this.settings.voxThreshold * 255); - - if (average > (this.settings.voxThreshold * 255)) { - this.lastVoiceTime = Date.now(); - if (!this.voxActive) { - this.voxActive = true; - this.updateMuteState(); - } - } else { - if (this.voxActive && Date.now() - this.lastVoiceTime > this.voxHoldTime) { - this.voxActive = false; - this.updateMuteState(); - } + if (this.audioContext) { + if (this.audioContext.state === 'suspended') { + this.audioContext.resume(); } - }; + return; + } + + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.analyser = this.audioContext.createAnalyser(); + this.analyser.fftSize = 512; + this.microphone = this.audioContext.createMediaStreamSource(this.localStream); + this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1); + + this.microphone.connect(this.analyser); + this.analyser.connect(this.scriptProcessor); + this.scriptProcessor.connect(this.audioContext.destination); + + this.currentVolume = 0; + + this.scriptProcessor.onaudioprocess = () => { + const array = new Uint8Array(this.analyser.frequencyBinCount); + this.analyser.getByteFrequencyData(array); + let values = 0; + for (let i = 0; i < array.length; i++) values += array[i]; + const average = values / array.length; + this.currentVolume = average / 255; + + if (this.settings.mode !== 'vox') { + this.voxActive = false; + return; + } + + if (this.currentVolume > this.settings.voxThreshold) { + this.lastVoiceTime = Date.now(); + if (!this.voxActive) { + this.voxActive = true; + this.updateMuteState(); + } + } else { + if (this.voxActive && Date.now() - this.lastVoiceTime > this.voxHoldTime) { + this.voxActive = false; + this.updateMuteState(); + } + } + }; + } catch (e) { + console.error('Failed to setup VOX:', e); + } + } + + getVolume() { + return this.currentVolume || 0; } updateMuteState() { diff --git a/data/22.log b/data/22.log index 96c6b9e..e69de29 100644 --- a/data/22.log +++ b/data/22.log @@ -1,2 +0,0 @@ -{"from":"a0645f38fb2bbdb5","to":"1ca650259dcce60e","data":{"type":"voice_speaking","channel_id":"22","user_id":3,"speaking":true},"time":1771339536024} -{"from":"a0645f38fb2bbdb5","to":"1ca650259dcce60e","data":{"type":"voice_speaking","channel_id":"22","user_id":3,"speaking":false},"time":1771339536956} diff --git a/data/22.participants.json b/data/22.participants.json index 0637a08..bb2fb68 100644 --- a/data/22.participants.json +++ b/data/22.participants.json @@ -1 +1 @@ -[] \ No newline at end of file +{"45a8f0c9dde7c4a2":{"id":"45a8f0c9dde7c4a2","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771340822998}} \ No newline at end of file diff --git a/data/3.log b/data/3.log index 16d88e7..e69de29 100644 --- a/data/3.log +++ b/data/3.log @@ -1,8 +0,0 @@ -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 2993514939591859431 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 E9:89:B8:DE:41:F8:AD:79:17:A8:4D:03:2D:53:FC:15:5A:3B:B4:CA:45:A6:F7:EB:C7:F0:01:7C:0B:29:91:F1\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:cacccb2562e0f73ce1667f9773fb670d\r\na=ice-ufrag:2b377091\r\na=mid:0\r\na=msid:{5d44acc3-9af3-4110-98ed-2de218fc7450} {ed9d31cb-0fc7-497e-8c79-4cc9159d5f97}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:2872346815 cname:{74df363a-4160-4031-a110-5285ddfe55ff}\r\n"}},"time":1771339604154} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 61807 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604155} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604161} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604164} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 61808 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604170} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604171} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 30532 typ srflx raddr 192.168.26.26 rport 61807","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604172} -{"from":"9afce7ba24e9091b","to":"a39fca0ca87f4e62","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 30534 typ srflx raddr 192.168.26.26 rport 61808","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"2b377091"}},"time":1771339604175} diff --git a/data/3.participants.json b/data/3.participants.json index 942c50d..cbc2de9 100644 --- a/data/3.participants.json +++ b/data/3.participants.json @@ -1 +1 @@ -{"1e0fff021b7ad021":{"id":"1e0fff021b7ad021","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771339631904},"9afce7ba24e9091b":{"id":"9afce7ba24e9091b","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771339632155}} \ No newline at end of file +{"1e0897d1dcb980dc":{"id":"1e0897d1dcb980dc","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771340822155}} \ No newline at end of file diff --git a/index.php b/index.php index 82fc216..3f3915a 100644 --- a/index.php +++ b/index.php @@ -1232,10 +1232,14 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
@@ -1311,6 +1315,57 @@ document.addEventListener('DOMContentLoaded', () => { pttInput.value = e.key; }); } + + // Voice meter update + const voxThresholdInput = document.getElementById('vox_threshold_input'); + const meterThreshold = document.getElementById('voice-meter-threshold'); + const meterBar = document.getElementById('voice-meter-bar'); + + // Handle voice tab activation for mic preview + const voiceTabBtn = document.querySelector('[data-bs-target="#settings-voice"]'); + if (voiceTabBtn) { + voiceTabBtn.addEventListener('shown.bs.tab', async () => { + if (window.voiceHandler && !window.voiceHandler.localStream) { + try { + console.log('Voice tab active, requesting mic for preview...'); + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + window.voiceHandler.localStream = stream; + window.voiceHandler.setupVOX(); + } catch (e) { + console.error('Failed to get mic for preview:', e); + } + } else if (window.voiceHandler && window.voiceHandler.localStream) { + window.voiceHandler.setupVOX(); + } + }); + } + + if (voxThresholdInput && meterThreshold) { + const updateThresholdPos = () => { + // Threshold input is 0 (loud) to 1 (quiet) + // But actually 1.0 means high threshold, so quiet needs more voice + // 0.1 means low threshold, easy to trigger. + // Meter is 0 to 100%. + meterThreshold.style.left = (voxThresholdInput.value * 100) + '%'; + }; + voxThresholdInput.addEventListener('input', updateThresholdPos); + updateThresholdPos(); + } + + setInterval(() => { + if (window.voiceHandler && meterBar && document.getElementById('settings-voice').classList.contains('active')) { + const volume = window.voiceHandler.getVolume(); // 0 to 1 + meterBar.style.width = (volume * 100) + '%'; + + // Color feedback + const threshold = parseFloat(voxThresholdInput.value); + if (volume > threshold) { + meterBar.style.backgroundColor = '#23a559'; // Green + } else { + meterBar.style.backgroundColor = '#4f545c'; // Grey + } + } + }, 50); }); function handlePTTKeyCapture(e, input) { @@ -1352,7 +1407,32 @@ async function handleSaveUserSettings(btn) { if (result.success) { btn.innerHTML = ' Saved!'; - setTimeout(() => window.location.reload(), 500); + + // Update local voiceHandler settings without reload + if (window.voiceHandler) { + const mode = formData.get('voice_mode'); + const pttKey = formData.get('voice_ptt_key'); + const voxThreshold = parseFloat(formData.get('voice_vox_threshold')); + + window.voiceHandler.settings.mode = mode; + window.voiceHandler.settings.pttKey = pttKey; + window.voiceHandler.settings.voxThreshold = voxThreshold; + + console.log('Voice settings updated locally:', window.voiceHandler.settings); + + if (mode === 'vox' && !window.voiceHandler.audioContext) { + window.voiceHandler.setupVOX(); + } + + window.voiceHandler.updateVoiceUI(); + } + + setTimeout(() => { + btn.innerHTML = originalContent; + btn.disabled = false; + // Optional: close modal after save? + // bootstrap.Modal.getInstance(document.getElementById('userSettingsModal')).hide(); + }, 1500); } else { alert('Error: ' + (result.error || 'Unknown error')); btn.disabled = false; diff --git a/requests.log b/requests.log index 2ad73f1..fb2110c 100644 --- a/requests.log +++ b/requests.log @@ -582,3 +582,27 @@ 2026-02-17 14:46:17 - GET /index.php?server_id=1&channel_id=22 - POST: [] {"date":"2026-02-17 14:46:39","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} 2026-02-17 14:46:40 - GET /index.php?server_id=1&channel_id=22 - POST: [] +{"date":"2026-02-17 14:48:43","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-17 14:48:44 - GET /index.php?server_id=1&channel_id=22 - POST: [] +{"date":"2026-02-17 14:48:59","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-17 14:48:59 - GET /index.php?server_id=1&channel_id=22 - POST: [] +2026-02-17 14:59:20 - GET / - POST: [] +2026-02-17 14:59:50 - GET /?fl_project=38443 - POST: [] +2026-02-17 14:59:50 - GET /?fl_project=38443 - POST: [] +2026-02-17 15:02:24 - GET /index.php?server_id=1&channel_id=22 - POST: [] +2026-02-17 15:02:30 - GET /index.php?server_id=1&channel_id=1 - POST: [] +{"date":"2026-02-17 15:02:45","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-17 15:02:57 - GET /index.php - POST: [] +2026-02-17 15:03:02 - GET /index.php - POST: [] +{"date":"2026-02-17 15:03:19","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true} +2026-02-17 15:03:33 - GET /index.php?server_id=1&channel_id=1 - POST: [] +{"date":"2026-02-17 15:03:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.01","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +{"date":"2026-02-17 15:03:44","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.01","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +{"date":"2026-02-17 15:04:13","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.38","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true} +2026-02-17 15:04:30 - GET /index.php?server_id=1&channel_id=1 - POST: [] +{"date":"2026-02-17 15:04:48","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-17 15:04:52 - GET /index.php?server_id=1&channel_id=1 - POST: [] +{"date":"2026-02-17 15:05:02","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +{"date":"2026-02-17 15:05:57","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-17 15:06:05 - GET /index.php?server_id=1&channel_id=1 - POST: [] +2026-02-17 15:06:09 - GET /index.php?server_id=1&channel_id=1 - POST: []