How it fits together
mod_audio_stream (or mod_audio_fork) forks channel audio as L16 — signed
linear 16-bit PCM — at 16 kHz, and plays audio you send back into the same
channel. L16/16 kHz is byte-for-byte the same as Omni’s PCM16 at 16 kHz, so
run Omni with ?rate=16000 and the audio path is a straight passthrough — no
resampling. Call control rides a separate ESL (Event Socket) connection.
This guide is correct on transport, the fork codec/rate, and the Omni event
behaviors. The exact JSON payloads of Omni events (notably the
dtmf frame you
forward) are defined by the Omni wire protocol — they
live in one helper so updating them is a one-line change.Prerequisites
FreeSWITCH with a fork module
mod_audio_stream (bidirectional; recommended) or mod_audio_fork loaded and
in modules.conf.xml. Confirm with fs_cli -x "module_exists mod_audio_stream".Event Socket access
The inbound Event Socket enabled (default
127.0.0.1:8021, password
ClueCon). Lock this down to localhost or your bridge host.Project layout
package.json
Build it
Dialplan: fork the channel to your bridge
Answer the call, then start
mod_audio_stream toward your bridge at 16 kHz
mono, and park the channel so it stays up while audio streams. Pass the
channel UUID on the URL so the bridge can issue ESL commands for it.dialplan/omni-agent.xml
Bridge: relay fork audio ↔ Omni
FreeSWITCH connects to
/fork and streams binary L16 frames. Forward them to
Omni untouched, and stream Omni’s binary frames straight back into the channel.server.js (audio bridge)
ESL: barge-in, transfer, and DTMF
One Event Socket connection drives every call. Subscribe to DTMF events and
forward digits to Omni; react to Omni events with
uuid_break (barge-in) and
uuid_transfer (escalate to a human).server.js (control plane)
The
flush, dtmf, transfer_to_human, and session_ending event names
are stable; exact payload fields come from the
Omni wire protocol. forwardDtmf is the only spot
that constructs an Omni control frame.Run it
fs_cli -x "reloadxml"), set bridge_host to where your
Node process is reachable, then dial extension 9000. The agent should greet the
caller within a second. Talk over it to confirm uuid_break cuts the agent off;
press a DTMF key and watch it reach Omni; trigger a transfer to confirm the
channel reaches the human extension.
Codec & rate notes
- No resampling at the fork. Fork at
16kand run Omni atrate=16000— L16/16 kHz ↔ PCM16/16 kHz is a passthrough. If the caller’s leg is 8 kHz, FreeSWITCH transcodes it to 16 kHz for the fork (8000 → 16000 is a 2:1 upsample done inside FreeSWITCH), so Omni always sees a clean 16 kHz stream. - Endianness, not rate, is the usual culprit for distorted audio — see the warning above.
parkkeeps the channel alive. Without it the call can tear down before the fork is established.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Static / white noise both ways | Endian mismatch | buf.swap16() on the fork audio in both directions |
| Agent sounds slow or fast | Fork rate ≠ Omni rate | Fork at 16k and connect Omni at rate=16000 |
| Channel hangs up before audio | Missing park | Add <action application="park"/> after starting the fork |
| Agent talks over the caller | flush not wired to uuid_break | Call uuid_break <uuid> on every flush event |
| DTMF never reaches Omni | Not subscribed to DTMF, or wrong UUID | control.subscribe(["DTMF"]); map by Unique-ID |
| Transfer fails | Bad extension/context | Verify the uuid_transfer <uuid> <ext> XML <context> args route in your dialplan |
| Omni WS closes immediately | Bad key/agent | Check the close code in Errors & limits |
module_exists returns false | Fork module not loaded | Load mod_audio_stream in modules.conf.xml and reloadxml |
Next steps
Omni wire protocol
Exact event payloads and close codes.
Browser voice agent
The same agent in the browser via WebRTC.
Phone agent with Twilio
Media Streams bridge at μ-law 8 kHz.
Errors & limits
Rate limits, concurrency, and retries.