feat: voice mode switcher (near/world) + mob fixes
This commit is contained in:
parent
cc46b93e96
commit
6b6125ae81
97
game.js
97
game.js
|
|
@ -1245,39 +1245,42 @@
|
||||||
let voiceSocket = null;
|
let voiceSocket = null;
|
||||||
let voiceStream = null;
|
let voiceStream = null;
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
let voiceWorklet = null;
|
let voiceProcessor = null;
|
||||||
let voiceActive = false;
|
let voiceActive = false;
|
||||||
|
let voiceMode = 'near'; // 'near' or 'world'
|
||||||
|
let voiceDebugCount = 0;
|
||||||
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
|
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');
|
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.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.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);
|
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');
|
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.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;
|
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.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';
|
voiceBtn.style.background = '#555';
|
||||||
if (voiceWorklet) { voiceWorklet.disconnect(); voiceWorklet = null; }
|
|
||||||
if (voiceStream) {
|
if (voiceStream) {
|
||||||
voiceStream.getTracks().forEach(t => t.stop());
|
voiceStream.getTracks().forEach(t => t.stop());
|
||||||
voiceStream = null;
|
voiceStream = null;
|
||||||
}
|
}
|
||||||
|
if (voiceProcessor) { voiceProcessor.disconnect(); voiceProcessor = null; }
|
||||||
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
if (audioCtx) { audioCtx.close(); audioCtx = null; }
|
||||||
if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; }
|
if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; }
|
||||||
return;
|
return;
|
||||||
|
|
@ -1306,26 +1309,53 @@
|
||||||
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
|
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
|
||||||
audioCtx = new AudioContext({ sampleRate: 24000 });
|
audioCtx = new AudioContext({ sampleRate: 24000 });
|
||||||
if (audioCtx.state === 'suspended') await audioCtx.resume();
|
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);
|
const source = audioCtx.createMediaStreamSource(voiceStream);
|
||||||
voiceWorklet = new AudioWorkletNode(audioCtx, 'voice-pcm');
|
voiceProcessor = audioCtx.createScriptProcessor(2048, 1, 1);
|
||||||
voiceWorklet.port.onmessage = (e) => {
|
console.log('[voice] ScriptProcessor created, bufferSize=2048');
|
||||||
if (!voiceActive || !voiceSocket || !voiceSocket.connected) return;
|
|
||||||
voiceSocket.emit('voice_data', e.data);
|
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 = io(VOICE_SERVER, { transports: ['websocket'] });
|
||||||
voiceSocket.on('connect', () => {
|
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) => {
|
voiceSocket.on('voice_in', (payload) => {
|
||||||
// Воспроизводим входящий голос — raw PCM int16
|
// Воспроизводим входящий голос — raw PCM int16
|
||||||
const { data, meta, volume } = payload;
|
const { data, meta, volume } = payload;
|
||||||
if (!audioCtx || audioCtx.state === 'closed') return;
|
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 int16 = new Int16Array(data);
|
||||||
const float32 = new Float32Array(int16.length);
|
const float32 = new Float32Array(int16.length);
|
||||||
|
|
@ -1351,8 +1381,9 @@
|
||||||
voiceActive = true;
|
voiceActive = true;
|
||||||
voiceBtn.textContent = '🎤';
|
voiceBtn.textContent = '🎤';
|
||||||
voiceBtn.style.background = '#2ecc71';
|
voiceBtn.style.background = '#2ecc71';
|
||||||
|
console.log('[voice] Voice chat ACTIVE');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error('Voice error:', e);
|
console.error('[voice] Error:', e);
|
||||||
voiceBtn.style.background = '#e74c3c';
|
voiceBtn.style.background = '#e74c3c';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="game.js?v=10"></script>
|
<script src="game.js?v=12"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><title>Voice Test</title></head><body>
|
||||||
|
<h1>Voice Capture Test</h1>
|
||||||
|
<button id="btn" style="padding:20px;font-size:24px;background:#2ecc71;color:#fff;border:none;border-radius:12px;cursor:pointer;">Start Mic</button>
|
||||||
|
<div id="log" style="font-family:monospace;white-space:pre;margin-top:20px;"></div>
|
||||||
|
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const log = document.getElementById('log');
|
||||||
|
function addLog(msg) { log.textContent += msg + '\n'; console.log(msg); }
|
||||||
|
|
||||||
|
let voiceStream, audioCtx, voiceProcessor, voiceSocket;
|
||||||
|
let debugCount = 0;
|
||||||
|
|
||||||
|
document.getElementById('btn').onclick = async () => {
|
||||||
|
try {
|
||||||
|
addLog('1. Requesting mic...');
|
||||||
|
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
|
||||||
|
addLog('2. Got stream: ' + voiceStream.getTracks().map(t => t.label + ' ' + t.readyState).join(', '));
|
||||||
|
|
||||||
|
audioCtx = new AudioContext({ sampleRate: 24000 });
|
||||||
|
if (audioCtx.state === 'suspended') await audioCtx.resume();
|
||||||
|
addLog('3. AudioContext: state=' + audioCtx.state + ' sampleRate=' + audioCtx.sampleRate);
|
||||||
|
|
||||||
|
const source = audioCtx.createMediaStreamSource(voiceStream);
|
||||||
|
voiceProcessor = audioCtx.createScriptProcessor(2048, 1, 1);
|
||||||
|
addLog('4. ScriptProcessor created');
|
||||||
|
|
||||||
|
voiceProcessor.onaudioprocess = (e) => {
|
||||||
|
debugCount++;
|
||||||
|
const pcm = e.inputBuffer.getChannelData(0);
|
||||||
|
const maxVal = Math.max(...Array.from(pcm).map(Math.abs));
|
||||||
|
if (debugCount <= 10) addLog('5. onaudioprocess #' + debugCount + ' samples=' + pcm.length + ' max=' + maxVal.toFixed(4));
|
||||||
|
|
||||||
|
if (!voiceSocket || !voiceSocket.connected) return;
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const silentGain = audioCtx.createGain();
|
||||||
|
silentGain.gain.value = 0;
|
||||||
|
source.connect(voiceProcessor);
|
||||||
|
voiceProcessor.connect(silentGain);
|
||||||
|
silentGain.connect(audioCtx.destination);
|
||||||
|
addLog('6. Audio chain connected: source→processor→gain(0)→destination');
|
||||||
|
|
||||||
|
addLog('7. Connecting to voice server...');
|
||||||
|
voiceSocket = io('https://voicegrech.mkn8n.ru', { transports: ['websocket'] });
|
||||||
|
voiceSocket.on('connect', () => {
|
||||||
|
addLog('8. Socket connected: ' + voiceSocket.id);
|
||||||
|
voiceSocket.emit('voice_join', { world_id: 'test', x: 0, y: 0, name: 'Tester' });
|
||||||
|
addLog('9. Sent voice_join. Speak into mic — watch onaudioprocess logs above!');
|
||||||
|
});
|
||||||
|
voiceSocket.on('connect_error', (err) => addLog('ERROR: ' + err.message));
|
||||||
|
|
||||||
|
voiceSocket.on('voice_in', (payload) => {
|
||||||
|
addLog('VOICE_IN from ' + payload.meta.name + ' vol=' + payload.volume + ' bytes=' + payload.data.byteLength);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn').textContent = 'Listening...';
|
||||||
|
document.getElementById('btn').style.background = '#e74c3c';
|
||||||
|
} catch(e) {
|
||||||
|
addLog('ERROR: ' + e.message + '\n' + e.stack);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
Loading…
Reference in New Issue