接收与录音¶

vc.receive() 会打开一个接收器:一个有界的异步迭代器,遍历机器人听到的音频。每位说话者的音频单独到达,已在 Rust 中解密、重新排序并解码。一个连接上可以同时打开多个接收器,每个都有自己的格式和滤镜。
import logging
import bostie
from bostie import Context, voice
log = logging.getLogger("my-bot")
bot = bostie.Bot()
@bot.command
async def meter(ctx: Context, seconds: int = 10) -> None:
"""Log how loud each speaker is for a few seconds."""
vc = ctx.voice.current or await ctx.voice.connect()
await ctx.respond("Listening.")
loudest: dict[int, float] = {}
async with vc.receive(voice.PCM(rate=16_000, channels=1)) as frames:
async for frame in frames:
if frame.user_id is not None and frame.level is not None:
level = max(loudest.get(frame.user_id, frame.level), frame.level)
loudest[frame.user_id] = level
if frame.offset >= seconds * 48_000:
break
for user_id, level in loudest.items():
log.info("user %d peaked at %.1f dBFS", user_id, level)
@bot.command
async def transcribe(ctx: Context) -> None:
"""Collect what each person says, one utterance at a time."""
vc = ctx.voice.current or await ctx.voice.connect()
await ctx.respond("Transcribing until the bot leaves.")
rx = vc.receive(voice.PCM(rate=16_000, channels=1), markers=True)
async for said in voice.utterances(rx, min_silence=0.6, max_length=20.0):
log.info(
"user %d spoke %.1f s (%d bytes of 16 kHz mono)",
said.user_id,
said.duration,
len(said.data),
)
格式¶
vc.receive(format) 根据一个 voice.ReceiveFormat 返回一个 voice.Receiver:
vc.receive(format) |
产出项 | 用途 |
|---|---|---|
voice.PCM(rate=48000, channels=2, sample="s16le") |
该格式的已解码采样组成的 Frame;采样率为 voice.DECODE_RATES 之一,即 8、12、16、24 或 48 kHz |
收听、语音识别、电平 |
voice.Opus() |
data 为一个 Opus 包的 Frame |
无需解码的录音或转发 |
voice.Packets() |
已解密、按到达顺序排列、未经抖动缓冲区的 Packet |
你自己的抖动缓冲区、RTP 头部扩展 |
Frame 带有 user_id、ssrc、RTP timestamp、基于接收器 48 kHz 时钟的 offset 和 samples、received_at、data、lost(对于 Opus 为掩盖丢包而生成的音频为真),PCM 还带有 level,即该帧以 dBFS 表示的 RMS。时钟从接收器打开时开始计时,因此不同用户的帧可以按 offset 对齐。Packet 带有 user_id、ssrc、sequence、timestamp、received_at、opus 载荷以及 RTP 头部 extensions。
选择收听谁¶
users=[...]只保留这些用户,exclude=[...]排除某些用户。include_self=True加入机器人自己的音频。mix=True(PCM 和 Opus)传递一条将所有人混在一起的流,user_id设为None。markers=True会在帧之间插入SpeechStart、SpeechStop和UserLeave事件,每个都带有基于同一时钟的offset,因此消费者无需自己计时就能知道说话从哪里开始、到哪里结束。
设置¶
vc.receive 接受以下关键字参数;Packets() 没有抖动缓冲区,所以不接受 jitter。
| 设置 | 默认值 | 含义 |
|---|---|---|
buffer |
Defaults.receive_buffer,2.0 |
接收器为较慢的消费者保留的产出项秒数,0.02 到 60 |
overflow |
Defaults.overflow,"drop_oldest" |
缓冲区满时:丢弃最早的产出项,或 "drop_newest" |
jitter |
Defaults.jitter,0.1 |
继续播放前等待迟到数据包的秒数,0 到 1;仅限 PCM 和 Opus |
stall |
Defaults.receive_stall,"wait" |
连接重连时:"wait" 保持接收器打开,"end" 结束它 |
读取¶
async for item in rx 会等待产出项,并在接收器结束时停止。async with rx: 在代码块结束时关闭它。
rx.get_nowait()返回下一个产出项,没有排队的产出项时引发voice.Empty,接收器已结束时引发voice.ReceiverClosed。rx.queued是最早排队的产出项已等待的时长:即消费者的延迟。rx.clear()丢弃所有排队的内容,并继续实时接收。await rx.mark()在调用之前接收到的音频都已排队或被丢弃时返回True,或在 1.2 秒加上两倍 jitter 之后返回False;取消某一次等待不会影响其他等待的标记。rx.stats()返回一个ReceiverStats:已传递(delivered)的产出项、因溢出而丢弃(dropped)的产出项,以及途中丢失(lost)的数据包。rx.close()停止接收。rx.closed_reason是一个voice.ClosedReason,说明接收器结束的原因:CLOSED、DISCONNECTED、MOVED(机器人换了频道)或RECONNECTING(在stall="end"时)。
版主可以将机器人拒听,此时 Discord 不会向它发送任何音频;在机器人被拒听时打开接收器,或在接收器打开时机器人被拒听,bostie 都会记录一条警告。
中继¶
接收器和 sink 可以直接相连,把音频从一个通话传到另一个通话:
from bostie import voice
async def relay_call(
source: voice.VoiceConnection, target: voice.VoiceConnection
) -> None:
async with source.receive(voice.PCM(), mix=True) as rx, target.sink() as out:
async for frame in rx:
await out.write(frame.data)
async def relay_speaker(
source: voice.VoiceConnection, target: voice.VoiceConnection, user_id: int
) -> None:
async with (
source.receive(voice.Opus(), users=[user_id]) as rx,
target.sink(voice.Opus()) as out,
):
async for frame in rx:
if not frame.lost:
await out.write(frame.data)
混合帧没有 user_id、ssrc 或 timestamp,混音流也无法携带标记。丢失的 Opus 帧的 data 为空,因为没有可以传递的数据包;在写入 Opus sink 之前请跳过它,就像 relay_speaker 那样。两端默认都是 48 kHz 立体声 16 位 PCM,因此 PCM 接收器可以直接写入默认的 sink。
说话检测¶
只要 speech_activity 开启,每个连接上都会在 Rust 中运行说话检测:用户的第一段音频即表示开始说话,静默 speech_hangover 秒(默认 0.2)后视为停止。vc.speaking 保存当前正在说话的人,SpeechStart 和 SpeechStop 事件把它报告给监听器,以 markers=True 打开的接收器则把它带在自己的流中。当 speech_activity 关闭且没有打开的接收器时,bostie 甚至不会解密收到的音频。
voice.utterances(rx, min_silence=0.8, max_length=30.0) 把 PCM 接收器的帧分组为 Utterance:一位用户从开始到结束的一段话,带有接收器格式的 data、其 offset、duration 以及丢失(lost)的秒数。语段在没有说话持续 min_silence 秒后,或达到 max_length 秒时结束。它需要一个不带 mix、以 markers=True 打开的 PCM 接收器,或者一个以 dBFS 表示的 level,此时由它代替标记来决定什么算作说话。
录音¶
import bostie
from bostie import Context, voice
bot = bostie.Bot()
@bot.command
async def record_call(ctx: Context, minutes: float = 5.0) -> None:
"""Record the whole call as one WAV file, for at most a few minutes."""
vc = ctx.voice.current or await ctx.voice.connect()
await ctx.respond("Recording. Everyone in the call can see the bot.")
recorder = voice.Recorder(
vc,
"recordings/{guild_id}/call.wav",
mix=True,
max_duration=minutes * 60,
max_size=200_000_000,
)
files = await recorder.wait()
await ctx.followup(f"Saved {files[None]}; {recorder.stats().lost} packets lost.")
voice.Recorder(vc, path) 把每位用户录制到各自的文件中,或者在 mix=True 时把整个通话录制到一个文件中。路径是一个包含 {guild_id} 和 {user_id} 的模板,每位用户一个文件时 {user_id} 是必需的,mix=True 时则不允许使用,路径的扩展名决定容器格式:
.ogg或.opus写入 Ogg Opus。按用户录制时,收到的 Opus 包会原样写入,无需解码。.wav以录音器的format写入 16 位 PCM,默认为 48 kHz 立体声;其采样率为voice.DECODE_RATES之一。
默认的 align=True 会用静音填补空隙,因此每个文件都从录音开始时开始,各文件可以对齐。users、exclude 和 include_self 选择录制谁,与接收器相同;buffer(30 秒)和 jitter(0.3 秒)比接收器的默认值更大,因为磁盘可能会卡顿。录音会在 max_duration 秒后、文件达到 max_size 字节后,或连接结束时自行结束。
await recorder.stop() 会在已听到的音频写入完毕后结束录音,并以从用户 ID 到 Path 的字典形式返回文件,混音文件的键为 None。await recorder.wait() 等待它自行结束。recorder.files 列出到目前为止写入的文件,recorder.stats() 返回接收器的计数器,其中 dropped 表示磁盘落后的时间超过了 buffer。
请告诉通话中的人机器人正在录音,就像示例代码那样。