- Add web UI panel (dark theme) at / with REST API - Add Bilibili QR code scan login with browser header emulation - Add wanakana for English -> katakana conversion - Add volume control (0%-200%) - Add message format with '说、' pause marker - PyInstaller --onedir build for portable exe - Rate-limit danmaku logging (1/20) - Fix aiohttp.access log spam - Add README.md
40 lines
1.0 KiB
Python
40 lines
1.0 KiB
Python
"""Audio playback for WAV files."""
|
|
|
|
import asyncio
|
|
import io
|
|
import logging
|
|
|
|
import numpy as np
|
|
import sounddevice as sd
|
|
import soundfile as sf
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def play_wav(wav_data: bytes, volume: int = 100) -> None:
|
|
data, samplerate = sf.read(io.BytesIO(wav_data), dtype="float32")
|
|
if volume != 100:
|
|
gain = max(0.0, volume / 100.0)
|
|
data = np.clip(data * gain, -1.0, 1.0)
|
|
sd.play(data, samplerate)
|
|
sd.wait()
|
|
|
|
|
|
async def play_wav_async(wav_data: bytes, volume: int = 100) -> None:
|
|
loop = asyncio.get_running_loop()
|
|
await loop.run_in_executor(None, play_wav, wav_data, volume)
|
|
|
|
|
|
def get_output_devices() -> list[dict]:
|
|
devices = sd.query_devices()
|
|
result = []
|
|
for i, dev in enumerate(devices):
|
|
if dev["max_output_channels"] > 0:
|
|
result.append({
|
|
"index": i,
|
|
"name": dev["name"],
|
|
"channels": dev["max_output_channels"],
|
|
"default_samplerate": dev["default_samplerate"],
|
|
})
|
|
return result
|