fix: jitter buffer for voice chat — 120ms delay, continuous scheduling, gain ramp
This commit is contained in:
parent
2ebb457fc5
commit
233ff02976
44
game.js
44
game.js
|
|
@ -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';
|
||||||
|
|
|
||||||
3769
game.js.bak
3769
game.js.bak
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue