From 6b6125ae81efb065ba9869b094c498b321decd3e Mon Sep 17 00:00:00 2001 From: Mk Date: Tue, 26 May 2026 12:51:45 +0000 Subject: [PATCH] feat: voice mode switcher (near/world) + mob fixes --- game.js | 97 ++++++++++++++++++++++++++++++++----------------- index.html | 2 +- voice-test.html | 70 +++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 voice-test.html diff --git a/game.js b/game.js index b73196c..a3e37ce 100644 --- a/game.js +++ b/game.js @@ -1245,39 +1245,42 @@ let voiceSocket = null; let voiceStream = null; let audioCtx = null; - let voiceWorklet = null; + let voiceProcessor = null; let voiceActive = false; + let voiceMode = 'near'; // 'near' or 'world' + let voiceDebugCount = 0; const VOICE_SERVER = 'https://voicegrech.mkn8n.ru'; - // AudioWorklet processor as Blob URL — no separate file needed - const workletCode = ` - class VoiceProcessor extends AudioWorkletProcessor { - process(inputs) { - const ch = inputs[0]; - if (ch && ch[0]) { - const pcm = ch[0]; - const int16 = new Int16Array(pcm.length); - for (let i = 0; i < pcm.length; i++) { - const s = Math.max(-1, Math.min(1, pcm[i])); - int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; - } - this.port.postMessage(int16.buffer, [int16.buffer]); - } - return true; - } - } - registerProcessor('voice-pcm', VoiceProcessor); - `; - const workletBlob = new Blob([workletCode], { type: 'application/javascript' }); - const workletUrl = URL.createObjectURL(workletBlob); - // Кнопка микрофона const voiceBtn = document.createElement('div'); voiceBtn.innerHTML = '🎤/'; voiceBtn.title = 'Голосовой чат (выкл)'; - voiceBtn.style.cssText = 'position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; + voiceBtn.style.cssText = 'position:absolute;top:74px;right:170px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; document.querySelector('.ui').appendChild(voiceBtn); + // Кнопка режима голоса (близко / весь мир) + const voiceModeBtn = document.createElement('div'); + voiceModeBtn.innerHTML = '📢'; + voiceModeBtn.title = 'Режим: рядом (600px)'; + voiceModeBtn.style.cssText = 'position:absolute;top:74px;right:112px;width:48px;height:52px;border-radius:12px;background:#3498db;z-index:200;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;border:2px solid rgba(255,255,255,0.7);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;color:#fff;font-weight:bold;'; + document.querySelector('.ui').appendChild(voiceModeBtn); + voiceModeBtn.onclick = () => { + if (voiceMode === 'near') { + voiceMode = 'world'; + voiceModeBtn.innerHTML = '🌍'; + voiceModeBtn.title = 'Режим: весь мир'; + voiceModeBtn.style.background = '#e67e22'; + } else { + voiceMode = 'near'; + voiceModeBtn.innerHTML = '📢'; + voiceModeBtn.title = 'Режим: рядом (600px)'; + voiceModeBtn.style.background = '#3498db'; + } + if (voiceSocket && voiceSocket.connected) { + voiceSocket.emit('voice_mode', { mode: voiceMode }); + } + }; + // Индикатор говорящего const speakingIndicator = document.createElement('div'); speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;'; @@ -1291,11 +1294,11 @@ voiceActive = false; voiceBtn.innerHTML = '🎤/'; voiceBtn.style.background = '#555'; - if (voiceWorklet) { voiceWorklet.disconnect(); voiceWorklet = null; } if (voiceStream) { voiceStream.getTracks().forEach(t => t.stop()); voiceStream = null; } + if (voiceProcessor) { voiceProcessor.disconnect(); voiceProcessor = null; } if (audioCtx) { audioCtx.close(); audioCtx = null; } if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; } return; @@ -1306,26 +1309,53 @@ voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } }); audioCtx = new AudioContext({ sampleRate: 24000 }); if (audioCtx.state === 'suspended') await audioCtx.resume(); + console.log('[voice] AudioContext state:', audioCtx.state, 'sampleRate:', audioCtx.sampleRate); - await audioCtx.audioWorklet.addModule(workletUrl); const source = audioCtx.createMediaStreamSource(voiceStream); - voiceWorklet = new AudioWorkletNode(audioCtx, 'voice-pcm'); - voiceWorklet.port.onmessage = (e) => { - if (!voiceActive || !voiceSocket || !voiceSocket.connected) return; - voiceSocket.emit('voice_data', e.data); + voiceProcessor = audioCtx.createScriptProcessor(2048, 1, 1); + console.log('[voice] ScriptProcessor created, bufferSize=2048'); + + voiceProcessor.onaudioprocess = (e) => { + if (!voiceActive) return; + voiceDebugCount++; + if (voiceDebugCount <= 5) { + const pcm = e.inputBuffer.getChannelData(0); + console.log('[voice] onaudioprocess #' + voiceDebugCount, 'samples:', pcm.length, 'max:', Math.max(...pcm.map(Math.abs)).toFixed(4), 'socket:', !!voiceSocket, 'connected:', voiceSocket?.connected); + } + if (!voiceSocket || !voiceSocket.connected) return; + const pcm = e.inputBuffer.getChannelData(0); + const int16 = new Int16Array(pcm.length); + for (let i = 0; i < pcm.length; i++) { + const s = Math.max(-1, Math.min(1, pcm[i])); + int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + voiceSocket.emit('voice_data', int16.buffer); }; - source.connect(voiceWorklet); + + // Chain: source → processor → gain(0) → destination + // ScriptProcessor MUST reach destination to fire onaudioprocess + const silentGain = audioCtx.createGain(); + silentGain.gain.value = 0; + source.connect(voiceProcessor); + voiceProcessor.connect(silentGain); + silentGain.connect(audioCtx.destination); + console.log('[voice] Audio chain: source → processor → silentGain(0) → destination'); // Подключаемся к голосовому серверу voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); voiceSocket.on('connect', () => { - voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'Игрок' }); + console.log('[voice] Socket connected, id:', voiceSocket.id); + voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'Игрок', mode: voiceMode }); + }); + voiceSocket.on('connect_error', (err) => { + console.error('[voice] Socket connect error:', err.message); }); voiceSocket.on('voice_in', (payload) => { // Воспроизводим входящий голос — raw PCM int16 const { data, meta, volume } = payload; if (!audioCtx || audioCtx.state === 'closed') return; + console.log('[voice] voice_in from', meta.name, 'volume:', volume, 'bytes:', data.byteLength); const int16 = new Int16Array(data); const float32 = new Float32Array(int16.length); @@ -1351,8 +1381,9 @@ voiceActive = true; voiceBtn.textContent = '🎤'; voiceBtn.style.background = '#2ecc71'; + console.log('[voice] Voice chat ACTIVE'); } catch(e) { - console.error('Voice error:', e); + console.error('[voice] Error:', e); voiceBtn.style.background = '#e74c3c'; } }; diff --git a/index.html b/index.html index 01c2bce..cffd717 100644 --- a/index.html +++ b/index.html @@ -92,6 +92,6 @@ - + diff --git a/voice-test.html b/voice-test.html new file mode 100644 index 0000000..887203d --- /dev/null +++ b/voice-test.html @@ -0,0 +1,70 @@ + +Voice Test +

Voice Capture Test

+ +
+ + +