2025, Nov 06 03:00

Troubleshooting beeping in WebSocket live audio with Flask-SocketIO: it's the webcam microphone, not a race condition

Hear periodic beeps in WebSocket live audio with Flask-SocketIO and Eventlet? The culprit may be a webcam microphone driver. Use ffmpeg to isolate it fast.

Streaming live audio over WebSockets looks straightforward until you hit artifacts that seem to scream “race condition.” A classic case: a periodic beeping, roughly two times per second, interrupting otherwise valid audio frames captured with sounddevice and delivered via Flask-SocketIO. The setup uses Eventlet with monkey patching and is deployed under gunicorn, so it’s natural to suspect concurrency, timing, or the deployment stack. Yet the root cause turned out to live elsewhere.

Setup and the symptom

The application captures microphone input and emits raw PCM chunks to a Socket.IO room. Eventlet is enabled, including monkey patching, and the process runs under gunicorn with the Eventlet worker.

import eventlet
eventlet.monkey_patch()
gunicorn -k eventlet -w 1 -t 4 -b 0.0.0.0:5001 app:app

The following snippet shows the core streaming logic where the beeping manifested. Names are illustrative, but the structure and behavior match the original implementation.

import sounddevice as audioio
import threading
from extensions import sio as sock
from flask_socketio import join_room, leave_room
class StreamRelay:
    def __init__(self, rate_hz=44100, chan_count=2, slice_len=1024):
        self.rate_hz = rate_hz
        self.chan_count = chan_count
        self.slice_len = slice_len
        self.bg_worker = None
        self.group_name = 'audio_listeners'
        self.live = False
    def begin(self):
        if self.live:
            return
        self.live = True
        self.bg_worker = threading.Thread(target=self._run_capture, daemon=True).start()
        print('[StreamRelay] worker started')
    def end(self):
        if not self.live:
            return
        self.live = False
        self.bg_worker.join(timeout=1)
        print('[StreamRelay] worker stopped')
    def include_peer(self, sid):
        join_room(self.group_name, sid=sid)
        if not self.live:
            self.begin()
    def exclude_peer(self, sid):
        leave_room(self.group_name, sid=sid)
    def _run_capture(self):
        try:
            with audioio.InputStream(samplerate=self.rate_hz, channels=self.chan_count, dtype='int16') as mic:
                while self.live:
                    buf, _ = mic.read(self.slice_len)
                    sock.emit('audio_chunk', buf.tobytes(), room=self.group_name)
                    sock.sleep(0)
        except Exception as exc:
            print(f'[StreamRelay] error: {exc}')

On the client side, the audio arrives and plays, but an intrusive, periodic beep cuts in roughly twice per second.

What’s really going on

The noise pattern looks like a scheduling hiccup or buffer underrun, so it’s tempting to blame Eventlet, the WebSocket loop, or thread interplay. However, an external capture ruled that out. Recording with ffmpeg reproduced the beeping, independent of Flask-SocketIO or the deployment command, which means the artifact isn’t introduced by the web stack.

There is another key detail: the microphone is part of a webcam. With the camera enabled, the beeping appears even in an ffmpeg recording. With the camera off, the microphone behaves correctly. This narrows the cause to the hardware or driver layer rather than application code, concurrency, or the network path.

A minimal control run

A simple Flask-SocketIO script can serve as a control experiment. Even if such a script works at times and makes the issue look like app complexity, the external recording test matters more here because it bypasses the app entirely.

import eventlet
eventlet.monkey_patch()
from flask import Flask, render_template
from flask_socketio import SocketIO
import sounddevice as audioio
import threading
webapp = Flask(__name__)
ws = SocketIO(webapp)
rate_hz = 44100
nchan = 2
framesz = 1024
def capture_loop():
    with audioio.InputStream(samplerate=rate_hz, channels=nchan, dtype='int16') as mic:
        while True:
            chunk, _ = mic.read(framesz)
            ws.emit('audio_chunk', chunk.tobytes())
            ws.sleep(0)
@webapp.route('/')
def homepage():
    return render_template('index.html')
if __name__ == '__main__':
    threading.Thread(target=capture_loop, daemon=True).start()
    ws.run(webapp, host='0.0.0.0', port=5001, allow_unsafe_werkzeug=True)

Resolution

The solution did not involve Flask-SocketIO, Eventlet, gunicorn, or thread coordination. The beeping originated at the hardware or driver level. Since the microphone is integrated with a webcam, having the camera active caused the periodic artifact. Turning the camera off removed the noise, and the audio stream became clean. This aligns with the external ffmpeg test that reproduced the issue outside the application.

No code changes were required to fix the problem. The mitigation sits at the device layer: with the camera disabled, the microphone input is clean and the streaming path remains unchanged.

Why this matters

When working with real-time media pipelines, it’s easy to attribute audible artifacts to concurrency primitives, asynchronous loops, or network jitter. This case shows why isolating the audio source and validating it outside the app is crucial. If an external recorder picks up the same issue, the problem is upstream of your code. It saves time and avoids unnecessary refactoring of a healthy streaming stack.

Takeaways

Start by verifying the capture chain independently from your web server and framework. If you hear the same beeping in an ffmpeg recording, look at the device or driver configuration next. And if the microphone is part of a webcam, test with the camera off to see whether the artifact disappears. These steps are lightweight and decisive, and they can prevent a detour into debugging threads, schedulers, or Socket.IO when the signal path itself is the culprit.

The article is based on a question from StackOverflow by Daniel and an answer by Daniel.