Перейти к содержанию

Воспроизведение звука

Бости кивает в такт музыке

await vc.play(source) проигрывает источник в голосовом подключении и сразу возвращает Track. Несколько треков играют одновременно и микшируются; Queue проигрывает их один за другим.

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.")

Источники

Источник — это звук, который нужно проиграть. Каждый используется один раз: play открывает его, повторное открытие после окончания вызывает StateError, а source.again() возвращает новый источник с тем же звуком, чтобы проиграть его ещё раз.

Источник Проигрывает Перемотка
voice.File(path) локальный файл да
voice.Bytes(data, hint=None) содержимое файла в памяти; hint — расширение, например "ogg" да
voice.Url(url, headers=None) прямой HTTP- или HTTPS-URL медиафайла потоком или плейлист HLS если сервер принимает запросы диапазонов; HLS — нет
voice.FFmpeg(args, executable="ffmpeg") вывод команды ffmpeg — для форматов, которые bostie не декодирует нет
voice.Stream(items, format) асинхронный итерируемый объект с порциями PCM или пакетами Opus в формате format нет

File, Bytes и Url декодируются в Rust: Ogg (Opus, Vorbis), WebM и Matroska, MP4 и M4A (AAC), MP3, FLAC и WAV. URL должен указывать на сам медиафайл, а не на веб-страницу, которая его встраивает. Когда соединение с сервером обрывается, Url переподключается до трёх раз — через 0,5, 1 и 2 секунды — и продолжает с места обрыва, если сервер принимает запросы диапазонов. Главный плейлист HLS проигрывает свою звуковую версию, живой плейлист начинается за три сегмента до конца, а зашифрованные плейлисты и сегменты с диапазонами байтов отклоняются с SourceError. FFmpeg запускает программу с вашими входными аргументами и читает из её вывода стерео PCM 16 бит 48 кГц; он вызывает SourceError, если программы нет в PATH или она завершилась с ошибкой. Источник, который не удаётся открыть, декодировать или перемотать, вызывает SourceError или завершает свой трек с причиной ERROR.

После open(), который устанавливает opened, у источника есть format, duration (в секундах, None, если неизвестна), seekable и tags — voice.Tags с title, artist и album, которые указаны в файле.

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()

Звук Opus 48 кГц стерео в пакетах по 20 мс, как в большинстве файлов Ogg Opus и WebM, отправляется как есть, без повторного декодирования и кодирования, пока он играет один, с громкостью 1 и без фильтров. Когда это происходит, сообщает track.passthrough.

Треки

vc.play(source, volume=1.0, filters=Filters(), meta=None) принимает начальную громкость и фильтры, а также meta — любое значение, которое нужно хранить вместе с треком, например кто его заказал. Он также принимает buffer и stall для синка трека.

Track Что делает
await track.wait() ждёт окончания и возвращает TrackEnd; отмена ожидания не останавливает трек
await track.started() ждёт, пока трек начнёт играть; False, если он закончился раньше
track.ended TrackEnd или None, пока трек играет
track.position сколько секунд источника уже проиграно
track.pause(), track.resume(), track.paused прекращают и возобновляют подачу звука; уже буферизованный звук всё равно доигрывает
await track.seek(seconds) продолжить с позиции; SourceError, если источник не перематывается
track.volume, track.filters меняются сразу, пока трек играет
track.stop() завершить трек
track.source, track.meta, track.sink что он проигрывает, ваше значение и синк, в который он пишет

У TrackEnd есть track, reason (FINISHED, STOPPED, ERROR вместе с error или CLEANUP, когда подключение завершилось) и may_start_next — истина после FINISHED или ERROR, — которое говорит, должен ли ваш собственный плеер переходить к следующему треку. vc.stop() завершает все треки.

Громкость и фильтры

volume умножает отсчёты: 0,5 — вдвое тише, 1,0 оставляет их без изменений. voice.Filters задаёт эффекты, которые Rust применяет во время воспроизведения:

Поле Filters По умолчанию Диапазон
equalizer {} до 16 полос: центральная частота от 20 до 20 000 Гц сопоставляется с усилением в дБ; каждая полоса шириной в октаву
bass 0.0 усиление в дБ ниже 100 Гц
speed 1.0 от 0,5 до 2,0 с сохранением высоты тона
pitch 1.0 от 0,5 до 2,0 с сохранением темпа

Усиление — от −24 до 24 дБ, а значения вне диапазона вызывают ValueError.

Очереди

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) проигрывает треки подряд без пауз или с перекрытием на crossfade секунд. Она принимает volume, filters, crossfade и history, по умолчанию взятые из настроек подключения, а также buffer и stall синка.

  • queue.add(source, meta=None, index=None) ставит источник в конец очереди или на позицию index среди предстоящих треков и возвращает его Track.
  • queue.now, queue.upcoming и queue.history — текущий трек, следующие и до history уже сыгранных.
  • queue.skip() завершает текущий трек; queue.back() снова проигрывает последний трек из истории, а затем текущий заново.
  • queue.move(track, index), queue.remove(track), queue.shuffle() и queue.clear() переупорядочивают предстоящие треки.
  • queue.loop = "track" повторяет текущий трек, "queue" — всю очередь, None отключает повтор; тип — voice.Loop. Повторы используют source.again().
  • queue.pause(), queue.resume(), queue.paused, queue.volume и queue.filters действуют на то, что сейчас играет.
  • queue.stop() завершает все треки; треки, добавленные позже, снова играют.
  • await queue.join() ждёт, пока в очереди и в воспроизведении ничего не останется.

Очередь на завершённом подключении становится closed и больше не принимает треки; после переподключения создайте новую, как делает пример.

Звуки звуковой панели

await bot.rest.soundboard.send(vc.channel_id, sound_id=...) проигрывает звук звуковой панели в канале, к которому подключён бот. Discord проигрывает его в самом звонке, рядом со всем, что отправляет бот; для звука с другого сервера передайте source_guild_id. bot.rest.soundboard.list_default() перечисляет собственные звуки Discord, а list(guild_id) — звуки сервера; bot.request_soundboard_sounds(guild_id) запрашивает их через шлюз.

Один и тот же звук на нескольких серверах

await voice.broadcast(source, connections) проигрывает один источник в нескольких подключениях, декодируя и кодируя его один раз. Завершившиеся подключения выбывают из набора; присоединившиеся позже не добавляются. Функция принимает encoder и stall, по умолчанию "drop", чтобы застрявшее подключение не задерживало остальные.

Кодировщик

voice.EncoderSettings настраивает кодировщик Opus подключения — через Defaults(encoder=...), connect(encoder=...) или vc.encoder = ... во время подключения:

Поле По умолчанию Значение
bitrate None бит в секунду, от 500 до 512 000; None следует битрейту канала или берёт 64 000
application "audio" "audio" для музыки, "voip" для речи, "lowdelay" для минимальной задержки
fec True упреждающая коррекция ошибок, которая скрывает потерянные пакеты
packet_loss 0.1 ожидаемая доля потерь от 0 до 1, на которую рассчитывает fec
complexity 10 от 0 до 10; меньшее значение тратит меньше CPU
bandwidth "auto" верхняя полоса звука: "narrow", "medium", "wide", "superwide" или "full"
signal_type "auto" настройка под "voice" или "music"

vc.speaking_flags задаёт флаги речи, которые объявляет бот: по умолчанию voice.SpeakingFlags.MICROPHONE, SOUNDSHARE для общего звука, PRIORITY для приоритетного режима.