Reproduzindo áudio¶

await vc.play(source) reproduz uma fonte em uma conexão de voz e retorna uma Track na hora. Várias faixas tocam ao mesmo tempo e são mixadas; uma Queue as toca uma depois da outra.
import bostie
from bostie import Context, voice
bot = bostie.Bot()
tracks: dict[int, voice.Track] = {}
@bot.command
async def play(ctx: Context, url: str) -> None:
"""Play a media URL."""
vc = ctx.voice.current or await ctx.voice.connect()
track = await vc.play(voice.Url(url), volume=0.8, meta=ctx.user.id)
tracks[vc.guild_id] = track
await ctx.respond(f"Playing {track.source.tags.title or url}.")
ended = await track.wait()
if ended.reason is voice.EndReason.ERROR:
await ctx.followup(f"The track failed: {ended.error}")
@bot.command
async def pause(ctx: Context) -> None:
"""Pause or resume the track."""
track = tracks.get(ctx.guild_id or 0)
if track is None or track.ended is not None:
await ctx.respond("Nothing is playing.", ephemeral=True)
return
if track.paused:
track.resume()
else:
track.pause()
await ctx.respond(f"At {track.position:.0f} s.")
@bot.command
async def seek(ctx: Context, seconds: float) -> None:
"""Jump to a point in the track."""
track = tracks.get(ctx.guild_id or 0)
if track is not None and track.source.seekable:
await track.seek(seconds)
await ctx.respond(f"Now at {seconds:.0f} s.")
@bot.command
async def bass(ctx: Context, gain: float = 6.0) -> None:
"""Boost the bass and slow down a little."""
track = tracks.get(ctx.guild_id or 0)
if track is not None:
track.filters = voice.Filters(bass=gain, speed=0.9)
await ctx.respond("Changed the filters.")
Fontes¶
Uma fonte é um áudio a reproduzir. Cada uma é usada uma vez: play a abre, abri-la depois que terminou levanta StateError, e source.again() retorna uma nova com o mesmo áudio, para tocá-lo duas vezes.
| Fonte | Reproduz | Permite busca |
|---|---|---|
voice.File(path) |
um arquivo local | sim |
voice.Bytes(data, hint=None) |
o conteúdo de um arquivo em memória; hint é uma extensão como "ogg" |
sim |
voice.Url(url, headers=None) |
uma URL HTTP ou HTTPS direta de mídia, transmitida em stream, ou uma playlist HLS | quando o servidor aceita requisições de intervalo; HLS não |
voice.FFmpeg(args, executable="ffmpeg") |
a saída de um comando do ffmpeg, para formatos que o bostie não decodifica | não |
voice.Stream(items, format) |
um iterável assíncrono de blocos PCM ou pacotes Opus em format |
não |
File, Bytes e Url são decodificados em Rust: Ogg (Opus, Vorbis), WebM e Matroska, MP4 e M4A (AAC), MP3, FLAC e WAV. Uma URL precisa apontar para a própria mídia, não para uma página web que a incorpora. Quando a conexão com o servidor cai, Url se reconecta até três vezes, depois de 0,5, 1 e 2 segundos, e continua de onde parou se o servidor aceitar requisições de intervalo. Uma playlist mestre HLS toca a sua versão de áudio, uma playlist ao vivo começa três segmentos antes do fim, e as playlists criptografadas e os segmentos por intervalo de bytes são recusados com SourceError. FFmpeg roda o programa com os seus argumentos de entrada e lê da saída dele PCM estéreo de 16 bits a 48 kHz; ele levanta SourceError quando o programa não está no PATH ou sai com erro. Uma fonte que não pode ser aberta, decodificada ou buscada levanta SourceError, ou encerra a sua faixa com o motivo ERROR.
Depois de open(), que define opened, uma fonte tem format, duration (segundos, None quando desconhecida), seekable e tags, um voice.Tags com o title, o artist e o album que o arquivo informa.
import asyncio
from collections.abc import AsyncIterator
from pathlib import Path
import bostie
from bostie import Context, voice
bot = bostie.Bot()
CHIME = Path("chime.ogg")
@bot.command
async def chime(ctx: Context) -> None:
"""Play a short sound from memory."""
vc = ctx.voice.current or await ctx.voice.connect()
data = await asyncio.to_thread(CHIME.read_bytes)
await vc.play(voice.Bytes(data, hint="ogg"))
await ctx.respond("Ding.")
@bot.command
async def radio(ctx: Context) -> None:
"""Play a station through ffmpeg."""
vc = ctx.voice.current or await ctx.voice.connect()
station = voice.FFmpeg(["-i", "https://example.com/radio.m3u8"])
await vc.play(station)
await ctx.respond("On air.")
async def silence(seconds: float) -> AsyncIterator[bytes]:
frame = bytes(3840)
for _ in range(int(seconds * 50)):
yield frame
@bot.command
async def hush(ctx: Context) -> None:
"""Play three seconds of generated silence."""
vc = ctx.voice.current or await ctx.voice.connect()
track = await vc.play(voice.Stream(silence(3), voice.PCM()))
await ctx.respond("Shh.")
await track.wait()
Áudio Opus a 48 kHz estéreo em pacotes de 20 ms, como a maioria dos arquivos Ogg Opus e WebM, é enviado do jeito que está, sem decodificar e codificar de novo, enquanto toca sozinho no volume 1 sem filtros. track.passthrough diz quando isso acontece.
Faixas¶
vc.play(source, volume=1.0, filters=Filters(), meta=None) aceita o volume e os filtros iniciais, e meta, qualquer valor a guardar com a faixa, como quem a pediu. Ele também aceita buffer e stall para o sink da faixa.
Track |
Faz |
|---|---|
await track.wait() |
espera o fim e retorna um TrackEnd; cancelar a espera não para a faixa |
await track.started() |
espera até ela começar a tocar; False se ela terminou antes |
track.ended |
o TrackEnd, ou None enquanto ela toca |
track.position |
os segundos da fonte tocados até agora |
track.pause(), track.resume(), track.paused |
param e retomam a alimentação dela; o áudio que já está no buffer continua tocando |
await track.seek(seconds) |
continua a partir de uma posição; SourceError quando a fonte não permite busca |
track.volume, track.filters |
mudam na hora enquanto ela toca |
track.stop() |
a encerra |
track.source, track.meta, track.sink |
o que ela toca, o seu valor e o sink em que ela escreve |
Um TrackEnd tem a track, o reason (FINISHED, STOPPED, ERROR com o error, ou CLEANUP quando a conexão terminou) e may_start_next, verdadeiro depois de FINISHED ou ERROR, que diz se um player seu deve passar para a próxima faixa. vc.stop() encerra todas as faixas.
Volume e filtros¶
volume multiplica as amostras: 0.5 é metade do volume, 1.0 as deixa sem mudança. voice.Filters define efeitos que o Rust aplica enquanto o áudio toca:
Campo de Filters |
Padrão | Faixa de valores |
|---|---|---|
equalizer |
{} |
até 16 bandas, mapeando uma frequência central de 20 a 20 000 Hz para um ganho em dB; cada banda tem uma oitava de largura |
bass |
0.0 | ganho em dB abaixo de 100 Hz |
speed |
1.0 | de 0.5 a 2.0, mantendo a altura |
pitch |
1.0 | de 0.5 a 2.0, mantendo o andamento |
Os ganhos vão de -24 a 24 dB, e valores fora da faixa levantam ValueError.
Filas¶
import bostie
from bostie import Context, voice
bot = bostie.Bot(voice=voice.Defaults(crossfade=3.0, history=20))
queues: dict[int, voice.Queue] = {}
async def queue_of(ctx: Context) -> voice.Queue:
vc = ctx.voice.current or await ctx.voice.connect()
queue = queues.get(vc.guild_id)
if queue is None or queue.closed:
queue = queues[vc.guild_id] = voice.Queue(vc)
return queue
@bot.command
async def add(ctx: Context, url: str, next_up: bool = False) -> None:
"""Add a URL to the queue, or play it next."""
queue = await queue_of(ctx)
queue.add(voice.Url(url), meta=ctx.user.id, index=0 if next_up else None)
await ctx.respond(f"{len(queue.upcoming)} tracks up next.")
@bot.command
async def queue(ctx: Context) -> None:
"""Show what plays now and next."""
player = await queue_of(ctx)
lines = [f"Now: {player.now.source!r}" if player.now else "Nothing playing"]
lines += [f"{i}. {track.source!r}" for i, track in enumerate(player.upcoming, 1)]
await ctx.respond("\n".join(lines))
@bot.command
async def back(ctx: Context) -> None:
"""Play the previous track again."""
player = await queue_of(ctx)
previous = player.back()
await ctx.respond("Going back." if previous else "Nothing played before.")
@bot.command
async def shuffle(ctx: Context) -> None:
"""Shuffle the upcoming tracks."""
(await queue_of(ctx)).shuffle()
await ctx.respond("Shuffled.")
voice.Queue(vc) toca as faixas uma atrás da outra sem intervalos, ou sobrepostas por crossfade segundos. Ela aceita volume, filters, crossfade e history, que têm como padrão as configurações da conexão, e o buffer e o stall do sink.
queue.add(source, meta=None, index=None)coloca uma fonte no fim da fila, ou emindexentre as próximas faixas, e retorna aTrackdela.queue.now,queue.upcomingequeue.historysão a faixa atual, as próximas e atéhistoryfaixas que já tocaram.queue.skip()encerra a faixa atual;queue.back()toca de novo a última faixa do histórico e depois a atual do começo.queue.move(track, index),queue.remove(track),queue.shuffle()equeue.clear()reorganizam as próximas faixas.queue.loop = "track"repete a faixa atual,"queue"a fila inteira,Nonepara de repetir; o tipo évoice.Loop. As repetições usamsource.again().queue.pause(),queue.resume(),queue.paused,queue.volumeequeue.filtersagem sobre o que estiver tocando.queue.stop()encerra todas as faixas; as faixas adicionadas depois voltam a tocar.await queue.join()espera até não haver nada na fila nem tocando.
Uma fila em uma conexão que terminou fica closed e não aceita mais faixas; crie uma nova depois de reconectar, como o trecho de código faz.
Sons do painel de sons¶
await bot.rest.soundboard.send(vc.channel_id, sound_id=...) toca um som do painel de sons no canal em que o bot está conectado. O próprio Discord o toca na chamada, junto com o que o bot envia; passe source_guild_id para um som de outro servidor. bot.rest.soundboard.list_default() lista os sons do próprio Discord e list(guild_id) os de um servidor, e bot.request_soundboard_sounds(guild_id) os pede ao gateway.
O mesmo áudio em vários servidores¶
await voice.broadcast(source, connections) toca uma fonte em várias conexões, decodificando-a e codificando-a uma única vez. As conexões que terminam saem do conjunto; as que entram depois não são adicionadas. Ele aceita encoder e stall, "drop" por padrão, para que uma conexão parada não segure as outras.
Codificador¶
voice.EncoderSettings configura o codificador Opus de uma conexão, por Defaults(encoder=...), connect(encoder=...) ou vc.encoder = ... enquanto conectado:
| Campo | Padrão | Significado |
|---|---|---|
bitrate |
None |
bits por segundo, de 500 a 512 000; None segue o bitrate do canal, ou 64 000 |
application |
"audio" |
"audio" para música, "voip" para fala, "lowdelay" para a menor latência |
fec |
True |
correção de erros antecipada (FEC), que disfarça pacotes perdidos |
packet_loss |
0.1 | a perda esperada, de 0 a 1, para a qual fec se prepara |
complexity |
10 | de 0 a 10; menos usa menos CPU |
bandwidth |
"auto" |
a banda de áudio mais alta: "narrow", "medium", "wide", "superwide" ou "full" |
signal_type |
"auto" |
ajusta para "voice" ou "music" |
vc.speaking_flags define as flags de fala que o bot anuncia, voice.SpeakingFlags.MICROPHONE por padrão, SOUNDSHARE para áudio compartilhado, PRIORITY para voz prioritária.