diff --git a/game.js b/game.js
index 2a4eb80..b73196c 100644
--- a/game.js
+++ b/game.js
@@ -1245,10 +1245,32 @@
let voiceSocket = null;
let voiceStream = null;
let audioCtx = null;
- let voiceRecorder = null;
+ let voiceWorklet = null;
let voiceActive = false;
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 = '🎤/';
@@ -1269,7 +1291,7 @@
voiceActive = false;
voiceBtn.innerHTML = '🎤/';
voiceBtn.style.background = '#555';
- if (voiceRecorder) { voiceRecorder.stop(); voiceRecorder = null; }
+ if (voiceWorklet) { voiceWorklet.disconnect(); voiceWorklet = null; }
if (voiceStream) {
voiceStream.getTracks().forEach(t => t.stop());
voiceStream = null;
@@ -1283,6 +1305,16 @@
try {
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
audioCtx = new AudioContext({ sampleRate: 24000 });
+ if (audioCtx.state === 'suspended') await audioCtx.resume();
+
+ 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);
+ };
+ source.connect(voiceWorklet);
// Подключаемся к голосовому серверу
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
@@ -1290,43 +1322,24 @@
voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'Игрок' });
});
- voiceSocket.on('voice_in', async (payload) => {
- // Воспроизводим входящий голос (WebM/Opus or raw PCM)
+ voiceSocket.on('voice_in', (payload) => {
+ // Воспроизводим входящий голос — raw PCM int16
const { data, meta, volume } = payload;
- if (!audioCtx || audioCtx.state === 'closed') {
- audioCtx = new AudioContext({ sampleRate: 24000 });
- }
- if (audioCtx.state === 'suspended') await audioCtx.resume();
+ if (!audioCtx || audioCtx.state === 'closed') return;
- try {
- // Try WebM/Opus decode first (if server relayed MediaRecorder output)
- const blob = new Blob([data], { type: 'audio/webm; codecs=opus' });
- const arrayBuf = await blob.arrayBuffer();
- const decoded = await audioCtx.decodeAudioData(arrayBuf);
- const src = audioCtx.createBufferSource();
- src.buffer = decoded;
- const gain = audioCtx.createGain();
- gain.gain.value = volume || 1;
- src.connect(gain).connect(audioCtx.destination);
- src.start();
- } catch(_) {
- // Fallback: raw PCM int16 → float32
- try {
- 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);
- }
- 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 || 1;
- src.connect(gain).connect(audioCtx.destination);
- src.start();
- } catch(e2) { /* ignore decode errors */ }
+ 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 || 1);
}
+ 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 = 1;
+ src.connect(gain).connect(audioCtx.destination);
+ src.start();
// Индикатор
speakingIndicator.style.display = 'block';
@@ -1335,23 +1348,6 @@
speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500);
});
- // MediaRecorder — moderne API, works in all browsers without ScriptProcessor
- const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
- ? 'audio/webm;codecs=opus' : 'audio/webm';
- voiceRecorder = new MediaRecorder(voiceStream, {
- mimeType,
- audioBitsPerSecond: 16000
- });
- voiceRecorder.ondataavailable = (e) => {
- if (!voiceActive || !voiceSocket || !voiceSocket.connected) return;
- if (e.data.size > 0) {
- e.data.arrayBuffer().then(buf => {
- voiceSocket.emit('voice_data', buf);
- });
- }
- };
- voiceRecorder.start(100); // 100ms chunks for low latency
-
voiceActive = true;
voiceBtn.textContent = '🎤';
voiceBtn.style.background = '#2ecc71';
diff --git a/index.html b/index.html
index 5a090a1..01c2bce 100644
--- a/index.html
+++ b/index.html
@@ -92,6 +92,6 @@
-
+