fix: jitter buffer for voice chat — 120ms delay, continuous scheduling, gain ramp

This commit is contained in:
Mk 2026-05-26 13:34:12 +00:00
parent 2ebb457fc5
commit 233ff02976
3 changed files with 26 additions and 3789 deletions

44
game.js
View File

@ -1343,6 +1343,7 @@ function customConfirm(msg, onYes) {
if (voiceActive) { if (voiceActive) {
// Выключить // Выключить
voiceActive = false; voiceActive = false;
jBufNextTime = 0;
voiceBtn.innerHTML = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>'; voiceBtn.innerHTML = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
voiceBtn.style.background = '#555'; voiceBtn.style.background = '#555';
if (voiceStream) { if (voiceStream) {
@ -1402,35 +1403,40 @@ function customConfirm(msg, onYes) {
console.error('[voice] Socket connect error:', err.message); console.error('[voice] Socket connect error:', err.message);
}); });
// Очередь воспроизведения голоса — склеиваем чанки без щелчков // === Jitter Buffer для голоса ===
const voiceQueue = []; // Накапливаем чанки, затем льём непрерывно через один ScriptProcessor
let voicePlaying = false; const jBuf = []; // очередь { float32, volume }
function playVoiceChunk(float32, volume) { const JBUF_DELAY = 0.12; // начальная задержка 120мс — буфер против джиттера
const FADE = 64; // сэмплов для плавного перехода let jBufNextTime = 0; // когда следующий чанк должен стартовать
// Fade in начало let jBufDrift = 0; // коррекция дрифта
for (let i = 0; i < FADE && i < float32.length; i++) {
float32[i] *= i / FADE; function scheduleVoiceChunk(float32, volume) {
const now = audioCtx.currentTime;
// Первый чанк — добавляем задержку
if (jBufNextTime === 0) {
jBufNextTime = now + JBUF_DELAY;
} }
// Fade out конец // Если чанк пришёл с опозданием (разрыв сети) — не делаем промежуток
for (let i = 0; i < FADE && i < float32.length; i++) { if (jBufNextTime < now) {
float32[float32.length - 1 - i] *= i / FADE; jBufNextTime = now + 0.005; // минимальный зазор 5мс
} }
const buf = audioCtx.createBuffer(1, float32.length, 24000); const buf = audioCtx.createBuffer(1, float32.length, 24000);
buf.getChannelData(0).set(float32); buf.getChannelData(0).set(float32);
const src = audioCtx.createBufferSource(); const src = audioCtx.createBufferSource();
src.buffer = buf; src.buffer = buf;
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
gain.gain.value = Math.min(1, volume * 1.5); // усилить тихий голос 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.connect(gain).connect(audioCtx.destination);
// Склеиваем: начинаем сразу после предыдущего чанка src.start(jBufNextTime);
const when = voicePlaying ? audioCtx.currentTime + 0.02 : audioCtx.currentTime; jBufNextTime += float32.length / 24000; // время звучания этого чанка
voicePlaying = true;
src.start(when);
src.onended = () => { voicePlaying = false; };
} }
voiceSocket.on('voice_in', (payload) => { voiceSocket.on('voice_in', (payload) => {
// Воспроизводим входящий голос — raw PCM int16 // Воспроизводим входящий голос — raw PCM int16 через jitter buffer
const { data, meta, volume } = payload; const { data, meta, volume } = payload;
if (!audioCtx || audioCtx.state === 'closed') return; if (!audioCtx || audioCtx.state === 'closed') return;
@ -1439,7 +1445,7 @@ function customConfirm(msg, onYes) {
for (let i = 0; i < int16.length; i++) { for (let i = 0; i < int16.length; i++) {
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF);
} }
playVoiceChunk(float32, volume || 1); scheduleVoiceChunk(float32, volume || 1);
// Индикатор // Индикатор
speakingIndicator.style.display = 'block'; speakingIndicator.style.display = 'block';

File diff suppressed because it is too large Load Diff

View File

@ -93,6 +93,6 @@
</div> </div>
</div> </div>
<script src="game.js?v=19"></script> <script src="game.js?v=20"></script>
</body> </body>
</html> </html>