# One-Way Audio

> Audio flows in only one direction on a call. The four causes — NAT/RTP path, SDP direction, codec mismatch, dropped bridge handle — and how to fix each.

One-way audio means the call connects, both sides think they talk, but audio
travels in only **one direction**. Either the caller hears the agent and you
hear nothing, or you hear the caller and the caller hears silence. Signalling
is correct — the SIP dialog reached `in_progress` — so this is always a
**media-plane** problem, not a connection problem. If the call never answers
at all, see
[Call does not connect](/modalities/voice/troubleshooting/call-not-connecting).

Work from the symptom. The dead direction tells you which half of the media
path to inspect first.

## Which way is dead?

<table>
  <thead>
    <tr><th>Symptom</th><th>Dead leg</th><th>Look here first</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><b>The caller hears the agent; you hear silence</b></td>
      <td><b>Uplink</b> (caller → you)</td>
      <td>NAT / RTP source address — the far end cannot reach the address you advertised.</td>
    </tr>
    <tr>
      <td><b>You hear the caller; the caller hears silence</b></td>
      <td><b>Downlink</b> (you → caller)</td>
      <td>A dropped <code>AudioBridge</code> handle, or an SDP <code>a=sendonly</code> / early-media direction.</td>
    </tr>
    <tr>
      <td><b>Both connect, but one leg is noise, not silence</b></td>
      <td>Either</td>
      <td>A codec / payload-type mismatch on the negotiated leg.</td>
    </tr>
  </tbody>
</table>

In FirstByte Voice, uplink and downlink are two **separate** media-over-QUIC
tracks on the same `call_sid`: `voice/<sid>/uplink` (caller audio) and
`voice/<sid>/downlink` (audio back to the caller). They fail independently.
That is why one direction can die while the other continues to work. See
[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams)
for the track model.

## Cause 1 — NAT / RTP path (the classic one-way)

This is the most common cause. The SDP you answered with advertises the
address the far end must send RTP to. Behind cloud NAT, the engine **binds**
on a private address but must **advertise** its public one. If the public RTP
address is missing or wrong, the carrier streams caller audio into a black
hole. Your egress still reaches the address *they* advertised. So **the caller
hears you, and you hear nothing**.

<table>
  <thead><tr><th>Cause</th><th>Fix</th></tr></thead>
  <tbody>
    <tr>
      <td>The trunk advertises a private RTP IP in the SDP <code>c=</code> line. The private bind leaked into the answer.</td>
      <td>Set the trunk's <b>external RTP IP</b> to the engine's public address. The SDP then carries a reachable <code>c=</code>. The private bind stays internal.</td>
    </tr>
    <tr>
      <td>Asymmetric RTP: the far end sends media from a different port than it signalled, so latching never occurs.</td>
      <td>The media plane latches egress to the source of the first inbound RTP packet. If no inbound packet arrives (see above), there is nothing to latch to. Fix the advertised address first.</td>
    </tr>
    <tr>
      <td>A firewall or SBC in front of the trunk drops inbound RTP.</td>
      <td>Open the RTP port range to the carrier's media IPs. Check that the SBC rewrites the SDP to its own public media address.</td>
    </tr>
  </tbody>
</table>

> **TIP:**
> Confirm the diagnosis with the SIP/RTP packet trace viewer. If you see RTP
> **leave** the engine but **zero** inbound RTP on the caller leg, the far end
> sends to an unreachable address. That is an advertised-IP problem, not a
> routing problem.
> See [SIP/RTP debugging](/modalities/voice/observability/sip-rtp-debugging).

## Cause 2 — SDP direction attribute

An SDP media line carries a direction: `sendrecv` (both ways), `sendonly`,
`recvonly`, or `inactive`. If early media (the `183 Session Progress`) or the
final answer negotiated anything other than `sendrecv`, the negotiation
**intentionally** muted one direction.

