コンテンツにスキップ

シンクと生の音声

シンクは、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 は接続の開いているシンクを一覧にします。