little-laptop-47712
12/16/2025, 11:13 AMtall-airline-16756
12/16/2025, 11:14 AMtall-airline-16756
12/16/2025, 11:14 AM// node_agent.js
// Requires: npm install wrtc wav
const fs = require('fs');
const { RTCPeerConnection, nonstandard: { RTCAudioSink } } = require('wrtc');
const net = require('net');
async function run() {
const pc = new RTCPeerConnection();
// When remote track arrives (from LiveKit participant), create sink
pc.ontrack = (event) => {
const track = event.track;
const sink = new RTCAudioSink(track);
// Connect to local DeepFilterNet processor over TCP
const client = new net.Socket();
client.connect(6000, '127.0.0.1', () => console.log('connected to denoiser'));
sink.ondata = (data) => {
// data.samples is Float32Array interleaved PCM at sampleRate (usually 48000)
// Convert Float32Array to 16-bit PCM
const float32 = data.samples;
const buffer = Buffer.alloc(float32.length * 2);
for (let i = 0; i < float32.length; i++) {
let s = Math.max(-1, Math.min(1, float32[i]));
buffer.writeInt16LE(Math.floor(s * 32767), i * 2);
}
// Send raw PCM to denoiser
client.write(buffer);
};
// When denoiser returns processed PCM, write to file (replace with TTS upload)
client.on('data', (processedBuffer) => {
// Append processed PCM to file then later convert to WAV or send to TTS directly
fs.appendFileSync('processed.raw', processedBuffer);
});
client.on('close', () => console.log('denoiser connection closed'));
};
// ... set remote description / signaling with LiveKit to receive track ...
// This example omits signaling; assume pc is connected to LiveKit and receiving audio.
}
run().catch(console.error);
Python DeepFilterNet processor (reads raw PCM from TCP, runs denoiser, returns PCM):
# denoiser_server.py
# Requires: pip install torch soundfile numpy deepfilternet (or local deepfilternet repo)
import socket
import numpy as np
import soundfile as sf
# from deepfilternet import DeepFilterNetModel # adapt import to your installation
import torch
HOST = '127.0.0.1'
PORT = 6000
SAMPLE_RATE = 48000
CHANNELS = 1
FRAME_SAMPLES = 480 # example frame size (10ms at 48kHz)
# Placeholder model wrapper - replace with actual DeepFilterNet usage
class DummyDenoiser:
def __init__(self):
pass
def process(self, pcm_int16):
# pcm_int16: np.int16 chunk
# convert to float, pretend we denoise, return same data
return pcm_int16
d = DummyDenoiser()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(1)
print('denoiser listening...')
conn, addr = s.accept()
print('connected by', addr)
buffer = b''
try:
while True:
data = conn.recv(4096)
if not data:
break
buffer += data
# process in multiples of frame bytes
frame_bytes = FRAME_SAMPLES * 2 # int16
while len(buffer) >= frame_bytes:
frame = buffer[:frame_bytes]
buffer = buffer[frame_bytes:]
pcm = np.frombuffer(frame, dtype=np.int16)
processed = d.process(pcm) # replace with model inference
conn.sendall(processed.tobytes())
finally:
conn.close()
s.close()
Convert processed.raw to WAV (so a TTS system that accepts files can consume it):
# Use ffmpeg to convert raw PCM 16-bit 48k mono to WAV
ffmpeg -f s16le -ar 48000 -ac 1 -i processed.raw processed.wav
Key adaptation points for your setup:
• If LiveKit audio sample rate differs, resample or configure appropriate frame sizes.
• Replace the DummyDenoiser with actual DeepFilterNet inference code (load model, perform STFT, mask/filters, inverse STFT). DeepFilterNet typically expects float32 input; run model per-frame or in chunks with overlap-add.
• If your TTS accepts streaming PCM, send the processed PCM directly instead of writing files.
• Ensure correct channel layout (mono vs stereo) and sample format expected by TTS.
If you want, I can:
• Provide a concrete DeepFilterNet Python snippet showing model loading and inference for float32 PCM frames.
• Adapt the node agent example to use LiveKit signaling (publish/subscribe) for a specific SDK (web, JS, or server-side).
Which follow-up would you like?little-laptop-47712
12/16/2025, 11:21 AMtall-airline-16756
12/16/2025, 11:22 AMnoise_cancellation.BVC() and <http://noise_cancellation.NC|noise_cancellation.NC>()) that are optimized for voice AI workflows and are applied in the agent’s AgentSession input stage. Using those built-in options ensures compatibility with STT and turn-detection and is the recommended approach.
• RNNoise and DeepFilterNet are external noise-suppression solutions. The LiveKit docs do not discuss integrating RNNoise or DeepFilterNet, and applying external filters outside LiveKit’s pipeline could interfere with STT/turn-detection and potentially degrade performance.
Recommendation:
• Prefer LiveKit’s built-in noise cancellation (BVC or NC) for Voice Agents to ensure optimal STT and turn-detection behavior.
• If you have a specific reason to use RNNoise or DeepFilterNet (licensing, latency, custom noise profiles), tell me:
1) which TTS/STT and sample rate you use, 2) whether you need real-time streaming or batch processing, 3) your deployment language (Node/Python)— and I’ll outline an integration pattern or provide example code for RNNoise or DeepFilterNet.
Sources: Enhanced noise cancellation | LiveKit docslittle-laptop-47712
12/16/2025, 11:26 AMtall-airline-16756
12/16/2025, 11:26 AMlittle-laptop-47712
12/16/2025, 11:27 AMtall-airline-16756
12/16/2025, 11:28 AMlittle-laptop-47712
12/16/2025, 11:28 AMtall-airline-16756
12/16/2025, 11:29 AMlittle-laptop-47712
12/16/2025, 11:30 AMtall-airline-16756
12/16/2025, 11:30 AMlittle-laptop-47712
12/16/2025, 11:31 AMtall-airline-16756
12/16/2025, 11:31 AMlittle-laptop-47712
12/16/2025, 11:35 AMtall-airline-16756
12/16/2025, 11:35 AMlittle-laptop-47712
12/16/2025, 11:37 AMtall-airline-16756
12/16/2025, 11:37 AM#include <rnnoise.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct {
DenoiseState *st;
} rnstate;
rnstate* rnstate_create() {
rnstate *r = malloc(sizeof(rnstate));
r->st = rnnoise_create(NULL);
return r;
}
void rnstate_destroy(rnstate* r) {
rnnoise_destroy(r->st);
free(r);
}
void rnprocess(rnstate* r, const short *in, short *out) {
float fin[480];
float fout[480];
for (int i=0;i<480;i++) fin[i] = in[i] / 32768.0f;
rnnoise_process_frame(r->st, fout, fin);
for (int i=0;i<480;i++) {
float v = fout[i];
if (v > 1.0f) v = 1.0f;
if (v < -1.0f) v = -1.0f;
out[i] = (short)(v * 32767.0f);
}
}
Compile the shim into a shared lib (adjust paths to rnnoise includes/libs): gcc -shared -O3 -fPIC rnnoise_shim.c -I../include -L../build -lrnnoise -o librnnoise_shim.so
• Load this shared lib in Python with ctypes. The shim above assumes 480-sample frames (10 ms at 48 kHz). Adjust frame size if you choose 20ms.
2) Python project layout & dependencies
• Create virtualenv:
python3 -m venv venv source venv/bin/activate pip install websockets aiohttp asyncio deepgram-sdk numpy soundfile
• Project files:
◦ app/
▪︎ twilio_ws_server.py
▪︎ livekit_agent_bridge.py
▪︎ rnnoise_worker.py
▪︎ deepgram_client.py
▪︎ utils.py
▪︎ librnnoise_shim.so (built shared lib)
3) Twilio Media Streams WebSocket server (receive 48k PCM)
• Twilio Media Streams can send binary audio frames as 16-bit PCM at 8000/16000/48000 depending on config. Configure Twilio to stream at 48 kHz mono.
• Minimal WebSocket receiver using websockets and asyncio:
# twilio_ws_server.py
import asyncio, websockets, json
from livekit_agent_bridge import AgentBridge
AGENT = AgentBridge() # see section 4
async def handler(ws, path):
async for msg in ws:
# Twilio sends JSON control messages and binary audio frames separately.
if isinstance(msg, bytes):
# raw PCM bytes from Twilio at 48kHz mono s16le
await AGENT.receive_audio_packet(msg)
else:
# handle JSON events if needed
obj = json.loads(msg)
# handle event messages (start/stop)
print("event:", obj)
async def main():
async with websockets.serve(handler, '0.0.0.0', 8080, max_size=None):
await asyncio.Future()
if __name__ == '__main__':
asyncio.run(main())
4) LiveKit AgentSession integration point
• You said you use AgentSession (LiveKit Python). The AgentSession input should accept audio frames. Implement an AgentBridge that receives Twilio packets and pushes them into the AgentSession input callback or buffer where STT/turn detection would normally read.
Example AgentBridge skeleton:
# livekit_agent_bridge.py
import asyncio
from rnnoise_worker import RNNoiseWorker
from deepgram_client import DeepgramStreamer
class AgentBridge:
def __init__(self):
self.rnworker = RNNoiseWorker()
self.deepgram = DeepgramStreamer()
self.queue = asyncio.Queue() # raw packets to process
# start worker tasks
asyncio.create_task(self._processing_loop())
async def receive_audio_packet(self, pcm_bytes):
# pcm_bytes expected s16le 48k mono
await self.queue.put(pcm_bytes)
async def _processing_loop(self):
while True:
pcm_bytes = await self.queue.get()
# split into 480-sample frames (480 samples * 2 bytes = 960 bytes)
buf = memoryview(pcm_bytes)
frame_size_bytes = 480 * 2
i = 0
while i + frame_size_bytes <= len(buf):
frame = bytes(buf[i:i+frame_size_bytes])
i += frame_size_bytes
denoised = await self.rnworker.process_frame(frame)
# send denoised frame to Deepgram streamer
await self.deepgram.send_pcm_frame(denoised)
# Also optionally feed denoised to AgentSession input if needed
# handle leftover bytes by prefixing to next packet (omitted for brevity)
Notes:
• Ensure Twilio sends frames aligned to 10ms if possible; otherwise assemble into frames across packets.
5) RNNoise denoiser worker (low-latency frame processing)
• Use asyncio and a thread pool for calling the C shim (ctypes calls are blocking). Keep per-call overhead low.
# rnnoise_worker.py
import asyncio
from concurrent.futures import ThreadPoolExecutor
import ctypes
import numpy as np
LIB = ctypes.CDLL('./librnnoise_shim.so')
class RNNoiseWorker:
def __init__(self):
LIB.rnstate_create.restype = ctypes.c_void_p
LIB.rnstate_destroy.argtypes = [ctypes.c_void_p]
LIB.rnprocess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_short), ctypes.POINTER(ctypes.c_short)]
self.state = LIB.rnstate_create()
self.executor = ThreadPoolExecutor(max_workers=4)
async def process_frame(self, frame_bytes):
# frame_bytes: s16le 480 samples => 960 bytes
pcm = np.frombuffer(frame_bytes, dtype=np.int16)
out = np.empty_like(pcm)
# call blocking C function in threadpool
def call():
in_arr = (ctypes.c_short * len(pcm)).from_buffer_copy(pcm)
out_arr = (ctypes.c_short * len(out))()
LIB.rnprocess(self.state, in_arr, out_arr)
return bytes(bytearray(out_arr))
loop = asyncio.get_running_loop()
denoised = await loop.run_in_executor(self.executor, call)
return denoised
def close(self):
LIB.rnstate_destroy(self.state)
self.executor.shutdown(wait=False)
6) Deepgram streaming client
• Use Deepgram’s realtime WebSocket API or their SDK to stream raw PCM. Configure Deepgram to accept 48 kHz linear16 PCM.
Example using websockets:
# deepgram_client.py
import asyncio, websockets, base64, json, os
DEEPGRAM_KEY = os.getenv('DEEPGRAM_API_KEY')
class DeepgramStreamer:
def __init__(self):
<http://self.ws|self.ws> = None
self.uri = f"<wss://api.deepgram.com/v1/listen?encoding=linear16&sample_rate=48000&channels=1>"
asyncio.create_task(self._connect())
async def _connect(self):
headers = [("Authorization", f"Token {DEEPGRAM_KEY}")]
<http://self.ws|self.ws> = await websockets.connect(self.uri, extra_headers=headers)
asyncio.create_task(self._recv_loop())
async def _recv_loop(self):
async for msg in <http://self.ws|self.ws>:
# Deepgram sends JSON transcripts; handle them here
obj = json.loads(msg)
print("Deepgram:", obj)
async def send_pcm_frame(self, frame_bytes):
# Deepgram expects raw bytes for linear16: send as base64 JSON or binary?
# Deepgram realtime accepts binary linear16 frames directly over the socket:
await self.ws.send(frame_bytes)
Notes:
• Confirm Deepgram realtime API binary format. Above assumes binary frames accepted for linear16. If Deepgram requires base64 JSON envelopes, wrap accordingly:
{ "type":"input_audio", "encoding":"linear16", "sample_rate":48000, "audio": "<base64>" }
7) Run steps and edge cases
• Start the Python server (activate venv).
• Start twilio_ws_server to accept connections from Twilio Media Streams.
• In Twilio, configure the <Stream> WebSocket URL to ws://your-server:8080 and audioFormat with 48kHz linear16.
• Verify Twilio sends small packets; if Twilio sends different packet sizes, buffer across packets to assemble exact 480-sample frames.
Command summary:
• Build rnnoise + shim -> librnnoise_shim.so
• venv, pip install dependencies
• export DEEPGRAM_API_KEY=your_key
• python twilio_ws_server.py
Tuning for minimal latency
• Use 10 ms frames (480 samples at 48 kHz).
• Keep ThreadPool max_workers modest (2–4).
• Avoid extra buffering; stream frames as soon as processed.
• If CPU saturated, consider increasing batch size to 20 ms (960 samples) at small latency cost.
Limitations & notes
• RNNoise is light but not state-of-the-art. It runs well on CPU with low latency.
• Ensure you handle packet framing (leftover samples across WebSocket messages).
• If you also want LiveKit AgentSession to consume denoised audio for turn detection, feed the same denoised frames into the AgentSession input hook—ensure timing is preserved.
• For production, add reconnection, jitter/buffer management, and per-call resource limits.
Do you want:
• The RNNoise shim C file and a complete build script I can paste so you can compile in one shot?
• A working end-to-end repo zip with all files above assembled?
Which next step would you like?