<table>
  <thead><tr><th>Cause</th><th>Fix</th></tr></thead>
  <tbody>
    <tr>
      <td>The far end offered <code>a=sendonly</code> (announcement / hold-style early media) and never renegotiated to <code>sendrecv</code>.</td>
      <td>This is expected during ringback. If it continues after the answer, the caller PBX holds the leg. Check for a stray hold/re-INVITE in the trace.</td>
    </tr>
    <tr>
      <td>The answer went out on the <code>183</code> with early media, but the <code>200 OK</code> SDP differs.</td>
      <td>The answer sequence must be <code>100</code> → <code>183</code> with SDP → <code>200</code> with the <b>same</b> SDP. A mismatched or SDP-less <code>200</code> desyncs the media direction. It can also trigger a carrier BYE.</td>
    </tr>
    <tr>
      <td>The call is on hold — a re-INVITE flipped the leg to <code>inactive</code>/<code>sendonly</code>.</td>
      <td>Resume the leg. Look for a <code>reinvited</code> event in the <a href="/modalities/voice/transport-telephony/call-lifecycle">call lifecycle</a>.</td>
    </tr>
  </tbody>
</table>

## Cause 3 — Codec / payload-type mismatch

When one leg is dead as **silence**, the cause is usually a path problem
(Cause 1/2). When one leg is dead as **noise or static**, suspect the codec.
The far end streams a payload type that you did not answer with. Frames
arrive, but they decode to garbage.

The bridge transcodes automatically across everything it negotiated: µ-law
(PCMU, PT 0), A-law (PCMA, PT 8), and Opus. This includes µ-law ⇄ A-law
between two mismatched G.711 legs. One-way noise means the far end sends a
format that was **never in the answer**. There is nothing to transcode it to.

<table>
  <thead><tr><th>Cause</th><th>Fix</th></tr></thead>
  <tbody>
    <tr>
      <td>The carrier streams a payload type that is absent from your SDP answer.</td>
      <td>Offer both PCMU and PCMA, so any PSTN carrier can pick one. Let the bridge transcode to your chosen <code>AudioCodec</code>.</td>
    </tr>
    <tr>
      <td>A static PT number is reused for a different codec (a non-standard carrier).</td>
      <td>Pin the codec on the trunk. Check that the negotiated <code>rtpmap</code> in the trace matches what the carrier sends.</td>
    </tr>
  </tbody>
</table>

See the [Codec guide](/modalities/voice/transport-telephony/codecs) for which
codec to pick per leg, and where transcode is free.

## Cause 4 — the dropped `AudioBridge` handle

> **WARNING:**
> **Keep the `AudioBridge` handle alive.** The bridge holds the live
> publish/subscribe on `voice/<sid>/{uplink,downlink}`. If the handle goes out
> of scope, is garbage-collected, or is dropped, **the tracks it owns tear
> down**. The direction you published goes silent, while the other direction
> (served elsewhere) continues to work. This shows as one-way audio that starts
> a few seconds *into* a call, when the GC runs.

If the caller stops hearing you mid-call, but the SIP dialog is still up and
your `onUplink` callback still fires, you almost certainly dropped the
downlink publisher. Keep the bridge for the **whole lifetime of the call**.
Close it explicitly on hangup.

  
```ts
// WRONG — the bridge is unreferenced the moment attach() returns.
// Once GC runs, downlink stops and the caller hears silence.
await voice.audioBridge.attach(sid, {
  onUplink: (frame, tsUs) => asr.push(frame),
});

// RIGHT — hold the handle for the life of the call, close on hangup.
const bridge = await voice.audioBridge.attach(sid, {
  onUplink: (frame, tsUs) => asr.push(frame),
});
calls.set(sid, bridge);            // keep a live reference

// ...later, when the call ends:
bridge.publishDownlink(lastFrame, Date.now() * 1000);
await bridge.close();              // deterministic teardown
calls.delete(sid);
```
  
  
```python
# WRONG — bridge is dropped as soon as the function returns.
async def handle(sid):
    await voice.audio_bridge.attach(sid, on_uplink=asr.push)

# RIGHT — keep it in scope (or a registry) until the call ends.
async def handle(sid):
    bridge = await voice.audio_bridge.attach(sid, on_uplink=asr.push)
    calls[sid] = bridge            # keep a live reference
    try:
        await run_agent(bridge)    # publish_downlink(...) inside
    finally:
        await bridge.close()       # deterministic teardown
        calls.pop(sid, None)
```
  

