diff --git a/game.js b/game.js
index d4a42b0..2a4eb80 100644
--- a/game.js
+++ b/game.js
@@ -1245,7 +1245,7 @@
let voiceSocket = null;
let voiceStream = null;
let audioCtx = null;
- let voiceProcessor = null;
+ let voiceRecorder = null;
let voiceActive = false;
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
@@ -1269,11 +1269,11 @@
voiceActive = false;
voiceBtn.innerHTML = 'π€/';
voiceBtn.style.background = '#555';
+ if (voiceRecorder) { voiceRecorder.stop(); voiceRecorder = 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;
@@ -1281,30 +1281,8 @@
// ΠΠΊΠ»ΡΡΠΈΡΡ
try {
- voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24000 } });
+ voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
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);
- // ScriptProcessor MUST connect to destination to fire onaudioprocess events
- // Use a zero-gain node to silence own playback while keeping the processor active
- const silentGain = audioCtx.createGain();
- silentGain.gain.value = 0; // mute β don't hear ourselves
- voiceProcessor.connect(silentGain);
- silentGain.connect(audioCtx.destination);
// ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π΅ΠΌΡΡ ΠΊ Π³ΠΎΠ»ΠΎΡΠΎΠ²ΠΎΠΌΡ ΡΠ΅ΡΠ²Π΅ΡΡ
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
@@ -1312,35 +1290,68 @@
voiceSocket.emit('voice_join', { world_id: worldId, x: player.x, y: player.y, name: playerName || 'ΠΠ³ΡΠΎΠΊ' });
});
- voiceSocket.on('voice_in', (payload) => {
- // ΠΠΎΡΠΏΡΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌ Π²Ρ
ΠΎΠ΄ΡΡΠΈΠΉ Π³ΠΎΠ»ΠΎΡ
+ voiceSocket.on('voice_in', async (payload) => {
+ // ΠΠΎΡΠΏΡΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΠΌ Π²Ρ
ΠΎΠ΄ΡΡΠΈΠΉ Π³ΠΎΠ»ΠΎΡ (WebM/Opus or raw PCM)
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;
+ if (!audioCtx || audioCtx.state === 'closed') {
+ audioCtx = new AudioContext({ sampleRate: 24000 });
}
+ if (audioCtx.state === 'suspended') await audioCtx.resume();
- 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();
+ 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 */ }
+ }
// ΠΠ½Π΄ΠΈΠΊΠ°ΡΠΎΡ
speakingIndicator.style.display = 'block';
- speakingIndicator.textContent = `π ${meta.name}`;
+ speakingIndicator.textContent = 'π ' + (meta.name || '???');
clearTimeout(speakingTimeout);
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 9a6d3e8..5a090a1 100644
--- a/index.html
+++ b/index.html
@@ -92,6 +92,6 @@
-
+