# Telnyx Voice: Noise Suppression — Full Documentation > Complete page content for Noise Suppression (Voice section) of the Telnyx developer docs (https://developers.telnyx.com). > This file: https://developers.telnyx.com/docs/development/llms/voice-noise-suppression-llms-full-txt · Root index: https://developers.telnyx.com/llms.txt ## WebSocket ### Overview > Source: https://developers.telnyx.com/docs/voice/noise-suppression/websocket-streaming.md Stream raw PCM audio over a WebSocket and receive the same audio back with background noise removed. The service runs AI noise suppression on Telnyx infrastructure. It is independent of calls: use it for any audio source, for example before you send audio to a speech-to-text engine. To remove noise from a live Voice API or TeXML call, use [in-call noise suppression](/docs/voice/programmable-voice/noise-suppression) instead. This page covers the standalone WebSocket API. ## Endpoint ``` wss://api.telnyx.com/v2/noise_suppression ``` ## Authentication Send your API key in the `Authorization` header of the WebSocket upgrade request: ``` Authorization: Bearer YOUR_API_KEY ``` A request without valid credentials fails with HTTP `401` before the WebSocket opens. ## Connection lifecycle ### 1. Handshake Set all options as query parameters on the upgrade request. You cannot change them during the session. ``` GET /v2/noise_suppression?format=l16&sample_rate=16000&channels=1 HTTP/1.1 Host: api.telnyx.com Upgrade: websocket Connection: Upgrade Authorization: Bearer YOUR_API_KEY Sec-WebSocket-Version: 13 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ``` ### 2. Streaming Send audio as **binary** frames. For each binary frame you send, the server returns exactly one message: - a **binary** frame with denoised audio, or - a **text** frame with a JSON error (see [Errors](#errors)). ``` Client → Server binary: 640 bytes (20 ms) Client ← Server binary: 640 bytes denoised Client → Server binary: 640 bytes (20 ms) Client ← Server binary: 640 bytes denoised ``` ### 3. End of stream When you have no more audio, send this text frame: ```json {"type": "end_of_stream"} ``` The server then returns all remaining audio, including the audio held back by the model's processing delay, as one final binary frame. Then it closes the connection with close code `1000`. Keep reading until you receive the close frame. ``` Client → Server text: {"type":"end_of_stream"} Client ← Server binary: remaining denoised audio Client ← Server close: 1000 end_of_stream ``` Always send `end_of_stream` before you stop. If you close the WebSocket without it, the server cannot send the remaining audio, and the end of your stream is lost. ## Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `format` | string | `l16` | Audio encoding. `l16` or `f32le`. Used for both input and output. | | `sample_rate` | integer | `16000` | Sample rate in Hz. Any integer from `8000` to `48000`. Used for both input and output. See [Sample rates](#sample-rates). | | `channels` | integer | `1` | Number of channels. Only `1` (mono) is supported. | | `model` | string | `rook` | Noise suppression model. `rook` or `quail`. See [Models](#models). | | `quality` | string or number | `balanced` | Enhancement strength. See [Quality](#quality). | An unsupported value closes the connection with an `invalid_params` error and close code `1008`. See [Errors](#errors). ## Sample rates The service accepts any integer sample rate from `8000` to `48000` Hz. A value outside that range closes the connection with an `invalid_params` error and close code `1008`. The output uses the same sample rate as the input. Use the native sample rate of your audio. Send telephony and PSTN (narrowband) audio at `8000`. Do not upsample 8 kHz audio to a higher rate. The full-band model treats the missing upper band as noise and lowers the speech level by about 14 dB. ## Models You select the model family with `model`. The service selects the model variant that matches your `sample_rate`. | `model` | Use for | `8000` | `8001` to `16000` | `16001` to `48000` | |---------|---------|--------|-------------------|--------------------| | `rook` (default) | Audio for human listeners | 8 kHz model | 16 kHz model | 48 kHz model | | `quail` | Audio for speech-to-text | 8 kHz model | 16 kHz model | 16 kHz model | Above `16000`, `quail` processes the audio with the 16 kHz model. The output keeps your sample rate, but its audio bandwidth is limited to the 16 kHz model's bandwidth (8 kHz). ## Quality `quality` sets the enhancement strength: how much noise the model removes. | Value | Enhancement strength | |-------|----------------------| | A number from `0.0` to `1.0` | The given strength. `0.0` is the lightest processing, `1.0` is the strongest. | | `low_latency` | `0.5`. Lighter processing. The name is kept for compatibility. | | `balanced` | `0.8`. The default. | | `high_quality` | `1.0`. The strongest processing. | Any other value, for example `1.5` or `max`, closes the connection with an `invalid_params` error. ## Audio format Audio is raw mono PCM with no container and no header. Do not send WAV headers. Strip the header and send only the sample data. | `format` | Sample type | Bytes per sample | Byte order | |----------|-------------|------------------|------------| | `l16` | 16-bit signed integer | 2 | little-endian | | `f32le` | 32-bit float, range -1.0 to 1.0 | 4 | little-endian | The output uses the same `format` and `sample_rate` as the input. `l16` is little-endian. This is different from the big-endian L16 of RTP (RFC 3551). Byte-swap RTP payloads before you send them. ## Chunk size - Each binary frame must contain a whole number of samples: a multiple of 2 bytes for `l16`, a multiple of 4 bytes for `f32le`. - The maximum binary frame size is **65,536 bytes**. - 20 ms chunks are a good default. At 16 kHz `l16`, 20 ms is 640 bytes. At 8 kHz `l16`, 20 ms is 320 bytes. ## Buffering and latency The model processes audio in fixed-size internal frames, and it has a short processing delay. The server keeps input that does not fill a complete frame and processes it together with your next chunk. As a result: - A response can be smaller or larger than the matching request. It can also be an **empty** binary frame. Ignore empty frames. - The output starts with the model's processing delay. The first samples you receive correspond to that delay, not to your first input samples. - When you send `end_of_stream`, the server returns all of the audio that it still holds. The total output is your input length plus the processing delay. ## Session duration Sessions are limited to 590 seconds (just under 10 minutes). Reconnect to continue longer streams. At the limit, the server returns the remaining audio, sends a `max_session_duration` error message, and closes the connection with code `1008`. You do not lose audio. To end with a normal close (`1000`) instead, send `end_of_stream` before the limit and then open a new connection for the rest of the stream. ## Errors ### HTTP errors (handshake) | Code | Cause | |------|-------| | 400 | `sample_rate` or `channels` is not a valid non-negative integer | | 401 | Missing or invalid API key | | 503 | The server is restarting and does not accept new sessions. Reconnect with backoff. | ### Error messages The server sends errors as JSON text frames: ```json { "type": "error", "code": "invalid_params", "message": "unsupported sample_rate 96000 (supported: 8000-48000 Hz)" } ``` `code` is a stable identifier. `message` is a human-readable description; do not parse it. ### Error codes | Error code | Close code | Cause | |------------|------------|-------| | `invalid_params` | 1008 | A query parameter has an unsupported value: `format`, `sample_rate`, `channels`, `model`, or `quality`. | | `max_session_duration` | 1008 | The session reached the 590-second limit. The server returns the remaining audio before it closes. | | `session_error` | 1011 | The server could not start the noise suppression session. | | `processing_error` | 1011 | The noise suppression engine failed. | | `encode_error` | 1011 | The server could not encode the denoised audio. | | `server_shutdown` | 1001 | The server is restarting, for example during a deploy. The server returns the remaining audio before it closes. | | `max_connections` | 1013 | The server has too many open sessions. Try again later. | | `overloaded` | 1013 | The server is overloaded and does not accept new sessions. Try again later. | | `chunk_too_large` | None, the session continues | The binary frame is larger than 65,536 bytes. The server drops the chunk. | | `decode_error` | None, the session continues | The frame length is not a whole number of samples. The server drops the chunk. | | `unsupported_message` | None, the session continues | The server does not recognize the text frame. The only supported text frame is `end_of_stream`. | ### Close codes When the server closes the connection, the close frame reason is the error code. Before every close that the server makes with a code other than `1000`, including at the [session limit](#session-duration), it sends the matching error message. A close caused by a network failure comes without an error message. | Close code | Reason | What to do | |------------|--------|---------| | 1000 | `end_of_stream` | Nothing. Normal end: the server returned all audio after your `end_of_stream`. | | 1001 | `server_shutdown` | The server is restarting. Reconnect with backoff. | | 1008 | `invalid_params` | A query parameter has an unsupported value. Fix the parameter before you reconnect. | | 1008 | `max_session_duration` | The session reached the 590-second limit. Reconnect to continue. | | 1011 | `session_error`, `processing_error`, `encode_error` | Server error. Reconnect and try again. | | 1013 | `max_connections`, `overloaded` | The service is busy and does not accept new sessions. Open sessions continue. Reconnect after a short wait. | ### Reconnecting Close code `1001` and HTTP `503` mean that the server is restarting, for example during a deploy. Close codes `1011` (server error) and `1013` (service busy) are also temporary. None of these are errors in your request. Reconnect with exponential backoff; the new session goes to a healthy server. The new session starts empty: the service keeps no state between connections. Audio that you sent after the server started to close is not returned. For a live stream, continue with new audio on the new connection. For a file, send the file again. ## Next steps See [Examples](/docs/voice/noise-suppression/websocket-streaming/examples) for complete Python and Node.js code that denoises a WAV file. --- ### Examples > Source: https://developers.telnyx.com/docs/voice/noise-suppression/websocket-streaming/examples.md Denoise a WAV file and save the result to a new WAV file. ## Prerequisites - A Telnyx API key. - A mono 16-bit PCM WAV file with a sample rate from 8 kHz to 48 kHz. To convert a file with `ffmpeg`: ```bash ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le noisy.wav ``` For telephony audio, use `-ar 8000`. Do not upsample 8 kHz audio to a higher rate. ## How the examples work 1. Read the sample rate from the WAV header. 2. Connect with `format=l16`, the same `sample_rate`, and `channels=1`. 3. Send the audio in 20 ms binary chunks, then send `{"type": "end_of_stream"}`. 4. Read messages until the server closes the connection. Keep the binary frames and print JSON errors. 5. On close `1000`, write the denoised audio to `denoised.wav`. 6. On close `1001`, `1011`, or `1013`, or HTTP `503`, the failure is temporary. Wait, then send the file again on a new connection, with exponential backoff. Any other close is an error. The examples send the whole file in one session, so they suit files shorter than 590 seconds. A longer file reaches the [session limit](/docs/voice/noise-suppression/websocket-streaming#session-duration) and closes with `1008`. Split longer audio into parts and send each part in its own session. See [End of stream](/docs/voice/noise-suppression/websocket-streaming#3-end-of-stream) for why the examples send `end_of_stream` instead of only closing the connection. ## Code ```python Python # pip install websockets import asyncio import contextlib import json import wave import websockets API_KEY = "YOUR_API_KEY" INPUT_FILE = "noisy.wav" OUTPUT_FILE = "denoised.wav" URL = "wss://api.telnyx.com/v2/noise_suppression" # Temporary failures: server restart, server error, server busy. RETRY_CLOSE_CODES = {1001, 1011, 1013} class Retryable(Exception): """A temporary failure (see RETRY_CLOSE_CODES, or HTTP 503). Try again.""" async def denoise_once(pcm, sample_rate): chunk_bytes = sample_rate // 50 * 2 # 20 ms url = f"{URL}?format=l16&sample_rate={sample_rate}&channels=1" headers = {"Authorization": f"Bearer {API_KEY}"} output = bytearray() try: async with websockets.connect(url, additional_headers=headers) as ws: async def send(): for i in range(0, len(pcm), chunk_bytes): await ws.send(pcm[i:i + chunk_bytes]) await ws.send(json.dumps({"type": "end_of_stream"})) sender = asyncio.create_task(send()) try: async for message in ws: if isinstance(message, bytes): output.extend(message) # empty frames add nothing else: print("Error:", json.loads(message)) except websockets.ConnectionClosedError: pass # the close code is checked below finally: sender.cancel() with contextlib.suppress(asyncio.CancelledError, websockets.ConnectionClosed): await sender if ws.close_code in RETRY_CLOSE_CODES: raise Retryable(f"closed with {ws.close_code} {ws.close_reason}") if ws.close_code != 1000: raise RuntimeError(f"closed with {ws.close_code} {ws.close_reason}") except websockets.InvalidStatus as e: if e.response.status_code == 503: # server is draining raise Retryable("service unavailable") from e raise return bytes(output) async def denoise(): with wave.open(INPUT_FILE, "rb") as wav: if wav.getsampwidth() != 2 or wav.getnchannels() != 1: raise ValueError("Input must be mono 16-bit PCM") sample_rate = wav.getframerate() pcm = wav.readframes(wav.getnframes()) for attempt in range(5): try: output = await denoise_once(pcm, sample_rate) break except Retryable as e: if attempt == 4: raise RuntimeError("service unavailable after 5 attempts") from e print(f"{e}, retrying") await asyncio.sleep(2 ** attempt) with wave.open(OUTPUT_FILE, "wb") as out: out.setnchannels(1) out.setsampwidth(2) out.setframerate(sample_rate) out.writeframes(output) print(f"Wrote {len(output)} bytes to {OUTPUT_FILE}") asyncio.run(denoise()) ``` ```javascript Node.js // npm install ws const fs = require("fs"); const WebSocket = require("ws"); const API_KEY = "YOUR_API_KEY"; const INPUT_FILE = "noisy.wav"; const OUTPUT_FILE = "denoised.wav"; const URL = "wss://api.telnyx.com/v2/noise_suppression"; // Find the "fmt " and "data" chunks of a PCM WAV file. function readWav(path) { const buf = fs.readFileSync(path); let fmt; let data; for (let pos = 12; pos + 8 <= buf.length; ) { const id = buf.toString("ascii", pos, pos + 4); const size = buf.readUInt32LE(pos + 4); if (id === "fmt ") fmt = buf.subarray(pos + 8, pos + 8 + size); if (id === "data") data = buf.subarray(pos + 8, pos + 8 + size); pos += 8 + size + (size % 2); } if (fmt.readUInt16LE(14) !== 16 || fmt.readUInt16LE(2) !== 1) { throw new Error("Input must be mono 16-bit PCM"); } return { sampleRate: fmt.readUInt32LE(4), pcm: data }; } function writeWav(path, pcm, sampleRate) { const header = Buffer.alloc(44); header.write("RIFF", 0); header.writeUInt32LE(36 + pcm.length, 4); header.write("WAVEfmt ", 8); header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(1, 22); // mono header.writeUInt32LE(sampleRate, 24); header.writeUInt32LE(sampleRate * 2, 28); header.writeUInt16LE(2, 32); header.writeUInt16LE(16, 34); header.write("data", 36); header.writeUInt32LE(pcm.length, 40); fs.writeFileSync(path, Buffer.concat([header, pcm])); } // Temporary failures: server restart, server error, server busy. const RETRY_CLOSE_CODES = [1001, 1011, 1013]; // Resolves with the denoised audio. Rejects with err.retryable = true on a // temporary failure (RETRY_CLOSE_CODES or HTTP 503). function denoiseOnce(pcm, sampleRate) { const chunkBytes = Math.floor(sampleRate / 50) * 2; // 20 ms return new Promise((resolve, reject) => { const ws = new WebSocket( `${URL}?format=l16&sample_rate=${sampleRate}&channels=1`, { headers: { Authorization: `Bearer ${API_KEY}` } }, ); const output = []; ws.on("open", () => { for (let i = 0; i < pcm.length; i += chunkBytes) { ws.send(pcm.subarray(i, i + chunkBytes)); } ws.send(JSON.stringify({ type: "end_of_stream" })); }); ws.on("message", (data, isBinary) => { if (isBinary) output.push(data); // empty frames add nothing else console.error("Error:", JSON.parse(data)); }); ws.on("unexpected-response", (req, res) => { const err = new Error(`HTTP ${res.statusCode}`); err.retryable = res.statusCode === 503; // server is draining reject(err); req.destroy(); }); // 1000: all audio returned. RETRY_CLOSE_CODES: try again. ws.on("close", (code, reason) => { if (code === 1000) return resolve(Buffer.concat(output)); const err = new Error(`Closed with ${code} ${reason}`); err.retryable = RETRY_CLOSE_CODES.includes(code); reject(err); }); ws.on("error", reject); }); } async function main() { const { sampleRate, pcm } = readWav(INPUT_FILE); for (let attempt = 0; ; attempt += 1) { try { const denoised = await denoiseOnce(pcm, sampleRate); writeWav(OUTPUT_FILE, denoised, sampleRate); console.log(`Wrote ${denoised.length} bytes to ${OUTPUT_FILE}`); return; } catch (err) { if (!err.retryable || attempt >= 4) throw err; console.error(`${err.message}, retrying`); await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); } } } main().catch((err) => { console.error("Error:", err.message); process.exitCode = 1; }); ``` ## Real-time streams For live audio, such as a microphone or a media stream, send each chunk as soon as you capture it and play or forward each binary response as soon as it arrives. Keep chunks small (10–40 ms) for low latency. When the stream ends, send `{"type": "end_of_stream"}` and keep reading until the server closes the connection. Sessions are limited to 590 seconds. At the limit, the server returns the remaining audio and closes with `1008` and a `max_session_duration` error. Reconnect to continue. See [Session duration](/docs/voice/noise-suppression/websocket-streaming#session-duration). ---