1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VTuberの歌枠配信用カラオケ音源を5分で量産するPythonスクリプト【AI音源分離】

1
Posted at

歌枠配信、開始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

pyloudnormITU-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分」から、「フォルダに放り込むだけ→寝て待つ」に変えられると、その時間ぜんぶ歌の練習に回せます。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?