0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

送信側(PCM/μ-law → RTP)と 20ms 刻み送信の現状メモ

Posted at

12/18 までで「受信→録音」を押さえ、12/19 でジッタ改善案を固めた。今日は逆側、送信(WAV/PCM → PCMU → RTP) が現状どう組まれているかと、20ms刻み送信の実装をメモする。


全体の送信フロー(現行)

app/tts (WAVファイル)
   ↓ SessionIn::AppBotAudioFile
session/session.rs: send_wav_as_rtp_pcmu
   ↓ PCMUフレーム化(20ms相当) + RTPパケット生成
rtp::tx::RtpTxHandle
   ↓ UDP送信(transport経由)
UAC

送信のキーは「PCMU/8000固定」「20ms刻みでフレーム化」「Seq/Timestamp を増やしつつ RTP で吐く」。


1) WAV→PCMUフレーム化と送信(session)

// src/session/session.rs:502-
async fn send_wav_as_rtp_pcmu(
    call_id: &str,
    rtp_tx: &RtpTxHandle,
    dst_ip: &str,
    dst_port: u16,
    wav_path: &str,
) -> Result<()> {
    let frames = load_wav_as_pcmu_frames(wav_path)?;
    let mut seq = 0u16;
    let mut ts = 0u32;
    for frame in frames {
        let pkt = build_rtp_packet(0, seq, ts, 0x1234_5678, frame)?;
        rtp_tx.send(dst_ip, dst_port, pkt).await?;
        seq = seq.wrapping_add(1);
        ts = ts.wrapping_add(160); // 20ms * 8000Hz = 160 samples
        tokio::time::sleep(Duration::from_millis(20)).await;
    }
    Ok(())
}
  • build_rtp_packet(pt=0, seq, ts, ssrc=0x12345678, payload=pcmu) で RTP を作成(rtp/builder.rs)。
  • 20msごとに送信(sleep 20ms)。Timestamp は 160刻み(8000Hz × 0.02s)。
  • Seq はラップアラウンド考慮で wrapping_add

WAV読み込みと PCMU 変換

// src/session/session.rs:532-
fn load_wav_as_pcmu_frames(path: &str) -> Result<Vec<Vec<u8>>, Error> {
    let mut reader = hound::WavReader::open(path)?;
    let spec = reader.spec();
    if spec.sample_rate != 8000 || spec.channels != 1 {
        return Err(anyhow!("WAV must be mono/8k for PCMU"));
    }
    let mut cur = Vec::new();
    let mut frames = Vec::new();
    for sample in reader.samples::<i16>() {
        let s = sample?;
        let mu = linear16_to_mulaw(s);
        cur.push(mu);
        if cur.len() >= 160 { // 20ms @ 8k
            frames.push(cur);
            cur = Vec::new();
        }
    }
    if !cur.is_empty() {
        frames.push(cur);
    }
    Ok(frames)
}
  • 8k/mono の WAV を前提に 16bit PCM を μ-law へ変換し、160サンプルごとに 1 フレームに分割。
  • 余りフレームもそのまま送る(パディングはしない)。
  • μ-law変換は linear16_to_mulaw(同ファイル末尾)。

2) RTPパケット生成(rtp::builder)

// src/rtp/builder.rs:6-
pub fn build_rtp_packet(
    payload_type: u8,
    sequence_number: u16,
    timestamp: u32,
    ssrc: u32,
    payload: Vec<u8>,
) -> Result<Vec<u8>, Error> {
    let mut out = Vec::with_capacity(12 + payload.len());
    let b0 = (2 << 6) | (0 << 5) | (0 << 4) | 0; // V=2, P=0, X=0, CC=0
    let b1 = payload_type & 0b0111_1111; // M=0, PT=payload_type
    out.push(b0);
    out.push(b1);
    out.extend_from_slice(&sequence_number.to_be_bytes());
    out.extend_from_slice(&timestamp.to_be_bytes());
    out.extend_from_slice(&ssrc.to_be_bytes());
    out.extend_from_slice(&payload);
    Ok(out)
}
  • ヘッダ12バイト固定(拡張/CSRCなし)。Markerは常に0。
  • PT は呼び出し元指定(送信時は PCMU=0)。

3) RTP送信ハンドル(rtp::tx::RtpTxHandle)

// src/rtp/tx.rs:11-
#[derive(Clone)]
pub struct RtpTxHandle {
    tx: Arc<Mutex<UdpSocket>>,
}

impl RtpTxHandle {
    pub fn new() -> Self { ... } // bind(0.0.0.0:0)

    pub async fn send(&self, dst_ip: &str, dst_port: u16, pkt: Vec<u8>) -> std::io::Result<()> {
        let addr = format!("{}:{}", dst_ip, dst_port);
        let sock = self.tx.lock().unwrap();
        sock.send_to(&pkt, addr).await?;
        Ok(())
    }

    pub fn stop(&self, _call_id: &str) { /* no-op (将来RTCP等で拡張予定) */ }
}
  • 現状はシンプルに UdpSocket::send_to。ソケットは呼び出し時に共有(Mutex)。
  • SSRC/Seq の管理は送信呼び出し側(session)が持つ。

4) 送信トリガー(session内の経路)

  • SessionIn::AppBotAudioFile { path } を受けたら send_wav_as_rtp_pcmu を非同期で実行(session/session.rs 内)。
  • 宛先はセッション確立時に握った peer_rtp_dst()(SDPの c=/m=audio から取得)。
  • 送信中フラグ sending_audio で重複再生を防止。再生終了後に capture_window を開始。

5) 現状の割り切り / 改善余地

  • コーデックは PCMU 固定(PT=0)。Marker ビットは常に 0。
  • 20ms固定スリープで送るので、Tokio スケジューラの遅延が乗る場合がある(本格的には interval/timer wheel で精度を上げる余地)。
  • Seq/TS/SSRC は送信ごとに初期化(長時間/再送考慮はしていない)。
  • パケット化は拡張ヘッダ・RTPイベントなし(DTMF/SID 等は未対応)。

まとめ(12/20時点)

  • WAV(8k/mono) を μ-law にし、160サンプルごとにフレーム化→RTP化→20msごとに送信。
  • RTPパケットは最小ヘッダ(V=2/P=0/X=0/CC=0, M=0, PT=0)。
  • 送信ハンドルはシンプルな UDP 直送、Seq/TS は session 側で管理。
  • 今後の改善候補: Timer精度向上、SSRC/Seqの継続管理、Marker/SID/DTMF対応、PT可変化(他コーデック対応)。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?