fix: implement proper audio timing for listen with per-user timelines

- Add audioContextListen and userTimelines to state
- Create AudioContext on listen toggle
- Parse user ID hash from PCM packet header
- Track playback timing per user to prevent audio gaps
- Use proper AudioContext timing instead of Tone.now()
- Clear timelines when toggling listen off
This commit is contained in:
MythEclipse
2026-05-13 22:53:17 +07:00
parent 65dc73e903
commit 94dc460fc7

View File

@@ -12,6 +12,8 @@ const state = {
meter: null, meter: null,
synth: null, synth: null,
nextStartTime: 0, nextStartTime: 0,
audioContextListen: null,
userTimelines: new Map(),
}; };
const SAMPLE_RATE = 24000; const SAMPLE_RATE = 24000;
@@ -403,28 +405,47 @@ function stopStreaming() {
function toggleListen() { function toggleListen() {
state.isListening = !state.isListening; state.isListening = !state.isListening;
if (state.isListening) { if (state.isListening) {
state.audioContextListen = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE });
state.userTimelines.clear();
el.listenBtn.textContent = 'Leave Listen Channel'; el.listenBtn.textContent = 'Leave Listen Channel';
el.listenStatus.textContent = 'speaker on'; el.listenStatus.textContent = 'speaker on';
} else { } else {
state.audioContextListen?.close();
state.audioContextListen = null;
state.userTimelines.clear();
el.listenBtn.textContent = 'Join Listen Channel'; el.listenBtn.textContent = 'Join Listen Channel';
el.listenStatus.textContent = 'speaker off'; el.listenStatus.textContent = 'speaker off';
} }
} }
function playPcm(arrayBuffer) { function playPcm(arrayBuffer) {
if (!state.isListening) return; if (!state.isListening || !state.audioContextListen) return;
const bytes = new Uint8Array(arrayBuffer); const bytes = new Uint8Array(arrayBuffer);
if (bytes.byteLength <= 4) return; if (bytes.byteLength <= 4) return;
const pcm = new Int16Array(bytes.buffer, bytes.byteOffset + 4, (bytes.byteLength - 4) / 2);
const audioBuffer = Tone.context.createBuffer(1, pcm.length, 24000); const headerView = new DataView(arrayBuffer, 0, 4);
const channel = audioBuffer.getChannelData(0); const userIdHash = headerView.getInt32(0, true);
for (let i = 0; i < pcm.length; i++) channel[i] = pcm[i] / 32768; const audioData = arrayBuffer.slice(4);
const source = Tone.context.createBufferSource(); const int16Array = new Int16Array(audioData);
const float32Array = new Float32Array(int16Array.length);
for (let i = 0; i < int16Array.length; i++) float32Array[i] = int16Array[i] / 32768;
const audioBuffer = state.audioContextListen.createBuffer(1, float32Array.length, SAMPLE_RATE);
const channelData = audioBuffer.getChannelData(0);
for (let i = 0; i < float32Array.length; i++) channelData[i] = float32Array[i];
const source = state.audioContextListen.createBufferSource();
source.buffer = audioBuffer; source.buffer = audioBuffer;
source.connect(Tone.context.destination); source.connect(state.audioContextListen.destination);
source.start(Tone.now());
const currentTime = state.audioContextListen.currentTime;
let userNextStartTime = state.userTimelines.get(userIdHash) || 0;
if (userNextStartTime < currentTime) userNextStartTime = currentTime + 0.05;
source.start(userNextStartTime);
userNextStartTime += audioBuffer.duration;
state.userTimelines.set(userIdHash, userNextStartTime);
} }
function updateVisualizer(level) { function updateVisualizer(level) {