シンクと生の音声¶
シンクは、Python が音声を書き込み、Rust のミキサーが 20 ms ずつ読み出す、容量に上限のあるバッファーです。すべてのトラックとキューはシンクを通じて再生されます。シンセサイザー、音声合成エンジン、別のプログラムからのストリームなど、自分で生成した音声や別の場所から受け取った音声を流すには、独自のシンクを開きます。
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.Format)に対応する voice.Sink を返します。
voice.PCM(rate=48000, channels=2, sample="s16le")は、インターリーブされたリトルエンディアンのサンプルです。"s16le"(16 ビット整数)または"f32le"(32 ビット浮動小数点数)、モノラルまたはステレオ、8000〜192 000 Hz に対応します。bostie は Rust で 48 kHz にリサンプリングします。サンプルレートは、最大 960 サンプルのブロック単位で 48 kHz を割り切れる必要がありますが、一般的なサンプルレートはすべてこれを満たします。voice.Opus()は、1 回の書き込みにつき 1 つの Opus パケットです。48 kHz ステレオで 20 ms のパケットは、シンクがフィルターなし・音量 1 で単独再生している間は、書き込んだまま送信されます。それ以外はデコードされてミキシングされます。
voice.OpusEncoder(pcm_format, encoder=EncoderSettings()) は Python のプロセス内で PCM をそのようなパケットにエンコードします。1 回エンコードした音声を多数のシンクに送る場合に使います。encode(data) はその時点までに完成したすべてのパケットを返し、flush() は残りを無音で埋めます。追従するチャンネルがないため、ビットレートの既定値は 64 kbps です。
書き込み¶
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" はリアルタイムで破棄する。遅れてはならないライブ音声には "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 |
デコードせずに書き込んだまま送信したパケット数 |
注視すべき値はアンダーランです。buffer 秒分の先行を保てない生成側は遅すぎるか、イベントループがブロックされています。vc.sinks は接続の開いているシンクを一覧にします。