오디오 재생¶

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) |
format 형식의 PCM 청크나 Opus 패킷을 내놓는 비동기 이터러블 |
아니요 |
File, Bytes, Url은 Rust에서 디코딩합니다. 지원 포맷은 Ogg(Opus, Vorbis), WebM과 Matroska, MP4와 M4A(AAC), MP3, FLAC, WAV입니다. URL은 미디어를 삽입한 웹 페이지가 아니라 미디어 자체를 가리켜야 합니다. 서버와의 연결이 끊기면 Url은 0.5초, 1초, 2초 뒤에 최대 세 번 다시 연결하며, 서버가 범위 요청을 받아들이면 끊긴 지점부터 이어서 재생합니다. HLS 마스터 플레이리스트는 오디오 렌디션을 재생하고, 라이브 플레이리스트는 끝에서 세 세그먼트 앞부터 시작합니다. 암호화된 플레이리스트와 바이트 범위 세그먼트는 SourceError로 거부합니다. FFmpeg는 입력 인수를 넘겨 프로그램을 실행하고 그 출력에서 48 kHz 스테레오 16비트 PCM을 읽습니다. 프로그램이 PATH에 없거나 에러로 종료하면 SourceError를 발생시킵니다. 열거나 디코딩하거나 탐색할 수 없는 소스는 SourceError를 발생시키거나, 트랙을 이유 ERROR로 끝냅니다.
opened를 설정하는 open()을 호출한 뒤에는 소스에 format, duration(초, 알 수 없으면 None), seekable, tags가 생깁니다. tags는 파일에 적힌 title, artist, album을 담은 voice.Tags입니다.
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()
대부분의 Ogg Opus 파일과 WebM 파일처럼 48 kHz 스테레오, 20 ms 패킷의 Opus 오디오는 필터 없이 볼륨 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가 있습니다. may_start_next는 FINISHED나 ERROR 뒤에 참이며, 직접 만든 플레이어가 다음 트랙으로 넘어가야 하는지 알려 줍니다. vc.stop()은 모든 트랙을 끝냅니다.
볼륨과 필터¶
volume은 샘플에 곱하는 값입니다. 0.5는 소리를 절반으로 줄이고, 1.0은 그대로 둡니다. voice.Filters는 오디오가 재생될 때 Rust가 적용하는 효과를 설정합니다.
Filters 필드 |
기본값 | 범위 |
|---|---|---|
equalizer |
{} |
최대 16개 밴드. 20~20 000 Hz의 중심 주파수를 dB 단위 게인에 매핑하며, 각 밴드의 폭은 1옥타브입니다 |
bass |
0.0 | 100 Hz 미만의 게인(dB) |
speed |
1.0 | 0.5~2.0, 음높이는 유지 |
pitch |
1.0 | 0.5~2.0, 템포는 유지 |
게인은 -24~24 dB이며, 범위를 벗어난 값은 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을 받으며, 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 | fec가 대비하는 예상 손실률, 0~1 |
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를 씁니다.