Синки и сырой звук¶
Синк — это ограниченный буфер, в который Python пишет звук, а микшер на Rust читает его порциями по 20 мс. Каждый трек и каждая очередь играют через синк. Открывайте собственный синк для звука, который вы генерируете или получаете откуда-то ещё: из синтезатора, движка синтеза речи, потока из другой программы.
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)
Форматы¶
vc.sink(format) возвращает voice.Sink для того, что вы будете писать, — voice.Format:
voice.PCM(rate=48000, channels=2, sample="s16le"): чередующиеся отсчёты little-endian,"s16le"(16-битные целые) или"f32le"(32-битные числа с плавающей точкой), моно или стерео, от 8000 до 192 000 Гц. bostie пересэмплирует в 48 кГц на Rust; частота должна делить 48 кГц на блоки не больше 960 отсчётов, что выполняется для всех распространённых частот.voice.Opus(): один пакет Opus на запись. Пакеты по 20 мс, 48 кГц стерео отправляются как записаны, пока синк играет один, с громкостью 1 и без фильтров; всё остальное декодируется и микшируется.
voice.OpusEncoder(pcm_format, encoder=EncoderSettings()) кодирует PCM в такие пакеты в процессе Python — для звука, который вы хотите закодировать один раз и отправить во много синков: encode(data) возвращает все готовые на данный момент пакеты, а flush() дополняет остаток тишиной. Его битрейт по умолчанию — 64 кбит/с, поскольку ему не за каким каналом следовать.
Запись¶
await sink.write(data)ставит звук в очередь и ждёт, пока в синке уже лежитbufferсекунд. Это ожидание и есть обратное давление, которое держит ваш источник данных в темпе реального времени.sink.write_nowait(data)ставит в очередь всю порцию сразу или вызываетBufferFull.async with sink:в конце блока ждёт, пока всё записанное проиграется, а затем закрывает синк; исключение в блоке вместо этого отбрасывает то, что стоит в очереди.await sink.drain()ждёт, пока проиграется всё записанное на данный момент;False, если часть была отброшена.sink.clear()отбрасывает то, что стоит в очереди;clear(until=marker)— только часть до маркера,clear(since=marker)— только часть после него.sink.close()отбрасывает то, что стоит в очереди, и закрывает синк. Запись в закрытый синк вызываетSinkClosed.
sink.mark() возвращает Marker, стоящий после записанного на данный момент звука. await marker возвращает True, когда весь звук до него проигран, или False, если часть была отброшена раньше; marker.done говорит, известно ли это уже. С помощью маркеров плеер узнаёт, когда трек действительно закончился, а не просто был записан.
Настройки¶
vc.sink принимает эти именованные аргументы; vc.play и voice.Queue тоже принимают buffer и stall.
| Настройка | По умолчанию | Значение |
|---|---|---|
buffer |
Defaults.sink_buffer, 0.2 |
сколько секунд синк держит, прежде чем write начнёт ждать, от 0,02 до 60 |
stall |
Defaults.stall, "hold" |
что делать, когда звук нельзя отправить, — во время переподключения или перехода DAVE: "hold" сохраняет его, чтобы проиграть потом, "drop" отбрасывает его в реальном времени — для живого звука, который не должен отставать |
volume |
1.0 | умножает отсчёты; sink.volume меняет его во время воспроизведения |
filters |
Filters() |
эффекты; sink.filters меняет их во время воспроизведения |
after |
None |
другой синк этого подключения; этот молчит, пока тот не закроется или не доиграет то, что было записано до его последнего drain() |
crossfade |
0.0 | вместе с after — на сколько секунд раньше начать и сделать плавный переход; синк тогда буферизует на столько же больше |
after и crossfade — это то, как очередь играет без пауз: синк следующего трека заполняется, пока играет текущий, и подхватывает воспроизведение точно с нужного отсчёта.
Состояние и счётчики¶
sink.queued — сколько секунд звука ждёт в очереди, sink.passthrough — ушёл ли последний пакет так, как был записан, а sink.closed — закрыт ли синк. sink.stats() возвращает SinkStats:
| Поле | Значение |
|---|---|
sent |
сколько секунд звука отправлено |
position |
позиция воспроизведения в секундах, как её отсчитывают маркеры |
underruns |
сколько раз синк опустел во время воспроизведения вне drain(): источник данных отстал |
held_ticks |
сколько тактов синк удерживался, потому что звук нельзя было отправить |
dropped |
сколько секунд отброшено при остановке с "drop" |
passthrough_packets |
сколько пакетов отправлено как записано, без декодирования |
Главное, за чем стоит следить, — опустошения (underruns): источник данных, который не успевает держаться на buffer секунд впереди, слишком медленный, или заблокирован цикл событий. vc.sinks перечисляет открытые синки подключения.