diff --git a/game.js b/game.js index 3c035d9..6f82f1a 100644 --- a/game.js +++ b/game.js @@ -1343,7 +1343,7 @@ function customConfirm(msg, onYes) { if (voiceActive) { // Выключить voiceActive = false; - jBufNextTime = 0; + ringReady = 0; ringRead = ringWrite; voicePlayActive = false; voiceBtn.innerHTML = '🎤/'; voiceBtn.style.background = '#555'; if (voiceStream) { @@ -1403,55 +1403,77 @@ function customConfirm(msg, onYes) { console.error('[voice] Socket connect error:', err.message); }); - // === Jitter Buffer для голоса === - // Накапливаем чанки, затем льём непрерывно через один ScriptProcessor - const jBuf = []; // очередь { float32, volume } - const JBUF_DELAY = 0.12; // начальная задержка 120мс — буфер против джиттера - let jBufNextTime = 0; // когда следующий чанк должен стартовать - let jBufDrift = 0; // коррекция дрифта - - function scheduleVoiceChunk(float32, volume) { - const now = audioCtx.currentTime; - // Первый чанк — добавляем задержку - if (jBufNextTime === 0) { - jBufNextTime = now + JBUF_DELAY; + // === Ring Buffer + ScriptProcessor приём голоса === + // Единый непрерывный поток вместо отдельных BufferSource на чанк + const RING_SIZE = 24000 * 3; // 3 секунды ring buffer + const ringBuf = new Float32Array(RING_SIZE); + let ringWrite = 0; // позиция записи + let ringRead = 0; // позиция чтения + let ringReady = 0; // сколько сэмплов готово + let voicePlayActive = false; + const JBUF_TARGET = 2400; // целевой jitter buffer: 100мс при 24kHz + let jbufFill = 0; // текущее заполнение + let lastVoiceFrom = ''; // кто говорит (для индикатора) + + // Воспроизводящий ScriptProcessor — читает из ring buffer + const playProcessor = audioCtx.createScriptProcessor(2048, 1, 1); + playProcessor.onaudioprocess = (e) => { + const out = e.outputBuffer.getChannelData(0); + if (ringReady < 1) { + // Тишина — нет голоса + out.fill(0); + return; } - // Если чанк пришёл с опозданием (разрыв сети) — не делаем промежуток - if (jBufNextTime < now) { - jBufNextTime = now + 0.005; // минимальный зазор 5мс + // Ждём накопления jitter buffer перед стартом + if (!voicePlayActive && ringReady >= JBUF_TARGET) { + voicePlayActive = true; } - const buf = audioCtx.createBuffer(1, float32.length, 24000); - buf.getChannelData(0).set(float32); - const src = audioCtx.createBufferSource(); - src.buffer = buf; - const gain = audioCtx.createGain(); - const vol = Math.min(1.4, volume * 1.5); // усиление, макс 1.4 - gain.gain.setValueAtTime(vol, jBufNextTime); - // Лёгкий fade в конце чанка (последние 128 сэмплов) для склейки - gain.gain.linearRampToValueAtTime(vol * 0.3, jBufNextTime + float32.length / 24000 - 0.005); - gain.gain.linearRampToValueAtTime(0, jBufNextTime + float32.length / 24000); - src.connect(gain).connect(audioCtx.destination); - src.start(jBufNextTime); - jBufNextTime += float32.length / 24000; // время звучания этого чанка - } + if (!voicePlayActive) { + out.fill(0); + return; + } + // Читаем из ring buffer + for (let i = 0; i < out.length; i++) { + if (ringReady > 0) { + out[i] = ringBuf[ringRead]; + ringRead = (ringRead + 1) % RING_SIZE; + ringReady--; + } else { + out[i] = 0; + } + } + }; + const playGain = audioCtx.createGain(); + playGain.gain.value = 1.0; + playProcessor.connect(playGain).connect(audioCtx.destination); voiceSocket.on('voice_in', (payload) => { - // Воспроизводим входящий голос — raw PCM int16 через jitter buffer + // Пишем входящий голос в ring buffer const { data, meta, volume } = payload; if (!audioCtx || audioCtx.state === 'closed') return; const int16 = new Int16Array(data); - const float32 = new Float32Array(int16.length); + const vol = Math.min(1.4, (volume || 1) * 1.5); for (let i = 0; i < int16.length; i++) { - float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); + const sample = (int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF)) * vol; + ringBuf[ringWrite] = sample; + ringWrite = (ringWrite + 1) % RING_SIZE; + ringReady = Math.min(ringReady + 1, RING_SIZE); } - scheduleVoiceChunk(float32, volume || 1); + // Сброс jitter fill если пауза была + jbufFill = ringReady; + lastVoiceFrom = meta.name || '???'; // Индикатор speakingIndicator.style.display = 'block'; - speakingIndicator.textContent = '🔊 ' + (meta.name || '???'); + speakingIndicator.textContent = '🔊 ' + lastVoiceFrom; clearTimeout(speakingTimeout); - speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500); + speakingTimeout = setTimeout(() => { + speakingIndicator.style.display = 'none'; + voicePlayActive = false; // сброс при паузе + ringReady = 0; // очистить буфер + ringRead = ringWrite; // синхронизировать + }, 600); }); voiceActive = true; diff --git a/index.html b/index.html index 1905551..422a27b 100644 --- a/index.html +++ b/index.html @@ -93,6 +93,6 @@ - +