# Stream Audio to ASR

> Feed caller uplink audio to your own speech-to-text engine as PCM16 @ 16 kHz.

Run the audio bridge yourself. Pipe the caller's **uplink** (what the caller
says) straight into a streaming recognizer that you control: Deepgram, a
self-hosted Whisper, or any ASR that accepts 16-bit PCM. `audioBridge.attach`
subscribes the uplink track and gives you every frame in `onUplink`. Ask for
`pcm16` at `16000` Hz. The bridge then transcodes the G.711 PSTN leg for you,
so your ASR gets exactly the audio it wants.

> **NOTE:**
> This is the "bring your own ASR" path. You own the recognizer and the dialog
> logic. If you prefer that FirstByte runs a managed cascade
> (ASR → LLM → TTS) from one config file, see
> [BYO ASR / LLM / TTS](/modalities/voice/runtime/byo-asr-llm-tts) instead.

## Attach the bridge and feed your ASR

`attach` takes the `call_sid`, a codec/rate, and your `onUplink` callback.
Frames arrive as raw little-endian PCM16. A mono 20 ms frame at 16 kHz is 320
samples (640 bytes). Push each frame to your recognizer as it arrives.

  
```ts
import { Voice } from "clutchcall_connect/voice";

const v = new Voice({
  baseUrl: "https://engine.firstbyte.fast",
  apiKey:  process.env.CLUTCHCALL_API_KEY!,
  orgId:   "org_abc",
});

// `asr` is your own streaming recognizer client (e.g. a WebSocket to Deepgram
// or a self-hosted Whisper). It accepts linear PCM16 @ 16 kHz.
async function transcribeCall(sid: string) {
  const bridge = await v.audioBridge.attach(sid, {
    codec:      "pcm16",   // raw 16-bit PCM, not Opus/G.711
    sampleRate: 16000,     // what most streaming ASRs expect
    channels:   1,
    frameMs:    20,
    onUplink: (frame /* Uint8Array */, tsUs /* bigint */) => {
      asr.send(frame);     // stream each 640-byte frame straight through
    },
  });

  asr.on("transcript", (text: string, isFinal: boolean) => {
    if (isFinal) console.log(`[${sid}] ${text}`);
  });

  return bridge;   // keep this reference alive for the whole call
}
```
  
  
```python
import os
from clutchcall.voice import Voice

v = Voice(
    base_url="https://engine.firstbyte.fast",
    api_key=os.environ["CLUTCHCALL_API_KEY"],
    org_id="org_abc",
)

# `asr` is your own streaming recognizer (e.g. a WebSocket to Deepgram or a
# self-hosted Whisper). It accepts linear PCM16 @ 16 kHz.
def transcribe_call(sid: str):
    def on_uplink(frame: bytes, ts_us: int) -> None:
        asr.send(frame)          # stream each 640-byte frame straight through

    bridge = v.audio_bridge.attach(
        sid,
        on_uplink=on_uplink,
        codec="pcm16",           # raw 16-bit PCM, not Opus/G.711
        sample_rate=16000,       # what most streaming ASRs expect
        channels=1,
        frame_ms=20,
    )

    @asr.on("transcript")
    def _(text: str, is_final: bool) -> None:
        if is_final:
            print(f"[{sid}] {text}")

    return bridge                # keep this reference alive for the whole call
```
  

> **WARNING:**
> `attach` subscribes the uplink **and** publishes a downlink track. Hold the
> returned `bridge` for the full duration of the call. If the garbage collector
> removes it, both tracks drop and the bridge ends. When you are done, call
> `bridge.close()` before `call.hangup()` so that the tracks close cleanly.

## Transcribe an inbound call

Bind the bridge as soon as the call answers. The example below originates an
outbound call with no server-side agent, so your ASR is the only consumer of
the uplink. The same `attach` works for an inbound `sid` that you already hold.

  
```ts
const call = await v.calls.originate({
  to:      "+15551234567",
  from:    "+15558675309",
  trunkId: "trunk_main",
});

const bridge = await transcribeCall(call.sid);

// … later, when you're finished listening:
await bridge.close();
await call.hangup();
```
  
  
```python
call = v.calls.originate(
    to="+15551234567", from_="+15558675309", trunk_id="trunk_main",
)

bridge = transcribe_call(call.sid)

# … later, when you're finished listening:
bridge.close()
call.hangup()
```
  

## Want Opus frames instead?

Your recognizer possibly takes Opus directly, or you only capture to disk. In
that case, ask for `codec: "opus"` and skip the transcode. The caller's audio
then reaches you as 20 ms encoded Opus packets. Use `pcm16` only when your ASR
needs raw samples.

  
```ts
const bridge = await v.audioBridge.attach(sid, {
  codec: "opus",
  onUplink: (frame, tsUs) => sink.write(frame),
});
```
  
  
```python
bridge = v.audio_bridge.attach(
    sid, on_uplink=lambda frame, ts_us: sink.write(frame), codec="opus",
)
```
  

> **TIP:**
> `tsUs` / `ts_us` is the frame's presentation timestamp in microseconds. Feed it
> to a recognizer that supports word-level timing. Or use it to align the
> transcript with a recording keyed on the same `call_sid`.

## Related

  - **[Send TTS audio back](/modalities/voice/cookbooks/tts-back)** — Push synthesized audio to the caller with `publishDownlink`.
  - **[BYO ASR / LLM / TTS](/modalities/voice/runtime/byo-asr-llm-tts)** — Let the runtime assemble a managed cascade from one config file.
  - **[Turn detection](/modalities/voice/concepts/turn-detection)** — VAD and barge-in: decide when a caller's turn is over.
  - **[Sessions, calls & tracks](/modalities/voice/concepts/sessions-calls-tracks-streams)** — How `voice/<sid>/uplink` and `downlink` tracks are addressed.
