Poor audio quality when processing live PCM data f...
# support
g
Hi Vapi Support Team, I'm trying to play back the live audio data that I'm processing via a websocket connection using the
listenUrl
. I'm able to successfully process the data without any issues and I'm attempting to play it back using the Web Audio API. The playback audio is somewhat audible and the voices don't sound distorted or slow/fast, but the quality is poor and it almost sounds muffled (like there's static or something). Any examples, suggestions or guidance would be much appreciated! I've looked through the documentation and the discord tickets and haven't found anything that is helpful for my situation. Below is a snippet of my code:
Copy code
wsRef.current.onmessage = async (event) => {
        // Check if it's binary data (PCM audio)
        if (event.data instanceof ArrayBuffer) {
          try {
            // Convert PCM (16-bit signed) to Float32 immediately
            const int16Array = new Int16Array(event.data);
            const float32Array = new Float32Array(int16Array.length);
            for (let i = 0; i < int16Array.length; i++) {
              float32Array[i] = int16Array[i] / 32768.0;
            }

            // Create audio buffer - 16kHz stereo
            // VAPI Channel mapping: Left = Customer, Right = Assistant TTS
            const sampleRate = 16000;
            const channels = 2; // Stereo
            const framesPerChannel = float32Array.length / 2; // Divide by 2 because input is stereo interleaved

            const audioBuffer = audioContextRef.current.createBuffer(
              channels,
              framesPerChannel,
              sampleRate
            );

            // De-interleave stereo channels
            ...

            // Play immediately
            const source = audioContextRef.current.createBufferSource();
            source.buffer = audioBuffer;
            source.connect(audioContextRef.current.destination);
            source.start(0);
          }
Thanks!
c
Hey! To help track down this issue, could you share: - The call ID - When exactly this happened (the timestamp) - What response you expected to get - What response you actually got instead This would really help us figure out what went wrong!
g
Hi @Praveen, Thanks for getting back to me so quickly. Here is the info you requested: * Call ID: d4b45113-8d92-4310-a505-f439e10803cd * Timestamp: Jan 13, 2026, 15:58 * I'm successfully receiving the binary PCM data over the websocket connection (the
listenUrl
). This what I'm expected and I actually got So the issue is not so much that I'm not receiving the data via the
listenUrl
, but rather the audio quality when I'm attempting to play it back in the browser live is poor. When i write all the binary data to a PCM file and then play it back using an app called Audacity, then the playback sounds perfect. The issue arises when I'm trying to process and playback the audio live using the Web Audio API. My app is written using React (JavaScript/TypeScript). Do you have any guidance or potentially example of doing this successfully in a web app setting?
c
Hi, thanks for sharing your details. Our team will look into it and get back to you soon with an update.
Hi g3nbl0k, 1. verify sample rate alignment - ensure your Web Audio API AudioContext is created with the exact same sample rate as the incoming PCM data (typically 8000, 16000, or 48000 Hz). if the listenUrl returns PCM at 16000 Hz, your AudioContext should be initialized at 16000 Hz. 2. proper buffer queuing - implement a buffer queue to handle chunks arriving at irregular intervals. do not play each chunk immediately as it arrives. instead, queue them and play from the queue to avoid gaps and underruns. 3. correct PCM to Float32 conversion - Web Audio API expects Float32 samples in the range of -1.0 to 1.0. if your PCM is 16-bit signed integers, you need to divide each sample by 32768.0 to normalize it properly. 4. handle empty buffers gracefully - when no audio data is available, write silence bytes rather than skipping. the pattern used internally was: "buffer = b'\\x00' _frame\_count_ NUM_CHANNELS * BYTES_PER_SAMPLE" 5. use AudioWorklet instead of ScriptProcessorNode - ScriptProcessorNode is deprecated and can cause latency and quality issues. AudioWorklet runs on a separate thread and provides more consistent playback. 6. chunk size alignment - ensure audio chunks align with the expected frame size. for 16kHz mono 16-bit audio, each sample is 2 bytes. misaligned chunks can cause clicks or distortion.
g
Thanks Praveen! I was able to get the issue resolved using AudioWorklet. Works perfectly now. Let me know if you'd like me to share a code snippet in case others run into simliar issues.
c
Hi, sure you can share your code snippet.
g
Sure let me see if I can drop all the code here without discord limiting me
usage-example.js
Copy code
/**
 * VAPI Audio Streaming Usage Examples
 *
 * This file demonstrates how to use the VAPIAudioStreamer class
 * to integrate with VAPI's real-time audio streaming.
 */

import VAPIAudioStreamer from './VAPIAudioStreamer.js';

// ============================================
// Example 1: Basic Usage
// ============================================

async function basicExample() {
  // Get listenUrl from VAPI API when creating a call
  // Format: wss://api.vapi.ai/call/listen/{callId}
  const listenUrl = 'wss://api.vapi.ai/call/listen/your-call-id';

  // Create streamer instance
  const streamer = new VAPIAudioStreamer(listenUrl);

  try {
    // Initialize and start streaming
    await streamer.initialize();
    console.log('🎧 Audio streaming started');

    // The audio will now stream in real-time until you call cleanup()
  } catch (err) {
    console.error('Failed to start audio stream:', err);
  }

  // When the call ends, clean up resources
  // streamer.cleanup();
}

// ============================================
// Example 2: With Error Handling and Retry
// ============================================

async function exampleWithRetry(listenUrl, maxRetries = 3) {
  const streamer = new VAPIAudioStreamer(listenUrl);
  let attempts = 0;

  while (attempts < maxRetries) {
    try {
      await streamer.initialize();
      console.log('✅ Audio streaming started successfully');
      return streamer;
    } catch (err) {
      attempts++;
      console.error(`❌ Attempt ${attempts} failed:`, err.message);

      // Handle specific AudioWorklet race condition
      if (err.message.includes('not defined in AudioWorkletGlobalScope') && attempts < maxRetries) {
        const delay = 1000 * Math.pow(2, attempts - 1); // Exponential backoff
        console.log(`⏳ Retrying in ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw err; // Give up after max retries
      }
    }
  }
}