Server-Side WebRTC Noise Reduction with Pion, FFmpeg, and RNN Models

5 min read Original article ↗

This system is closer to a B2B institutional device-fleet scenario than a consumer calling app. In healthcare, eldercare, and similar facilities, many shared devices may need to stay online for long periods and keep calls usable in noisy rooms where pickup distance and speech quality both matter.

Server-side audio noise reduction for WebRTC calls should start with a narrow validation target: can a Go media service receive an Opus track with Pion, decode it to PCM, run FFmpeg’s RNN noise reduction filter, and produce an output that is worth evaluating? This prototype does not try to replace WebRTC’s built-in audio processing.

The source experiment is based on a public sample project: snowlyg/webrtc_denoise_use_ffmpge. It is a prototype, not production-ready RTC infrastructure.

Background

WebRTC already includes audio processing blocks such as echo cancellation, noise suppression, and automatic gain control. In a controlled device environment, using the client-side WebRTC audio stack is usually the first and best option.

In field deployments, the device side is not always controlled:

  • microphones and speakers may vary across batches.
  • hardware acoustic design may change faster than the software release cycle.
  • vendor firmware may expose inconsistent audio behavior.
  • tuning each device family deeply can create a high maintenance cost.

That makes a server-side experiment useful. If a service can receive audio, apply a known filter, and compare the result offline, it becomes easier to decide whether server-side processing is a viable supplement for specific environments.

The first safe milestone is not real-time forwarding. It is file-based validation.

Processing Boundary

The server-side path is:

Server-side WebRTC audio processing pipeline

  1. Browser or client sends a WebRTC audio track.
  2. Pion receives the remote track in OnTrack.
  3. The service reads RTP packets with track.ReadRTP().
  4. Opus payload is decoded to PCM.
  5. PCM is written to an FFmpeg process through stdin.
  6. FFmpeg applies arnndn with an RNN model.
  7. The filtered output is written to a file for comparison.

This boundary matters because RTP, Opus, PCM, and FFmpeg raw audio input are different formats. Mixing them up can produce a file, but not necessarily valid audio.

Minimal Experiment

The prototype starts from Pion’s save-to-disk idea: connect a browser page to a Go process, receive audio/video, and write media out for inspection.

The public dependencies are:

The prototype uses Pion v4 and an Opus decoder from gopkg.in/hraban/opus.v2.

Decoding Opus to PCM

The audio track is usually Opus at 48 kHz. The example code keeps the sample rate explicit and decodes the RTP payload into an int16 PCM buffer.

var sampleRate = 48000
var channels = 2
var frameSizeMs = 60
frameSize := channels * frameSizeMs * sampleRate / 1000

pcm := make([]int16, frameSize)

dec, err := opus.NewDecoder(sampleRate, channels)
if err != nil {
	return err
}

for {
	rtpPacket, _, err := track.ReadRTP()
	if err != nil {
		return err
	}
	if rtpPacket == nil || len(rtpPacket.Payload) == 0 {
		continue
	}

	n, err := dec.Decode(rtpPacket.Payload, pcm)
	if err != nil {
		continue
	}

	decoded := pcm[:n*channels]

	buf := new(bytes.Buffer)
	if err := binary.Write(buf, binary.LittleEndian, decoded); err != nil {
		return err
	}

	if _, err := pipeWriter.Write(buf.Bytes()); err != nil {
		return err
	}
}

Two details should not be hidden:

  • The channels value must match the decoded stream assumptions. Do not hard-code stereo if the negotiated track is mono.
  • The FFmpeg input format must match the PCM buffer. int16 PCM maps to s16le; using s32le with int16 bytes is a bug to review before treating results as trustworthy.

Running FFmpeg arnndn

FFmpeg’s arnndn filter applies an RNN noise reduction model. For a validation file, the command shape is:

cmd := exec.Command(
	"ffmpeg",
	"-v", "warning",
	"-f", "s16le",
	"-ac", "2",
	"-ar", "48000",
	"-i", "pipe:0",
	"-af", "arnndn=m=models/cb.rnnn",
	"-c:a", "libopus",
	"-b:a", "64k",
	"output_rnn.opus",
)

The prototype repository currently shows the experimental shape, including the pipe to FFmpeg. Before using this in a production path, the raw audio format, channel count, frame duration, process lifecycle, and error handling all need to be made explicit.

Validation

For the first pass, I would validate with files:

  1. Capture the unprocessed Opus output.
  2. Decode or open the file in an audio tool such as Audacity.
  3. Generate output_rnn.opus from the FFmpeg path.
  4. Compare the perceived noise, waveform, and frequency content.
  5. Check whether speech quality was damaged while noise was reduced.

Synthetic noise floor comparison

This comparison should not be reduced to “the waveform looks cleaner”. Noise suppression can also remove weak speech details, create artifacts, or change the perceived naturalness of a call. Listening tests are still required.

Production Boundaries

The file-based prototype is not the same as a real-time WebRTC media server.

For real-time use, the design has to answer:

  • How much buffering is introduced by Opus decoding, FFmpeg stdin, filtering, and re-encoding?
  • Is FFmpeg started once per call, per participant, or as a managed worker pool?
  • What happens when FFmpeg exits, blocks, or falls behind?
  • How are CPU and memory isolated from signaling services?
  • Does the system return processed audio to WebRTC, or only record a cleaned stream?
  • How are packet loss, jitter, RTP timestamps, and audio/video sync preserved?

My practical default is to keep the first implementation as an offline validation path. Only after measuring latency and CPU cost should it move toward a real-time forwarding path.

Takeaway

Server-side noise reduction can be useful when device-side audio behavior is hard to control, but it is not free. The clean boundary is:

  • Pion receives the WebRTC track.
  • Opus is decoded into correctly described PCM.
  • FFmpeg arnndn applies an RNN model.
  • The result is validated first as a file.
  • Real-time forwarding is a separate engineering decision.

This prototype isolates the audio processing boundary. The next decision can then be based on measured latency, resource cost, and listening results before turning it into production infrastructure.