---
name: baichuan-talk-sequence
description: Canonical Baichuan two-way-audio (talk) command order, IMA ADPCM block framing math, and the three intercom fine-tuning settings.
metadata:
  type: reference
---

# Baichuan talk (two-way audio) — canonical sequence & framing

Source of truth: `reolink-baichuan-js` (`@apocaliss92/nodelink-js`).
- `src/reolink/baichuan/utils/talkConfig.ts` — build session info + TalkConfig.
- `src/reolink/baichuan/utils/talkSession.ts` — buffered send pump + stop.
- `src/reolink/baichuan/utils/talk.ts` — TalkConfig XML + `encodeBcMediaAdpcmBlock`.
- `ReolinkBaichuanApi.createDedicatedTalkSession` (recommended; own pooled socket).

## Command order
1. `getTalkAbility(channel)` — cmd `BC_CMD_ID_TALK_ABILITY`. Returns
   `duplexList`, `audioStreamModeList`, `audioConfigList`.
2. Pick `audioConfig`: prefer entry with `audioType === 'adpcm'`, else first.
3. `sendTalkConfigWithReset` — cmd `BC_CMD_ID_TALK_CONFIG` (201), class
   `BC_CLASS_MODERN_24`. On responseCode **422**, send `BC_CMD_ID_TALK_RESET`
   (11) then retry config once; must end 200.
4. Stream audio: cmd `BC_CMD_ID_TALK` (binary payload, no reply) per payload
   of 1..N ADPCM blocks. Paced by `expectedStreamEndMs` (playLengthMs =
   samples/sampleRate).
5. Stop: flush remaining (pad last block with 0xff to `fullBlockSize`),
   wait out the remaining play time, then `BC_CMD_ID_TALK_RESET` (must be 200).

UDP/battery: `channelIdOverride = channel` (0-based header channelId).

## Block framing (the load-bearing math)
- `blockSize = floor(audioConfig.lengthPerEncoder / 2)`  (a.k.a. halfBlockSize)
- `fullBlockSize = blockSize + 4`  (4-byte IMA block header: int16 predictor +
  uint8 index + uint8 reserved)
- `samplesPerBlock = blockSize * 2 + 1`  (the +1 is the header predictor sample)
- PCM bytes needed per block = `samplesPerBlock * 2` (s16le)
- Wire wrapper per block (`encodeBcMediaAdpcmBlock`): magic `0x62773130` ("bw10")
  + payload_size(u16)×2 + `0x0100`(u16) + halfBlockSize(u16) + block, padded to
  8-byte multiple.

Validate at session open: `fullBlockSize === blockSize + 4` and
`sampleRate > 0` (Reolink typical 16000 Hz; block ≈ 64 ms).

## Input pipeline
Incoming WebRTC/mic audio is Opus → decode to **PCM s16le @ camera sampleRate,
mono** BEFORE IMA ADPCM encoding. `sampleRate` comes from
`session.info.audioConfig.sampleRate` (read AFTER start, not assumed).
Critical: if the decoder defaults to f32le, the talk session reads it as
Int16Array → pure noise. Pin s16le explicitly.

## Fine-tuning settings (Scrypted `intercom-mixin.ts` ⇄ camstack schema parity)
Same names, same ranges in both projects:
- `intercomBlocksPerPayload` — int [1,8], default 1. Lower = lower latency, more
  packets/sec. Clamped by lib to [1,8].
- `intercomMaxBacklogMs` — int [20,5000], default 120. PCM backlog cap; drops
  OLDEST samples (aligned to 16-bit) when exceeded → "backlog clamped" warning.
  `maxBacklogBytes = floor(ms/1000 * sampleRate * 2)`, floored at one block.
- `intercomGain` — number [0.1,10], default 1.0. Output gain applied to s16le
  PCM (hard-clipped) BEFORE encode (post-encode would corrupt ADPCM predictor).
  In Scrypted it's an ffmpeg `-filter:a volume=N`; in camstack a JS Int16 multiply.

Related: [[camstack-probe-gated-cap-registration]].
