fix: ring buffer voice playback — continuous stream, no BufferSource per chunk
This commit is contained in:
parent
233ff02976
commit
0e12ed7a18
94
game.js
94
game.js
|
|
@ -1343,7 +1343,7 @@ function customConfirm(msg, onYes) {
|
||||||
if (voiceActive) {
|
if (voiceActive) {
|
||||||
// Выключить
|
// Выключить
|
||||||
voiceActive = false;
|
voiceActive = false;
|
||||||
jBufNextTime = 0;
|
ringReady = 0; ringRead = ringWrite; voicePlayActive = false;
|
||||||
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) {
|
||||||
|
|
@ -1403,55 +1403,77 @@ function customConfirm(msg, onYes) {
|
||||||
console.error('[voice] Socket connect error:', err.message);
|
console.error('[voice] Socket connect error:', err.message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// === Jitter Buffer для голоса ===
|
// === Ring Buffer + ScriptProcessor приём голоса ===
|
||||||
// Накапливаем чанки, затем льём непрерывно через один ScriptProcessor
|
// Единый непрерывный поток вместо отдельных BufferSource на чанк
|
||||||
const jBuf = []; // очередь { float32, volume }
|
const RING_SIZE = 24000 * 3; // 3 секунды ring buffer
|
||||||
const JBUF_DELAY = 0.12; // начальная задержка 120мс — буфер против джиттера
|
const ringBuf = new Float32Array(RING_SIZE);
|
||||||
let jBufNextTime = 0; // когда следующий чанк должен стартовать
|
let ringWrite = 0; // позиция записи
|
||||||
let jBufDrift = 0; // коррекция дрифта
|
let ringRead = 0; // позиция чтения
|
||||||
|
let ringReady = 0; // сколько сэмплов готово
|
||||||
function scheduleVoiceChunk(float32, volume) {
|
let voicePlayActive = false;
|
||||||
const now = audioCtx.currentTime;
|
const JBUF_TARGET = 2400; // целевой jitter buffer: 100мс при 24kHz
|
||||||
// Первый чанк — добавляем задержку
|
let jbufFill = 0; // текущее заполнение
|
||||||
if (jBufNextTime === 0) {
|
let lastVoiceFrom = ''; // кто говорит (для индикатора)
|
||||||
jBufNextTime = now + JBUF_DELAY;
|
|
||||||
|
// Воспроизводящий 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;
|
||||||
}
|
}
|
||||||
// Если чанк пришёл с опозданием (разрыв сети) — не делаем промежуток
|
// Ждём накопления jitter buffer перед стартом
|
||||||
if (jBufNextTime < now) {
|
if (!voicePlayActive && ringReady >= JBUF_TARGET) {
|
||||||
jBufNextTime = now + 0.005; // минимальный зазор 5мс
|
voicePlayActive = true;
|
||||||
}
|
}
|
||||||
const buf = audioCtx.createBuffer(1, float32.length, 24000);
|
if (!voicePlayActive) {
|
||||||
buf.getChannelData(0).set(float32);
|
out.fill(0);
|
||||||
const src = audioCtx.createBufferSource();
|
return;
|
||||||
src.buffer = buf;
|
}
|
||||||
const gain = audioCtx.createGain();
|
// Читаем из ring buffer
|
||||||
const vol = Math.min(1.4, volume * 1.5); // усиление, макс 1.4
|
for (let i = 0; i < out.length; i++) {
|
||||||
gain.gain.setValueAtTime(vol, jBufNextTime);
|
if (ringReady > 0) {
|
||||||
// Лёгкий fade в конце чанка (последние 128 сэмплов) для склейки
|
out[i] = ringBuf[ringRead];
|
||||||
gain.gain.linearRampToValueAtTime(vol * 0.3, jBufNextTime + float32.length / 24000 - 0.005);
|
ringRead = (ringRead + 1) % RING_SIZE;
|
||||||
gain.gain.linearRampToValueAtTime(0, jBufNextTime + float32.length / 24000);
|
ringReady--;
|
||||||
src.connect(gain).connect(audioCtx.destination);
|
} else {
|
||||||
src.start(jBufNextTime);
|
out[i] = 0;
|
||||||
jBufNextTime += float32.length / 24000; // время звучания этого чанка
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
const playGain = audioCtx.createGain();
|
||||||
|
playGain.gain.value = 1.0;
|
||||||
|
playProcessor.connect(playGain).connect(audioCtx.destination);
|
||||||
|
|
||||||
voiceSocket.on('voice_in', (payload) => {
|
voiceSocket.on('voice_in', (payload) => {
|
||||||
// Воспроизводим входящий голос — raw PCM int16 через jitter buffer
|
// Пишем входящий голос в ring buffer
|
||||||
const { data, meta, volume } = payload;
|
const { data, meta, volume } = payload;
|
||||||
if (!audioCtx || audioCtx.state === 'closed') return;
|
if (!audioCtx || audioCtx.state === 'closed') return;
|
||||||
|
|
||||||
const int16 = new Int16Array(data);
|
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++) {
|
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.style.display = 'block';
|
||||||
speakingIndicator.textContent = '🔊 ' + (meta.name || '???');
|
speakingIndicator.textContent = '🔊 ' + lastVoiceFrom;
|
||||||
clearTimeout(speakingTimeout);
|
clearTimeout(speakingTimeout);
|
||||||
speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500);
|
speakingTimeout = setTimeout(() => {
|
||||||
|
speakingIndicator.style.display = 'none';
|
||||||
|
voicePlayActive = false; // сброс при паузе
|
||||||
|
ringReady = 0; // очистить буфер
|
||||||
|
ringRead = ringWrite; // синхронизировать
|
||||||
|
}, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
voiceActive = true;
|
voiceActive = true;
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="game.js?v=20"></script>
|
<script src="game.js?v=21"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue