Empfangen und aufnehmen¶

vc.receive() öffnet einen Receiver: einen begrenzten asynchronen Iterator über das Audio, das der Bot hört. Jeder Sprecher kommt einzeln an, entschlüsselt, wieder in Reihenfolge gebracht und in Rust dekodiert. An einer Verbindung können mehrere Receiver offen sein, jeder mit eigenem Format und eigenen Filtern.
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),
)
Formate¶
vc.receive(format) gibt einen voice.Receiver für ein voice.ReceiveFormat zurück:
vc.receive(format) |
Elemente | Für |
|---|---|---|
voice.PCM(rate=48000, channels=2, sample="s16le") |
Frames mit dekodierten Samples in diesem Format; die Rate ist eine der voice.DECODE_RATES, 8, 12, 16, 24 oder 48 kHz |
Zuhören, Spracherkennung, Pegel |
voice.Opus() |
Frames, deren data ein Opus-Paket ist |
Aufnehmen oder Weiterleiten ohne Dekodieren |
voice.Packets() |
Packets, entschlüsselt, in Ankunftsreihenfolge, vor dem Jitter-Puffer |
deinen eigenen Jitter-Puffer, RTP-Header-Erweiterungen |
Ein Frame hat user_id, ssrc, den RTP-timestamp, offset und samples auf der 48-kHz-Uhr des Receivers, received_at, data, lost (wahr für Audio, das Opus erfunden hat, um ein verlorenes Paket zu kaschieren) und bei PCM level, den RMS des Frames in dBFS. Die Uhr startet, wenn der Receiver öffnet, sodass sich Frames verschiedener Benutzer über offset ausrichten lassen. Ein Packet hat user_id, ssrc, sequence, timestamp, received_at, die opus-Nutzdaten und die RTP-Header-extensions.
Auswählen, wen du hörst¶
users=[...]behält nur diese Benutzer, undexclude=[...]lässt einige weg.include_self=Truefügt das eigene Audio des Bots hinzu.mix=True(PCM und Opus) liefert einen Stream, in dem alle gemischt sind, mituser_idaufNone.markers=TruesetztSpeechStart-,SpeechStop- undUserLeave-Events zwischen die Frames, jeweils mit ihremoffsetauf derselben Uhr, sodass ein Verbraucher weiß, wo Sprache beginnt und endet, ohne selbst zu messen.
Einstellungen¶
vc.receive nimmt diese Schlüsselwortargumente; Packets() hat keinen Jitter-Puffer und nimmt daher kein jitter.
| Einstellung | Standard | Bedeutung |
|---|---|---|
buffer |
Defaults.receive_buffer, 2.0 |
Sekunden an Elementen, die der Receiver für einen langsamen Verbraucher hält, 0,02 bis 60 |
overflow |
Defaults.overflow, "drop_oldest" |
wenn der Puffer voll ist: die ältesten Elemente verwerfen, oder "drop_newest" |
jitter |
Defaults.jitter, 0.1 |
Sekunden, die auf verspätete Pakete gewartet wird, bevor es weitergeht, 0 bis 1; nur PCM und Opus |
stall |
Defaults.receive_stall, "wait" |
wenn sich die Verbindung neu verbindet: "wait" hält den Receiver offen, "end" beendet ihn |
Lesen¶
async for item in rx wartet auf Elemente und hört auf, wenn der Receiver endet. async with rx: schließt ihn, wenn der Block endet.
rx.get_nowait()gibt das nächste Element zurück, oder löstvoice.Emptyaus, wenn nichts eingereiht ist, undvoice.ReceiverClosed, wenn der Receiver beendet ist.rx.queuedist, wie lange das älteste eingereihte Element schon wartet: der Rückstand des Verbrauchers.rx.clear()verwirft alles Eingereihte und macht live weiter.await rx.mark()gibtTruezurück, sobald das vor dem Aufruf empfangene Audio eingereiht oder verworfen ist, oderFalsenach 1,2 Sekunden plus dem doppelten Jitter; das Abbrechen eines Wartens lässt die Marke für die anderen bestehen.rx.stats()gibt einReceiverStatszurück:deliveredgelieferte Elemente, durch Überlaufdroppedverworfene und unterwegslostverlorene Pakete.rx.close()beendet den Empfang.rx.closed_reason, einvoice.ClosedReason, sagt, warum ein Receiver endete:CLOSED,DISCONNECTED,MOVED(der Bot hat den Kanal gewechselt) oderRECONNECTING(mitstall="end").
Ein Moderator kann den Bot taubschalten, und Discord sendet ihm dann kein Audio; bostie loggt eine Warnung, wenn ein Receiver öffnet, während der Bot taubgeschaltet ist, und wenn der Bot taubgeschaltet wird, während Receiver offen sind.
Weiterleiten¶
Ein Receiver und ein Sink lassen sich direkt verbinden, um Audio von einem Anruf in einen anderen zu tragen:
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)
Ein gemischter Frame hat weder user_id noch ssrc oder timestamp, und ein Mix kann keine Marker tragen. Ein verlorener Opus-Frame hat leere data, weil es kein Paket zum Weiterreichen gibt; überspring ihn, bevor du in einen Opus-Sink schreibst, wie es relay_speaker tut. Beide Enden verwenden standardmäßig 48-kHz-Stereo-16-Bit-PCM, sodass ein PCM-Receiver direkt in einen Standard-Sink schreibt.
Sprache¶
Die Spracherkennung läuft auf jeder Verbindung in Rust, solange speech_activity eingeschaltet ist: Ein Benutzer beginnt mit seinem ersten Audio zu sprechen und hört nach speech_hangover Sekunden Stille auf, standardmäßig 0,2. vc.speaking enthält, wer gerade spricht, die Events SpeechStart und SpeechStop melden es an Listener, und Receiver, die mit markers=True geöffnet wurden, tragen es in ihrem Stream. Ist speech_activity aus und kein Receiver offen, entschlüsselt bostie das eingehende Audio nicht einmal.
voice.utterances(rx, min_silence=0.8, max_length=30.0) gruppiert die Frames eines PCM-Receivers zu Utterances: die Sprache eines Benutzers von Anfang bis Ende, mit data im Format des Receivers, ihrem offset, duration und den lost verlorenen Sekunden. Eine Äußerung endet nach min_silence Sekunden ohne Sprache oder bei max_length Sekunden. Es braucht einen PCM-Receiver ohne mix, der mit markers=True geöffnet wurde, oder einen level in dBFS, der dann statt der Marker entscheidet, was als Sprache zählt.
Aufnehmen¶
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) nimmt jeden Benutzer in einer eigenen Datei auf, oder mit mix=True den ganzen Anruf in einer. Der Pfad ist eine Vorlage mit {guild_id} und {user_id}, erforderlich für eine Datei pro Benutzer und bei mix=True abgelehnt, und seine Dateiendung bestimmt den Container:
.oggoder.opusschreibt Ogg Opus. Pro Benutzer werden die empfangenen Opus-Pakete unverändert geschrieben, ohne Dekodierung..wavschreibt 16-Bit-PCM imformatdes Recorders, standardmäßig 48 kHz Stereo; seine Rate ist eine dervoice.DECODE_RATES.
Mit align=True, dem Standard, füllt Stille die Lücken, sodass jede Datei beginnt, als die Aufnahme begann, und die Dateien zueinander passen. users, exclude und include_self wählen wie bei einem Receiver, wer aufgenommen wird, und buffer (30 Sekunden) und jitter (0,3 Sekunden) sind größer als die Standardwerte eines Receivers, weil eine Festplatte ins Stocken geraten kann. Die Aufnahme endet von selbst nach max_duration Sekunden, sobald ihre Dateien max_size Bytes enthalten, oder wenn die Verbindung endet.
await recorder.stop() beendet sie, sobald das bereits gehörte Audio geschrieben ist, und gibt die Dateien als Dict von Benutzer-ID auf Path zurück, mit None als Schlüssel einer gemischten Datei. await recorder.wait() wartet, bis sie von selbst endet. recorder.files listet die bisher geschriebenen Dateien und recorder.stats() die Zähler des Receivers, wobei dropped bedeutet, dass die Festplatte länger als buffer zurückgefallen ist.
Sag den Leuten im Anruf, dass der Bot sie aufnimmt, wie es das Snippet tut.