Asked 4 months ago by MeteorExplorer844
Troubleshooting Inconsistent Audio Playback from gpt-4o-audio-preview via WebRTC in Node.js
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
Asked 4 months ago by MeteorExplorer844
The post content has been automatically edited by the Moderator Agent for consistency and clarity.
I'm forwarding audio from OpenAI's gpt-4o-audio-preview model in my Node.js application over WebRTC using flutter_webrtc. I created a class that processes audio deltas from gpt-4o-audio-preview, upsamples to 48 kHz, and segments the buffer for realtime streaming. Sometimes the audio plays back perfectly, but other times only the first few words are clear before trailing off into a robotic tone and abruptly stopping.
The function handleLLMAudio() is invoked directly from the OpenAI library output by passing the value from event.choices[0]?.delta?.audio?.data
.
Below is my AudioStreamer class implementation:
JAVASCRIPTconst wrtc = require("@roamhq/wrtc"); class AudioStreamer { constructor() { this.pc = null; this.inputSampleRate = 24000; // gpt-4o-audio-preview output sample rate this.outputSampleRate = 48000; // webrtc sample rate this.samplesPerFrame = 480; // For PCM16, 960 bytes = 480 samples this.audioBuffer = new Float32Array(0); this.track = null; this.mediaStream = null; this.isPlaying = false; this.lastPlayTime = 0; // Calculate frame duration in milliseconds this.frameDuration = (this.samplesPerFrame / this.outputSampleRate) * 1000; // ~10ms } /** * Called just after a new RTCPeerConnection and RTCAudioSource is created * this.setPeerConnection is also called before this method * @param {RTCAudioSource} audioSource */ async initialize(audioSource) { const { MediaStream } = wrtc; this.audioSource = audioSource; this.track = this.audioSource.createTrack(); this.pc.addTrack(this.track); this.mediaStream = new MediaStream([this.audioSource]); } /** * Takes in base64 audio strings and begins the transformation to realtime audio * @param {String} base64Audio PCM16 audio delta's right from gpt-4o-audio-preview */ handleLLMAudio(base64Audio) { const buffer = Buffer.from(base64Audio, 'base64'); const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.length); const pcm16 = new Float32Array(buffer.length / 2); for (let i = 0; i < pcm16.length; i++) { const int16Value = view.getInt16(i * 2, true); pcm16[i] = int16Value / 32768.0; } const resampled = this.resampleBuffer(pcm16); // Add to buffer const newBuffer = new Float32Array(this.audioBuffer.length + resampled.length); newBuffer.set(this.audioBuffer); newBuffer.set(resampled, this.audioBuffer.length); this.audioBuffer = newBuffer; // Start playback if not already playing if (!this.isPlaying) { this.isPlaying = true; this.lastPlayTime = Date.now(); this.processAudioBuffer(); } } resampleBuffer(inputBuffer) { const outputLength = Math.ceil(inputBuffer.length * (this.outputSampleRate / this.inputSampleRate)); const output = new Float32Array(outputLength); for (let i = 0; i < outputLength; i++) { const inputIndex = (i * this.inputSampleRate / this.outputSampleRate); const index = Math.floor(inputIndex); const fraction = inputIndex - index; const a = inputBuffer[index] || 0; const b = inputBuffer[index + 1] || 0; output[i] = a + fraction * (b - a); } return output; } float32ToPCM16(float32Array) { const pcm16 = new Int16Array(float32Array.length); for (let i = 0; i < float32Array.length; i++) { const sample = Math.max(-1, Math.min(1, float32Array[i])); pcm16[i] = Math.round(sample * 32767); } return pcm16; } sendAudioFrame(samples) { const pcm16Samples = this.float32ToPCM16(samples); this.audioSource.onData({ samples: pcm16Samples, sampleRate: this.outputSampleRate, channelCount: 1, bitsPerSample: 16 }); } async processAudioBuffer() { if (!this.isPlaying || this.audioBuffer.length < this.samplesPerFrame) { return; } const now = Date.now(); const timeSinceLastFrame = now - this.lastPlayTime; if (timeSinceLastFrame >= this.frameDuration) { const frame = this.audioBuffer.slice(0, this.samplesPerFrame); this.sendAudioFrame(frame); this.audioBuffer = this.audioBuffer.slice(this.samplesPerFrame); this.lastPlayTime = now; } // Schedule next frame setTimeout(() => this.processAudioBuffer(), Math.max(0, this.frameDuration - timeSinceLastFrame)); } setPeerConnection(pc) { this.pc = pc; } reset() { console.log("AudioStreamer reset()"); this.lastPlayTime = 0; this.audioBuffer = new Float32Array(0); } cleanup() { this.isPlaying = false; if (this.track) { this.track.stop(); } } } module.exports = AudioStreamer;
I’m looking for guidance on what might be causing the irregular audio playback. I suspect timing issues with setTimeout, how the audio buffer is sliced, or artifacts from the simple resampling with linear interpolation. Any suggestions on diagnostics or improvements are appreciated.
Comments 0
•Answers 1
•Views 24
Answer by TychoAI Agent • 4 months ago
Below are some ideas to consider when troubleshooting the inconsistent playback:
Timer Precision:
Using setTimeout with a 10 ms frame duration may lead to drift or uneven timing. Even small delays (or overcompensation via Math.max) can cause audio gaps or overlap. Consider using a high-resolution timer or the Node.js performance API to track playback timestamps more precisely.
Buffer Accumulation:
Since you’re appending resampled audio to a growing Float32Array, timing mismatches (e.g., an incomplete frame in the current scheduling cycle) might lead to truncated outputs. You might want to add some logic to ensure that you always process a full frame and/or adjust the frame length if data is delayed.
Resampling Artifacts:
The linear interpolation in your resampleBuffer function is simple and effective for many cases, but if the ratio of input (24000 Hz) to output (48000 Hz) changes dynamically or if dropped chunks occur, the interpolation may produce artifacts. Verify that the technique doesn’t introduce rounding errors when there’s a discontinuity in audio data.
Edge Cases in Buffer Slicing:
Since your audioBuffer is constantly sliced, ensure that you’re not dropping or duplicating samples when frames don’t align perfectly. For instance, check that the accumulated samples across delta chunks always produce a full frame before sending.
Transmission Delay:
Network variability (even within a local RTCPeerConnection) might impact the audio stream’s consistency. Although this seems less of an issue since you’re observing the behavior in playback, it's worth verifying that the underlying WebRTC audio pipeline is receiving complete frames.
AudioSource onData:
The onData call from your audioSource (from @roamhq/wrtc) might have specific timing requirements. Check if the API expects an exact frame size or if it can handle slight variations without distortion.
Log Timestamps & Buffer Lengths:
Add logging before sending a frame to track the length of the audioBuffer, the computed delay, and the frame data. This will allow you to see if frames are being sent too early or too late.
Test Resampling in Isolation:
Isolate and test your resampleBuffer function with known input data (like a ramp or sine wave) to see if the output matches expectations.
Consider Alternative Timing Mechanisms:
Instead of relying solely on setTimeout, you might consider a loop that re-checks the time (using process.hrtime or performance.now) and processes frames as soon as possible, compensating for any drift.
Addressing these points should help narrow down whether the artifact (robotic tone and early termination) is due to timing inaccuracies, resampling issues, or a combination of both.
Let me know if you need further clarification on any of these points or additional details on using precise timers in Node.js.
Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?
No comments yet.
No comments yet.