歌枠配信、開始3分前に「あの曲のカラオケ音源、持ってない…」となった経験、VTuber本人にも中の人スタッフにもあると思います。
JOYSOUNDの配信向けカラオケ機能は便利ですが、配信したい曲が必ず入っているとは限らないし、即興リクエストにも対応しづらい。Spotifyから流すと当然BGMミュート対象です。結局「セットリストが固まった時点で全曲ぶんのオフボーカルを用意しておく」のが安全策になります。
この記事では、AI音源分離を使ってセトリ10曲ぶんのカラオケ音源を寝ている間に量産するPythonパイプラインを作ります。配信向けの音圧調整・OBS取り込みまで含めた、現場で使える内容です。
著作権について:歌枠でJ-POP等を歌う場合、配信プラットフォームの包括契約(YouTubeはNexTone・JASRAC包括契約済、Twitchは部分対応)と、原盤権の扱い(Content IDによる収益化制限/分配)を必ず確認してください。AI分離音源を使う場合も、原盤を加工した派生物として扱われる可能性があります。
この記事でわかること
- ✅ 配信用カラオケ音源を「ファイル投げるだけ」で量産する仕組み
- ✅ 配信ラウドネス(-16 LUFS)に自動正規化する後処理
- ✅ OBSのソース切り替えをPythonからWebSocketで叩く方法
- ✅ Discord/Twitchチャットからの曲リクエストをキュー化
- ✅ 同時接続1000人規模の歌枠でも詰まらない構成
想定するワークフロー
歌枠当日の流れを、こうしたいです。
[セトリ確定]
↓
原曲ファイル 10個 → batch_prepare.py
↓
out/01_song_a_offvocal.wav
out/02_song_b_offvocal.wav
...(-16 LUFS、ハイパス済み、配信即投入OK)
↓
[配信中]
リクエストチャットコマンド → queue.py
↓
OBSソース自動切替 → obs_switcher.py
↓
[歌唱]
前提条件
- Python 3.10+
- ffmpeg(必須)
- OBS Studio + obs-websocket プラグイン(v5以上はOBS本体に同梱)
- 配信用音源は基本44.1kHz / 16bit WAV で揃えるのが扱いやすい
pip install requests pydub pyloudnorm obsws-python python-dotenv discord.py
ステップ1:原曲からオフボーカルを量産する
ローカルで完結したい場合はDemucs、寝てる間に量産したい場合はStemSplitのAIボーカルリムーバーAPIが楽です。GPUがないノートで配信PCを兼ねていることが多いと思うので、ここではAPI版を例に出します。
# separator.py
import os
import time
import requests
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
API = "https://api.stemsplit.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['STEMSPLIT_API_KEY']}"}
def make_off_vocal(input_path: Path, out_dir: Path) -> Path:
"""原曲ファイル → オフボーカルWAVを返す。"""
with open(input_path, "rb") as f:
res = requests.post(
f"{API}/jobs",
headers=HEADERS,
files={"file": f},
data={"stems": "vocals"},
)
res.raise_for_status()
job_id = res.json()["job_id"]
while True:
status = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS).json()
if status["status"] == "completed":
break
if status["status"] == "failed":
raise RuntimeError(status.get("error", "unknown"))
time.sleep(3)
url = status["stems"]["instrumental"]
out = out_dir / f"{input_path.stem}_offvocal.wav"
with requests.get(url, stream=True) as r, open(out, "wb") as f:
for chunk in r.iter_content(8192):
f.write(chunk)
return out
ポイントは stems: vocals を指定して2-stem分離にしていることです。ドラム/ベース/その他に細かく分けても歌枠では使わないので、処理時間とコストが半分以下になります。
ステップ2:配信向けに音圧を整える(-16 LUFS)
AI分離直後の音源は音圧が-3〜-6dB下がっていることが多いです。原曲と並べて流すと「歌枠だけ音が小さい」と言われがち。Twitch/YouTubeのリスニングラウドネス基準である -16 LUFS に揃えておきましょう。
# loudness.py
import numpy as np
import soundfile as sf
import pyloudnorm as pyln
from pathlib import Path
def normalize_to_lufs(input_path: Path, output_path: Path, target_lufs: float = -16.0) -> float:
"""
配信用ラウドネスに正規化する。返り値は実測LUFS。
"""
data, rate = sf.read(str(input_path))
meter = pyln.Meter(rate)
measured = meter.integrated_loudness(data)
if np.isinf(measured):
return measured
normalized = pyln.normalize.loudness(data, measured, target_lufs)
peak = np.max(np.abs(normalized))
if peak > 0.99:
normalized = normalized * (0.99 / peak)
sf.write(str(output_path), normalized, rate)
return measured
pyloudnorm はITU-R BS.1770準拠のLUFS計測なので、配信プラットフォームが内部で行っているラウドネス正規化と整合性が取れます。これを通しておくと「曲ごとに音量を毎回調整」という手作業が消えます。
ステップ3:セトリ10曲を一括で準備する
ステップ1+2をくっつけて、フォルダごと処理するスクリプトです。
# batch_prepare.py
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from separator import make_off_vocal
from loudness import normalize_to_lufs
def prepare_setlist(input_dir: str, output_dir: str, max_workers: int = 4) -> list[Path]:
in_dir = Path(input_dir)
out_dir = Path(output_dir)
out_dir.mkdir(exist_ok=True)
songs = sorted(in_dir.glob("*.mp3")) + sorted(in_dir.glob("*.wav"))
raw_dir = out_dir / "_raw"
raw_dir.mkdir(exist_ok=True)
def _process(song: Path) -> Path:
raw = make_off_vocal(song, raw_dir)
final = out_dir / f"{song.stem}_offvocal.wav"
lufs = normalize_to_lufs(raw, final)
print(f"✓ {song.name} (orig: {lufs:.1f} LUFS → -16 LUFS)")
return final
results = []
with ThreadPoolExecutor(max_workers=max_workers) as ex:
futures = {ex.submit(_process, s): s for s in songs}
for fut in as_completed(futures):
try:
results.append(fut.result())
except Exception as e:
print(f"✗ {futures[fut].name}: {e}")
return results
if __name__ == "__main__":
prepare_setlist("./setlist_input", "./setlist_ready", max_workers=4)
実測値:4分の楽曲10本で6〜8分で全部終わります。寝る前に投げて朝にはセトリ準備完了、というのが目標値です。
ステップ4:配信中にOBSのソースを自動切り替え
歌う曲を選んだら、OBSの音源シーンをPythonから切り替えます。obs-websocketを使います。
# obs_switcher.py
import obsws_python as obs
class OBSKaraokeController:
def __init__(self, host: str = "localhost", port: int = 4455, password: str = ""):
self.client = obs.ReqClient(host=host, port=port, password=password)
def switch_to(self, song_filename: str, scene_name: str = "歌枠") -> None:
"""
Media Source の `Karaoke Track` という入力名のファイルを差し替える。
"""
self.client.set_input_settings(
name="Karaoke Track",
settings={
"local_file": song_filename,
"restart_on_activate": True,
},
overlay=True,
)
self.client.set_current_program_scene(scene_name)
def stop(self) -> None:
self.client.trigger_media_input_action(
input_name="Karaoke Track",
media_action="OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP",
)
if __name__ == "__main__":
ctl = OBSKaraokeController(password="your-obs-ws-password")
ctl.switch_to("/path/to/setlist_ready/song_a_offvocal.wav")
OBS側の Karaoke Track 入力ソースはメディアソースで作っておきます。これで「次の曲ボタン → 自動で頭から再生」が完成します。
ステップ5:Discordチャットから曲リクエスト
メンバーシップ限定枠などで、Discordから曲リクエストを受けてキューに積む例です。
# request_queue.py
import asyncio
from collections import deque
from pathlib import Path
import discord
from obs_switcher import OBSKaraokeController
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
queue: deque[Path] = deque()
ctl = OBSKaraokeController(password="your-obs-ws-password")
SETLIST_DIR = Path("./setlist_ready")
def find_song(query: str) -> Path | None:
candidates = list(SETLIST_DIR.glob(f"*{query}*_offvocal.wav"))
return candidates[0] if candidates else None
@client.event
async def on_message(msg):
if msg.author.bot:
return
if msg.content.startswith("!req "):
title = msg.content[5:].strip()
song = find_song(title)
if not song:
await msg.reply(f"見つかりませんでした: {title}")
return
queue.append(song)
await msg.reply(f"キューに追加: {song.stem}(待ち {len(queue)})")
elif msg.content == "!next" and msg.author.guild_permissions.administrator:
if queue:
song = queue.popleft()
ctl.switch_to(str(song.absolute()))
await msg.reply(f"再生開始: {song.stem}")
スパム対策で !req は1ユーザー60秒1回などのレート制限を入れた方が安全です。
同時接続1000人規模で気をつけるポイント
| 観点 | 推奨 |
|---|---|
| 音源ファイル形式 | WAV 44.1kHz / 16bit に統一(OBSの再生負荷が一番軽い) |
| 配信PC ↔ オーディオI/F | バッファサイズ256以上、Loopback使用時は512推奨 |
| 音源の事前ロード | OBSのMedia Sourceは「再起動」で初回読み込みが入るため、シーン遷移1秒前に空再生でメモリ展開しておく |
| ラウドネス | -16 LUFS統一、ピーク -1 dBTP(クリップ防止) |
| 著作権 | 演奏前に曲名をDescriptionに残す。Content ID当てが入っても異議申し立てしやすい |
特にピーク-1 dBTP は守ってください。-0.1まで攻めると配信エンコーダ通過時にクリップして「歌枠でブツブツ言ってる」と苦情が来ます。
よくある質問
Q. 配信PCにGPUがありません。Demucsローカルでも間に合いますか?
A. 当日の即興リクエストは厳しいです。事前準備(前日や朝)にバッチ処理しておくならOK。即時性が必要なら API利用がほぼ一択になります。
Q. 歌枠中にAPI遅延が出たら?
A. 必ずローカルキャッシュしてください。setlist_ready/ フォルダに事前生成しておけば、配信中は完全オフラインで完結します。
Q. ボーカルが薄く残るのが気になる
A. AI分離の宿命です。Demucsなら --shifts 5、APIなら高品質モードを選択。それでも残る場合は、後段でセンターチャンネル -3dB を入れると目立ちにくくなります。
Q. アカペラの曲(伴奏なし)はどうなりますか?
A. オフボーカル抽出すると無音に近くなります。歌枠用途では原曲を流すしかありません。
まとめ
- セトリが決まった時点で
batch_prepare.pyを一発叩けば、配信ラウドネス済みのカラオケ音源が揃います - OBSとPythonをWebSocketでつなぐと、配信中の操作も自動化できます
- 著作権処理(NexTone包括契約・Content ID)は配信プラットフォーム側で別途必要です
- 同接1000規模ではWAV統一・事前ロード・-1 dBTPピークを守れば事故りにくいです
歌枠の準備時間を「1曲5分の手作業×10曲=50分」から、「フォルダに放り込むだけ→寝て待つ」に変えられると、その時間ぜんぶ歌の練習に回せます。
参考リンク