// Голосовой чат import { state } from '../core/state.js'; import { worldId } from '../config.js'; let voiceSocket = null; let voiceStream = null; let audioCtx = null; let voiceProcessor = null; let voiceActive = false; const VOICE_SERVER = 'https://voicegrech.mkn8n.ru'; // Кнопка микрофона 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;'; document.querySelector('.ui').appendChild(voiceBtn); // Индикатор говорящего 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;'; speakingIndicator.textContent = '🔊'; document.querySelector('.ui').appendChild(speakingIndicator); let speakingTimeout = null; voiceBtn.onclick = async () => { if (voiceActive) { // Выключить voiceActive = false; voiceBtn.innerHTML = '🎤/'; voiceBtn.style.background = '#555'; 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; } // Включить try { voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24000 } }); audioCtx = new AudioContext({ sampleRate: 24000 }); const source = audioCtx.createMediaStreamSource(voiceStream); voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1); voiceProcessor.onaudioprocess = (e) => { if (!voiceActive || !voiceSocket || !voiceSocket.connected) return; const pcm = e.inputBuffer.getChannelData(0); // Конвертируем float32 → int16 для экономии трафика 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(voiceProcessor); voiceProcessor.connect(audioCtx.createGain()); // не в destination — чтобы не слышать себя // Подключаемся к голосовому серверу voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); voiceSocket.on('connect', () => { voiceSocket.emit('voice_join', { world_id: worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' }); }); voiceSocket.on('voice_in', (payload) => { // Воспроизводим входящий голос const { data, meta, volume } = payload; if (!audioCtx || audioCtx.state === 'closed') return; // Int16 → Float32 const int16 = new Int16Array(data); const float32 = new Float32Array(int16.length); for (let i = 0; i < int16.length; i++) { float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF) * volume; } const buf = audioCtx.createBuffer(1, float32.length, 24000); buf.getChannelData(0).set(float32); const src = audioCtx.createBufferSource(); src.buffer = buf; const gain = audioCtx.createGain(); gain.gain.value = volume; src.connect(gain).connect(audioCtx.destination); src.start(); // Индикатор speakingIndicator.style.display = 'block'; speakingIndicator.textContent = `🔊 ${meta.name}`; clearTimeout(speakingTimeout); speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500); }); voiceActive = true; voiceBtn.textContent = '🎤'; voiceBtn.style.background = '#2ecc71'; } catch (e) { console.error('Voice error:', e); voiceBtn.style.background = '#e74c3c'; } }; export function initVoice() { // Voice position update hook — should be called from main loop let voicePosT = 0; return { update(dt) { voicePosT += dt; if (voicePosT > 0.5 && voiceSocket && voiceSocket.connected) { voicePosT = 0; voiceSocket.emit('voice_pos', { x: state.player.x, y: state.player.y }); } } }; }