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:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user