Sinks y audio sin procesar¶
Un sink es un búfer acotado en el que Python escribe audio y del que el mezclador de Rust lee, de a 20 ms. Cada pista y cada cola se reproduce a través de uno. Abre uno propio para el audio que generas o que recibes de otro lugar: un sintetizador, un motor de texto a voz, un stream de otro programa.
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)
Formatos¶
vc.sink(format) devuelve un voice.Sink para lo que vas a escribir, un voice.Format:
voice.PCM(rate=48000, channels=2, sample="s16le"): muestras intercaladas en little-endian,"s16le"(enteros de 16 bits) o"f32le"(flotantes de 32 bits), mono o estéreo, de 8000 a 192 000 Hz. bostie remuestrea a 48 kHz en Rust; una frecuencia tiene que dividir los 48 kHz en bloques de como máximo 960 muestras, algo que cumplen todas las frecuencias comunes.voice.Opus(): un paquete Opus por escritura. Los paquetes de 20 ms a 48 kHz estéreo se envían tal como se escribieron mientras el sink se reproduce solo, a volumen 1 y sin filtros; cualquier otra cosa se decodifica y se mezcla.
voice.OpusEncoder(pcm_format, encoder=EncoderSettings()) codifica PCM en esos paquetes dentro del proceso de Python, para el audio que quieres codificar una vez y enviar a muchos sinks: encode(data) devuelve todos los paquetes completos hasta el momento, y flush() rellena el resto con silencio. Su tasa de bits es de 64 kbps por defecto, porque no tiene ningún canal al que seguir.
Escribir¶
await sink.write(data)pone audio en la cola, y espera mientras el sink ya contienebuffersegundos. Esa espera es la contrapresión que mantiene a tu productor al ritmo del tiempo real.sink.write_nowait(data)pone en la cola el bloque entero de una vez, o lanzaBufferFull.async with sink:espera al final del bloque hasta que todo lo escrito se reprodujo y luego cierra el sink; una excepción en el bloque descarta en cambio lo que está en la cola.await sink.drain()espera hasta que todo lo escrito hasta ahora se reprodujo;Falsesi se descartó una parte.sink.clear()descarta lo que está en la cola;clear(until=marker)solo la parte anterior a un marcador,clear(since=marker)solo la parte posterior.sink.close()descarta lo que está en la cola y lo cierra. Escribir en un sink cerrado lanzaSinkClosed.
sink.mark() devuelve un Marker ubicado después del audio escrito hasta el momento. await marker devuelve True cuando se reprodujo todo el audio anterior, o False si antes se descartó una parte; marker.done indica si eso ya se sabe. Con los marcadores, un reproductor sabe cuándo terminó de verdad una pista, no solo cuándo se escribió.
Ajustes¶
vc.sink acepta estos argumentos por palabra clave; vc.play y voice.Queue también aceptan buffer y stall.
| Ajuste | Por defecto | Significado |
|---|---|---|
buffer |
Defaults.sink_buffer, 0.2 |
segundos que contiene el sink antes de que write espere, de 0.02 a 60 |
stall |
Defaults.stall, "hold" |
cuando el audio no se puede enviar, durante una reconexión o una transición de DAVE: "hold" lo conserva para reproducirlo después, "drop" lo descarta en tiempo real, para el audio en vivo que no debe quedarse atrás |
volume |
1.0 | multiplica las muestras; sink.volume lo cambia durante la reproducción |
filters |
Filters() |
los efectos; sink.filters los cambia durante la reproducción |
after |
None |
otro sink de esta conexión; este queda en silencio hasta que ese se cierre o termine de reproducir lo escrito antes de su último drain() |
crossfade |
0.0 | con after, segundos para empezar antes y hacer un fundido entre los dos; el sink guarda entonces esa cantidad más en el búfer |
Con after y crossfade, una cola reproduce sin pausas: el sink de la pista siguiente se llena mientras suena la actual, y toma el relevo en la muestra exacta.
Estado y contadores¶
sink.queued son los segundos de audio en espera, sink.passthrough indica si el último paquete salió tal como se escribió, y sink.closed si está cerrado. sink.stats() devuelve un SinkStats:
| Campo | Valor |
|---|---|
sent |
segundos de audio enviados |
position |
el cabezal de reproducción en segundos, tal como lo cuentan los marcadores |
underruns |
cuántas veces el sink se quedó vacío mientras reproducía, fuera de un drain(): el productor se quedó atrás |
held_ticks |
ticks en los que el sink se retuvo porque el audio no se podía enviar |
dropped |
segundos descartados por una detención en modo "drop" |
passthrough_packets |
paquetes enviados tal como se escribieron, sin decodificar |
Los underruns son el número que hay que vigilar: un productor que no logra mantenerse buffer segundos por delante es demasiado lento, o el event loop está bloqueado. vc.sinks enumera los sinks abiertos de una conexión.