120 lines
5.0 KiB
JavaScript
120 lines
5.0 KiB
JavaScript
// Голосовой чат
|
|
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 = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
|
|
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 = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
|
|
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 });
|
|
}
|
|
}
|
|
};
|
|
} |