> **NOTE:**
> In the synchronous cores (Rust, Go), the bridge closes on **drop** (RAII).
> When the value falls out of scope, both tracks tear down. Bind the bridge to a
> variable that lives as long as the call — for example,
> `let bridge = voice.audio_bridge().attach(...)?;` held in your session struct.
> Do not use a temporary.

## Diagnose it in order

  1. **Confirm the dialog is actually up**
Check that the call reached `in_progress` and is still there. If the call
    dropped, this is a connection problem, not one-way audio. Go to
    [Call does not connect](/modalities/voice/troubleshooting/call-not-connecting).
  2. **Establish which direction is dead**
If only the caller hears you ⇒ the **uplink** is dead ⇒ start at Cause 1
    (NAT/RTP path). If the caller hears silence ⇒ the **downlink** is dead ⇒
    start at Cause 4 (bridge handle), then Cause 2 (SDP direction).
  3. **Read the packet trace**
In the SIP/RTP trace viewer, look for RTP flow on **each** leg separately.
    No inbound RTP on a leg = the far end cannot reach your advertised address
    (Cause 1). Inbound RTP present but the audio is static = a codec mismatch
    (Cause 3). See
    [SIP/RTP debugging](/modalities/voice/observability/sip-rtp-debugging).
  4. **Inspect the negotiated SDP**
Check that the `c=` line carries a **reachable** address, and that the
    media line is `sendrecv`. A private `c=` IP or a `sendonly`/`inactive`
    direction is your answer.
  5. **Audit your bridge lifetime**
If the dead direction is the one your code publishes, check that you hold
    the `AudioBridge` handle for the whole call. Call `close()` only on
    hangup.
  6. **Check MOS if audio is present but poor**
One-way audio is binary: present or absent. If audio flows both ways but
    is choppy or clipped, that is a quality problem. See
    [MOS, jitter & loss](/modalities/voice/observability/mos-jitter-loss).

> **NOTE:**
> **Human-agent downlink (browser softphones).** When you hand a call to a human
> agent, the human hears the caller over a media-over-QUIC track in the browser
> softphone. That return-audio (downlink) path is still being hardened — the
> caller-audio origin publish is being bridged into the relay fan-out. If a
> **browser** human agent reports one-way audio (the agent can talk but cannot
> hear the caller), validate that leg in staging. **Real SIP deskphone agents
> are not affected.** Their media rides the normal RTP bridge. See
> [AI ⇄ human handoff](/modalities/voice/transport-telephony/ai-human-handoff).

## Related

  - **[SIP/RTP debugging](/modalities/voice/observability/sip-rtp-debugging)** — Read per-leg SIP and RTP traces to find the dead direction.
  - **[Codec guide](/modalities/voice/transport-telephony/codecs)** — Pick a codec per leg; where transcode is free and where mismatches bite.
  - **[Call does not connect](/modalities/voice/troubleshooting/call-not-connecting)** — When the dialog never reaches in_progress in the first place.
  - **[Sessions, calls, tracks & streams](/modalities/voice/concepts/sessions-calls-tracks-streams)** — Why uplink and downlink are separate tracks that fail independently.
  - **[SIP trunking](/modalities/voice/transport-telephony/sip-trunking)** — Trunk fields for internal vs external RTP IP behind cloud NAT.
  - **[AI ⇄ human handoff](/modalities/voice/transport-telephony/ai-human-handoff)** — The human-agent downlink caveat, and how the return-audio path is wired.
