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