Zum Inhalt

Sinks und rohes Audio

Ein Sink ist ein begrenzter Puffer, in den Python Audio schreibt und aus dem der Rust-Mixer liest, jeweils 20 ms auf einmal. Jeder Track und jede Warteschlange spielt über einen. Öffne einen eigenen für Audio, das du erzeugst oder von anderswo bekommst: ein Synthesizer, eine Text-to-Speech-Engine, ein Stream aus einem anderen Programm.

import math
import struct

import bostie
from bostie import Context, voice

bot = bostie.Bot()
RATE = 24_000


def tone(frequency: float, seconds: float) -> bytes:
    count = int(RATE * seconds)
    samples = (
        int(8000 * math.sin(2 * math.pi * frequency * index / RATE))
        for index in range(count)
    )
    return struct.pack(f"<{count}h", *samples)


@bot.command
async def beep(ctx: Context) -> None:
    """Play three generated beeps."""
    vc = ctx.voice.current or await ctx.voice.connect()
    await ctx.respond("Beeping.")
    async with vc.sink(voice.PCM(rate=RATE, channels=1), volume=0.5) as sink:
        for frequency in (440.0, 554.4, 659.3):
            await sink.write(tone(frequency, 0.3))
            await sink.write(bytes(RATE // 10 * 2))
        done = sink.mark()
        await sink.write(tone(880.0, 1.0))
        if await done:
            await ctx.followup("The three beeps played; one long one to go.")
    await ctx.followup(f"Done: {sink.stats().underruns} underruns.")


@bot.command
async def opus(ctx: Context) -> None:
    """Encode in Python and send the packets as they are."""
    vc = ctx.voice.current or await ctx.voice.connect()
    encoder = voice.OpusEncoder(voice.PCM(rate=RATE, channels=1))
    await ctx.respond("Sending.")
    async with vc.sink(voice.Opus()) as sink:
        for packet in [*encoder.encode(tone(330.0, 2.0)), *encoder.flush()]:
            await sink.write(packet)

Formate

vc.sink(format) gibt einen voice.Sink für das zurück, was du schreiben wirst, ein voice.Format:

  • voice.PCM(rate=48000, channels=2, sample="s16le"): verschränkte Little-Endian-Samples, "s16le" (16-Bit-Ganzzahlen) oder "f32le" (32-Bit-Gleitkommazahlen), Mono oder Stereo, bei 8000 bis 192 000 Hz. bostie rechnet in Rust auf 48 kHz um; eine Rate muss sich in Blöcken von höchstens 960 Samples auf 48 kHz abbilden lassen, was jede gängige Rate tut.
  • voice.Opus(): ein Opus-Paket pro Schreibvorgang. Pakete von 20 ms bei 48 kHz Stereo werden so gesendet, wie sie geschrieben wurden, solange der Sink allein, mit Lautstärke 1 und ohne Filter spielt; alles andere wird dekodiert und gemischt.

voice.OpusEncoder(pcm_format, encoder=EncoderSettings()) kodiert PCM im Python-Prozess in solche Pakete, für Audio, das du einmal kodieren und an viele Sinks senden willst: encode(data) gibt jedes bis dahin vollständige Paket zurück, und flush() füllt den Rest mit Stille auf. Seine Bitrate beträgt standardmäßig 64 kbit/s, da es keinem Kanal folgen kann.

Schreiben

  • await sink.write(data) reiht Audio ein und wartet, solange der Sink bereits buffer Sekunden enthält. Dieses Warten ist der Gegendruck, der deinen Erzeuger im Takt der Echtzeit hält.
  • sink.write_nowait(data) reiht den ganzen Chunk auf einmal ein oder löst BufferFull aus.
  • async with sink: wartet am Ende des Blocks, bis alles Geschriebene abgespielt ist, und schließt dann den Sink; eine Exception im Block verwirft stattdessen, was eingereiht ist.
  • await sink.drain() wartet, bis alles bisher Geschriebene abgespielt ist; False, wenn ein Teil davon verworfen wurde.
  • sink.clear() verwirft, was eingereiht ist; clear(until=marker) nur den Teil vor einem Marker, clear(since=marker) nur den Teil danach.
  • sink.close() verwirft, was eingereiht ist, und schließt. Schreiben in einen geschlossenen Sink löst SinkClosed aus.

sink.mark() gibt einen Marker hinter dem bisher geschriebenen Audio zurück. await marker gibt True zurück, sobald alles Audio davor abgespielt ist, oder False, wenn vorher ein Teil verworfen wurde; marker.done sagt, ob das schon feststeht. Über Marker erfährt ein Player, wann ein Track wirklich zu Ende ist, nicht nur, wann er geschrieben wurde.

Einstellungen

vc.sink nimmt diese Schlüsselwortargumente; vc.play und voice.Queue nehmen ebenfalls buffer und stall.

Einstellung Standard Bedeutung
buffer Defaults.sink_buffer, 0.2 Sekunden, die der Sink hält, bevor write wartet, 0,02 bis 60
stall Defaults.stall, "hold" wenn sich Audio nicht senden lässt, während einer Neuverbindung oder eines DAVE-Übergangs: "hold" behält es, um es danach abzuspielen, "drop" verwirft es in Echtzeit, für Live-Audio, das nicht zurückfallen darf
volume 1.0 multipliziert die Samples; sink.volume ändert es während der Wiedergabe
filters Filters() die Effekte; sink.filters ändert sie während der Wiedergabe
after None ein anderer Sink dieser Verbindung; dieser bleibt stumm, bis jener schließt oder ausgespielt hat, was vor seinem letzten drain() geschrieben wurde
crossfade 0.0 mit after die Sekunden, um die früher begonnen und übergeblendet wird; der Sink puffert dann entsprechend mehr

Mit after und crossfade spielt eine Warteschlange lückenlos: Der Sink des nächsten Tracks füllt sich, während der aktuelle spielt, und übernimmt auf das Sample genau.

Zustand und Zähler

sink.queued sind die Sekunden wartenden Audios, sink.passthrough sagt, ob das letzte Paket unverändert hinausging, und sink.closed, ob er geschlossen ist. sink.stats() gibt ein SinkStats zurück:

Feld Wert
sent gesendete Sekunden Audio
position der Abspielkopf in Sekunden, so wie Marker zählen
underruns wie oft der Sink beim Abspielen leerlief, außerhalb eines drain(): Der Erzeuger ist zurückgefallen
held_ticks Takte, in denen der Sink zurückgehalten wurde, weil sich Audio nicht senden ließ
dropped Sekunden, die durch einen "drop"-Stillstand verworfen wurden
passthrough_packets Pakete, die unverändert gesendet wurden, ohne Dekodierung

Auf die Underruns solltest du achten: Ein Erzeuger, der nicht buffer Sekunden Vorsprung halten kann, ist zu langsam, oder die Event-Loop ist blockiert. vc.sinks listet die offenen Sinks einer Verbindung.