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 @@ - +