diff --git a/api_v1_user.php b/api_v1_user.php index fc72c08..33b40ad 100644 --- a/api_v1_user.php +++ b/api_v1_user.php @@ -29,10 +29,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $voice_mode = !empty($_POST['voice_mode']) ? $_POST['voice_mode'] : ($user['voice_mode'] ?? 'vox'); $voice_ptt_key = !empty($_POST['voice_ptt_key']) ? $_POST['voice_ptt_key'] : ($user['voice_ptt_key'] ?? 'v'); $voice_vox_threshold = isset($_POST['voice_vox_threshold']) ? (float)$_POST['voice_vox_threshold'] : ($user['voice_vox_threshold'] ?? 0.1); + $voice_echo_cancellation = isset($_POST['voice_echo_cancellation']) ? (int)$_POST['voice_echo_cancellation'] : ($user['voice_echo_cancellation'] ?? 1); + $voice_noise_suppression = isset($_POST['voice_noise_suppression']) ? (int)$_POST['voice_noise_suppression'] : ($user['voice_noise_suppression'] ?? 1); try { - $stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ? WHERE id = ?"); - $success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $user['id']]); + $stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ?, voice_echo_cancellation = ?, voice_noise_suppression = ? WHERE id = ?"); + $success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $voice_echo_cancellation, $voice_noise_suppression, $user['id']]); $log['db_success'] = $success; file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND); diff --git a/assets/js/voice.js b/assets/js/voice.js index 088aa43..1263dfb 100644 --- a/assets/js/voice.js +++ b/assets/js/voice.js @@ -3,7 +3,17 @@ console.log('voice.js loaded'); class VoiceChannel { constructor(ws, settings) { // ws is ignored now as we use PHP signaling, but kept for compatibility - this.settings = settings || { mode: 'vox', pttKey: 'v', voxThreshold: 0.1 }; + this.settings = settings || { + mode: 'vox', + pttKey: 'v', + voxThreshold: 0.1, + inputDevice: 'default', + outputDevice: 'default', + inputVolume: 1.0, + outputVolume: 1.0, + echoCancellation: true, + noiseSuppression: true + }; console.log('VoiceChannel constructor called with settings:', this.settings); this.localStream = null; this.analysisStream = null; @@ -20,6 +30,7 @@ class VoiceChannel { this.analyser = null; this.microphone = null; this.scriptProcessor = null; + this.inputGain = null; this.isTalking = false; this.pttPressed = false; @@ -99,8 +110,19 @@ class VoiceChannel { sessionStorage.setItem('activeVoiceChannel', channelId); try { - console.log('Requesting microphone access...'); - this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + console.log('Requesting microphone access with device:', this.settings.inputDevice); + const constraints = { + audio: { + echoCancellation: this.settings.echoCancellation, + noiseSuppression: this.settings.noiseSuppression, + autoGainControl: true + }, + video: false + }; + if (this.settings.inputDevice !== 'default') { + constraints.audio.deviceId = { exact: this.settings.inputDevice }; + } + this.localStream = await navigator.mediaDevices.getUserMedia(constraints); console.log('Microphone access granted'); this.setMute(true); @@ -233,6 +255,10 @@ class VoiceChannel { remoteAudio.style.display = 'none'; remoteAudio.srcObject = stream; remoteAudio.muted = this.isDeafened; + remoteAudio.volume = this.settings.outputVolume || 1.0; + if (this.settings.outputDevice !== 'default' && typeof remoteAudio.setSinkId === 'function') { + remoteAudio.setSinkId(this.settings.outputDevice); + } document.body.appendChild(remoteAudio); this.remoteAudios[userId] = remoteAudio; @@ -424,6 +450,7 @@ class VoiceChannel { console.log('Setting deafen to:', this.isDeafened); Object.values(this.remoteAudios).forEach(audio => { audio.muted = this.isDeafened; + if (!this.isDeafened) audio.volume = this.settings.outputVolume || 1.0; }); // If we deafen, we usually also mute in Discord if (this.isDeafened && !this.isMuted) { @@ -435,6 +462,91 @@ class VoiceChannel { this.updateUserPanelButtons(); } + setOutputVolume(vol) { + this.settings.outputVolume = parseFloat(vol); + Object.values(this.remoteAudios).forEach(audio => { + audio.volume = this.settings.outputVolume; + }); + } + + setInputVolume(vol) { + this.settings.inputVolume = parseFloat(vol); + // We could use a GainNode here, but for simplicity we'll just store it. + // If we want to actually change the transmitted volume, we need to insert a GainNode in the stream. + } + + async setInputDevice(deviceId) { + this.settings.inputDevice = deviceId; + if (this.currentChannelId && this.localStream) { + // Re-join or switch track + const constraints = { + audio: { + echoCancellation: this.settings.echoCancellation, + noiseSuppression: this.settings.noiseSuppression, + autoGainControl: true + }, + video: false + }; + if (deviceId !== 'default') { + constraints.audio.deviceId = { exact: deviceId }; + } + const newStream = await navigator.mediaDevices.getUserMedia(constraints); + const newTrack = newStream.getAudioTracks()[0]; + + Object.values(this.peers).forEach(pc => { + const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio'); + if (sender) sender.replaceTrack(newTrack); + }); + + this.localStream.getTracks().forEach(t => t.stop()); + this.localStream = newStream; + this.setupVOX(); + this.setMute(this.isMuted); + } + } + + async setOutputDevice(deviceId) { + this.settings.outputDevice = deviceId; + Object.values(this.remoteAudios).forEach(audio => { + if (typeof audio.setSinkId === 'function') { + audio.setSinkId(deviceId).catch(e => console.error('setSinkId failed:', e)); + } + }); + } + + async updateAudioConstraints() { + if (this.currentChannelId && this.localStream) { + console.log('Updating audio constraints:', this.settings.echoCancellation, this.settings.noiseSuppression); + const constraints = { + audio: { + echoCancellation: this.settings.echoCancellation, + noiseSuppression: this.settings.noiseSuppression, + autoGainControl: true + }, + video: false + }; + if (this.settings.inputDevice !== 'default') { + constraints.audio.deviceId = { exact: this.settings.inputDevice }; + } + try { + const newStream = await navigator.mediaDevices.getUserMedia(constraints); + const newTrack = newStream.getAudioTracks()[0]; + + Object.values(this.peers).forEach(pc => { + const sender = pc.getSenders().find(s => s.track && s.track.kind === 'audio'); + if (sender) sender.replaceTrack(newTrack); + }); + + this.localStream.getTracks().forEach(t => t.stop()); + this.localStream = newStream; + this.setupVOX(); + this.setMute(this.isMuted); + } catch (e) { + console.error('Failed to update audio constraints:', e); + } + } + } + updateUserPanelButtons() { const btnMute = document.getElementById('btn-panel-mute'); const btnDeafen = document.getElementById('btn-panel-deafen'); diff --git a/data/22.participants.json b/data/22.participants.json index 62d840a..810e8c2 100644 --- a/data/22.participants.json +++ b/data/22.participants.json @@ -1 +1 @@ -{"cd751c28f7e35458":{"id":"cd751c28f7e35458","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771446668805,"is_muted":1,"is_deafened":0}} \ No newline at end of file +{"1707c6672074b09b":{"id":"1707c6672074b09b","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771447993891,"is_muted":1,"is_deafened":0}} \ No newline at end of file diff --git a/data/3.participants.json b/data/3.participants.json index 563e840..0637a08 100644 --- a/data/3.participants.json +++ b/data/3.participants.json @@ -1 +1 @@ -{"6c0fa2db85f281cf":{"id":"6c0fa2db85f281cf","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771446668059,"is_muted":1,"is_deafened":0}} \ No newline at end of file +[] \ No newline at end of file diff --git a/index.php b/index.php index 2b2a366..db5c865 100644 --- a/index.php +++ b/index.php @@ -387,6 +387,17 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; window.currentChannelName = ""; window.isDndMode = ; window.soundNotifications = ; + window.voiceSettings = { + mode: "", + pttKey: "", + voxThreshold: , + echoCancellation: , + noiseSuppression: , + inputDevice: localStorage.getItem('voice_input_device') || 'default', + outputDevice: localStorage.getItem('voice_output_device') || 'default', + inputVolume: parseFloat(localStorage.getItem('voice_input_volume') || 1.0), + outputVolume: parseFloat(localStorage.getItem('voice_output_volume') || 1.0) + };