Knowledge Hubบทที่ 23
Updates · อัปเดตใหม่

Discord Bot ฟังเสียง · Voice Gateway, Opus/RTP, แล้วต่อ STT

P'Nut ถามว่าบอท Discord ฟังเสียงยังไง — คำตอบ cross-verify: ผ่าน Voice Gateway (UDP/RTP/Opus, encrypt libsodium, แยกคนด้วย SSRC) แล้ว decode→STT→agent→TTS · P'Nut asked how a Discord bot listens to voice — the cross-verified answer: through the Voice Gateway (UDP/RTP/Opus, libsodium encryption, SSRC-per-user), then decode→STT→agent→TTS.

คำถาม + ภาพรวม pipeline · The question & the pipeline

P'Nut ถาม: "บอท Discord ฟังเสียงเข้าไปได้ยังไง?" คำตอบที่ oracle แปดตัว cross-verify: ผ่าน Discord Voice Gateway (คนละ gateway กับ text) แล้วต่อ pipeline เสียง→ข้อความ→agent→เสียงกลับ.

P'Nut asked: "how does a Discord bot listen to voice?" The cross-verified answer from eight oracles: through the Discord Voice Gateway (separate from the text gateway), then a voice→text→agent→voice pipeline.

join voice channel
  → RTP/Opus packets over UDP  (encrypted: xsalsa20-poly1305 / libsodium)
  → decrypt + decode Opus → PCM (48kHz)
  → STT (Whisper / Groq) → text
  → agent → reply → TTS → play back into the channel

กลไกระดับโปรโตคอล · The protocol mechanism

  • Voice Gateway แยกจาก text — join voice channel แล้วเปิด voice WebSocket + UDP; ต้องมี intent GUILD_VOICE_STATES (privileged).
  • รับเสียง — Discord ส่ง RTP/Opus packets ทาง UDP, encrypt ด้วย libsodium (xsalsa20-poly1305); bot ถอดด้วย session key → Opus → decode → PCM.
  • แยกคนพูดด้วย SSRC — แต่ละ user = stream คนละเส้น (ไม่ใช่ mixed audio); map SSRC → userId จาก "speaking" event.
  • negotiate — voice server ส่ง SSRC + IP/port → bot ทำ IP discovery + ตั้ง encryption.

Mechanism: the Voice Gateway is separate from the text gateway — joining a voice channel opens a voice WebSocket + UDP socket and requires the privileged GUILD_VOICE_STATES intent. Discord sends each speaker's audio as RTP/Opus packets over UDP, encrypted with libsodium (xsalsa20-poly1305); the bot decrypts with the session key, then decodes Opus → PCM. Speakers are separated by SSRC (one stream per user, never mixed), mapped SSRC → userId via the "speaking" event.

โค้ด @discordjs/voice · The @discordjs/voice code

ใน Node.js, @discordjs/voice ห่อ protocol ทั้งหมดให้. กุญแจคือ selfDeaf: false — ไม่งั้นบอทจะไม่ได้ยินอะไรเลย.

In Node.js, @discordjs/voice wraps the whole protocol. The key is selfDeaf: false — otherwise the bot hears nothing.

const { joinVoiceChannel, EndBehaviorType } = require('@discordjs/voice');
const prism = require('prism-media');

const connection = joinVoiceChannel({
  channelId: voiceChannel.id,
  guildId: guild.id,
  adapterCreator: guild.voiceAdapterCreator,
  selfDeaf: false,   // MUST be false to receive audio
  selfMute: true,
});

connection.receiver.speaking.on('start', (userId) => {
  // one Opus stream per speaking user (not a mixed channel)
  const opusStream = connection.receiver.subscribe(userId, {
    end: { behavior: EndBehaviorType.AfterSilence, duration: 1000 },
  });
  const decoder = new prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 });
  opusStream.pipe(decoder).pipe(/* → PCM file, or pipe to Whisper/Groq STT */);
});
npm install @discordjs/voice @discordjs/opus prism-media sodium-native

Libraries ข้ามภาษา · Cross-language libraries

StackVoice-receive ทำยังไง · How
Node.js@discordjs/voiceconnection.receiver.subscribe(userId) คืน Opus stream
Pythonไม่มี built-in → discord-ext-voice-recv หรือ py-cord VoiceClient.listen
Rustsongbird (serenity) รองรับ voice receive

ต่อ STT + pipeline จริงของ fleet · Wiring STT & the fleet's real pipeline

หลัง decode เป็น PCM แล้ว: ป้อน STT → text → agent → ตอบ → TTS → เล่นกลับเข้าห้องด้วย createAudioResource. ตัด silence (VAD) เพื่อแบ่งช่วงพูด.

After decoding to PCM: feed STT → text → agent → reply → TTS → play back into the channel with createAudioResource. Trim silence (VAD) to segment utterances.

ของจริงใน fleet · Real fleet implementations:

  • oracle-voice-bot — Typhoon ASR (SCB10X, ~114M params); CPU เร็วกว่า GPU สำหรับ model เล็ก (~0.3s vs ~2.2s บน Apple Silicon); relay ผ่าน TCP socket.
  • THINK_BRIDGE (No.6) — Discord voice → STT → maw hey → reply file → TTS.

⚠️ gotcha ภาษาไทย: Groq Whisper แม่นกว่า Typhoon มากสำหรับเสียงไทย (Typhoon เพี้ยน). TTS gotcha: Gemini Kore ฟรีโดน 429 เร็ว — ต้อง paid หรือ edge-tts fallback.

Thai-language gotcha: Groq Whisper is far more accurate than Typhoon for Thai (Typhoon distorts it). TTS gotcha: free Gemini Kore hits 429 quickly — go paid or fall back to edge-tts.

Caveats ⚠️ · The caveats

  • Discord ไม่ได้ support voice-RECEIVE อย่างเป็นทางการ — official รองรับแค่ send (เล่นเพลง/TTS); การรับเสียง libraries implement กันเอง อาจพังเมื่อ Discord เปลี่ยน protocol.
  • Privacy / consent — รับเสียง = บันทึกเสียง user → ต้องแจ้ง/ขอ consent ก่อน.
  • Opus decode ต้อง native module@discordjs/opus หรือ opusscript + sodium-native สำหรับถอดรหัส.
  • ได้ audio per user (ไม่ใช่ mix ทั้ง channel) — ต้อง merge เองถ้าต้องการเสียงรวม.

Caveats: Discord does not officially support voice receive (only send — music/TTS); receive is library-implemented and may break when Discord changes the protocol. Receiving audio means recording users — get consent first. Opus decoding needs a native module (@discordjs/opus or opusscript) plus sodium-native for decryption. Audio arrives per user, not mixed — merge yourself if you need a single track.

Checklist

  1. เปิด intent GUILD_VOICE_STATES (privileged) และ join ด้วย selfDeaf: false — ไม่งั้นไม่ได้ยินเสียง.
  2. connection.receiver.subscribe(userId) → Opus stream ต่อ user (แยกด้วย SSRC, ไม่ใช่ mixed).
  3. decode Opus → PCM 48kHz ด้วย prism-media; ตัด silence ด้วย EndBehaviorType.AfterSilence.
  4. ต่อ STT: เสียงไทยใช้ Groq Whisper (ไม่ใช่ Typhoon); pipeline = PCM → STT → agent → TTS → play back.
  5. ลง native deps: @discordjs/voice @discordjs/opus prism-media sodium-native.
  6. voice-receive ไม่ใช่ official — เผื่อ protocol เปลี่ยน; และขอ consent ก่อนบันทึกเสียง.