wjs-syncing-multicam について
wjs-syncing-multicamは、同じイベントを複数の機材で録音した素材を同期するスキルです。音声エンベロープの相互相関でオフセットを推定し、元ファイルを変更せずJSONサイドカーに記録します。
主な機能
- 01音声相互相関によるオフセット推定
- 02音量が最大の音声ストリームを自動選択
- 03クロックドリフト検出と重複区間の算出
- 04.sync.jsonサイドカーの生成
- 05一部区間のみを収録した素材に対応
活用シーン
- 01イベントの複数カメラ映像を同期
- 02セッションの一部だけを収録した音声を同期
- 03マルチカメラ編集向けの検証可能な同期メタデータを用意
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-syncing-multicamインストール後、Claude Code で/wjs-syncing-multicamを入力して読み込まれたか確認します。
詳細
wjs-syncing-multicam
音声の相互相関を使って、同じイベントを複数のソースで録画した各映像の時間オフセットを1つ算出し、各オリジナルファイルの隣に .sync.json サイドカーを出力します。オリジナルファイルが変更、コピー、再エンコードされることはありません。 後続ツールは、オフセットを適用する際に -itsoffset を使用します。
セットアップとコマンド
実装はオープンソースの polysync pip パッケージ(https://pypi.org/project/polysync/ · https://github.com/jianshuo/polysync)に含まれており、このスキルには独自のスクリプトが付属しなくなりました。インストールされていることを確認してから、CLI 経由で実行してください。
python3 -m pip install -U polysync # needs ffmpeg/ffprobe on PATH
polysync sync REFERENCE SOURCE # align SOURCE to REFERENCE, write sidecars
polysync sync REFERENCE SOURCE --partial # source covers only part of the session
polysync verify REFERENCE SOURCE SOURCE.sync.json # independent residual check
基準アングル以外の各アングルについて、polysync sync を1回ずつ実行します(毎回、基準を最初に指定し、同じ基準を使用してください)。以下のセクションでは、アルゴリズム、サイドカーのスキーマ、パッケージに組み込まれている注意点について説明します。出力を解釈し、フラグを選ぶ際に参照してください。
設計原則 — 再エンコードせずサイドカーを使う
以前のバージョンでは、オフセットをファイルに反映するため、トリミングと再エンコードを行って *_synced.MOV ファイルを生成していました。現在はこの方式を廃止しています。
- ディスク容量 — 3台のカメラで撮影した75分の4K映像は60 GB以上になります。再エンコードした同期済みコピーを作ると、情報が増えないまま容量が倍になります。
- 品質 — 再エンコードするたびに品質が劣化します。オリジナルが信頼できるソースであり、サイドカーは元に戻せるメタデータです。
- 速度 —
_synced.MOVの生成には Apple Silicon でファイルあたり10分以上かかりました。サイドカーの出力なら数秒です。 - 組み合わせやすさ — 後続ツール(
polysync edit、NLE へのインポート、ffmpeg のワンライナー)はサイドカーを読み込み、それぞれでオフセットを適用します。ツール固有のファイル形式に依存しません。
使用しないケース
- シングルカメラの映像 — 同期対象がありません。1つのソースをクリップに分割するには、video-segmentation を使用してください。
- NLE のタイムラインですでに位置合わせ済みのソース — エディターと競合させないでください。
- 同期後の自動編集、カット、PiP レンダリングには、これらのサイドカーを使用する wjs-editing-multicam を使ってください。
生の波形ではなくエンベロープを使う理由
生の PCM の相互相関では、2つのマイクのゲインや部屋の音響特性が異なる場合、ピークが弱くなり、誤った一致が発生します。サブカメラでは、ほぼ常にこの状況になります。対数エネルギーエンベロープは、周波数特性に関係なく両方のマイクが拾う、会話や音楽のダイナミクスを捉えます。エンベロープ処理を省略しないでください。低 SNR でもこのスキルが堅牢に動作する理由は、まさにこの処理にあります。
アルゴリズム
- 各入力から、モノラル PCM を8 kHz、16ビットで抽出します。音声ストリームは、音量に基づいて自動選択されます(
loudest_audio_stream)。ファイル中央の60秒間を対象に各0:a:Nを調査し、平均音量が最も高いものを選びます。マルチトラック対応のプロ用カメラでは、単純な0:a:0の指定では問題が起こります。Sony FX6 の MXF クリップには4つのモノラル PCM トラックがあり、a:0 / a:1 が無音(約 -90 dB)で、ルームマイクが a:2 / a:3 に割り当てられていることがよくあります。無音トラックで相関を取ると同期に失敗します。単一ストリームの入力(多くの MP4 カメラ)は、a:0 をそのまま使用します。 - 100 Hz(10 ms のホップ、50 ms のウィンドウ)で対数エネルギーエンベロープを計算します。2次 Butterworth フィルターを使い、カットオフ周波数0.05 Hz でフィルタリングします。filtfilt によって、緩やかなドリフトとゲインオフセットを除去します。
- エンベロープを全区間で FFT 相互相関し、およそ10 ms の粗いオフセットを求めます。
- サンプル単位で精密化します。A 内で粗く位置合わせした箇所の近くから、B の60秒間を調査し、±2秒の検索ウィンドウと放物線によるピーク補間を使用します。
- 複数箇所でドリフトを確認します。約3分ごとにステップ4を繰り返します。線形フィット
delta(t) = slope·t + interceptによって、実際のクロックドリフト(通常は5~50 ppm)を明らかにします。残差誤差がゼロを中心に対称になるよう、中点を基準とするオフセット(slope · midpoint + intercept)を使用します。 - 基準のタイムライン上でオーバーラップ区間を計算します。
overlap = [max(0, delta), min(ref_dur, delta + src_dur)] - 基準以外の各入力ファイルの隣に
.sync.jsonサイドカーを出力します。ファイルのコピー、トリミング、再エンコードは行いません。後続コードがすべての入力を同じように扱えるよう、基準入力にもdelta_seconds: 0を含むサイドカーを出力します。
polysync sync がこの処理を実装します。出力されるのは .sync.json サイドカーのみです。_synced.MOV は生成せず、再エンコードも行いません。
サイドカースキーマ(<input>.sync.json)
元の入力ごとにサイドカーを1つ作成し、元ファイルと同じディレクトリに書き込みます。純粋なJSONで、ファイル内にコメントはありません。以下のフィールドリファレンスを正式な仕様とします。
{
"_about": "Sync metadata for cam_b.MOV. Apply via ffmpeg -itsoffset. See wjs-syncing-multicam SKILL.md for full schema.",
"schema_version": 1,
"source": "cam_b.MOV",
"reference": "cam_a.MOV",
"delta_seconds": 12.345,
"drift_slope": 1.8e-5,
"overlap_in_reference": [12.345, 4512.180],
"overlap_in_source": [0.000, 4499.835],
"verification": {
"median_residual_ms": 4.2,
"residual_spread_ms": 11.8,
"probe_count": 24
}
}
フィールドリファレンス
| フィールド | 型 | 意味 |
|---|---|---|
_about | string | 人が読める一行の説明です。このSKILL.mdへの参照を含みます。常に存在します。 |
schema_version | int | このスキーマに破壊的変更があるたびに更新します。現在の値:1。 |
source | string | このサイドカーが説明する元ファイルのファイル名です。サイドカーのディレクトリからの相対パスです。再エンコードされたファイルを指すことはありません。 |
reference | string | タイムラインの基準となる入力です。基準ファイル自身のサイドカーでは、ここにそのファイル自身が記載されます。 |
delta_seconds | float | 基準のタイムラインで表した、ソースの t=0 の位置です。**正の値の場合、ソースは基準より後に始まります。ffmpegには -itsoffset <delta> として渡します。**負の値になることもあります(ソースが基準より前に始まる場合。例:早回し撮影のカメラ)。 |
drift_slope | float | 線形なクロックドリフトの傾き(無次元、約10⁻⁵)です。0.0 は測定可能なドリフトがないことを示します。後続処理では、同期音声/長時間のリップシンクの場合にのみソースに atempo = 1 + drift_slope を適用します。カメラ切り替え編集では無視してください。 |
overlap_in_reference | [start, end](秒) | ソースと基準の両方をカバーする区間を、基準のタイムラインで表したものです。相互に有効な時間範囲に出力をトリミングするために使用します。 |
overlap_in_source | [start, end](秒) | 同じ区間をソースのローカルタイムラインで表したものです。overlap_in_reference[0] - delta_seconds = overlap_in_source[0] となります。 |
verification | object | polysync verify の実行結果です。「同期が収束したか」を判定するゲートとして使用します。median_residual_ms は数ミリ秒程度が目安です。residual_spread_ms が納品時のフレームレートで1フレームを超える場合、必要なドリフト補正が適用されていません。 |
後続処理でのサイドカーの使用方法
-itsoffset はffmpegで入力ごとに指定し、-i より前に適用します。必ずサイドカーからソースの delta_seconds を読み取ってください。
# Play cam_b aligned to cam_a's timeline
ffmpeg -itsoffset $(jq -r .delta_seconds cam_b.MOV.sync.json) -i cam_b.MOV \
-i cam_a.MOV \
-filter_complex "[0:v][1:v]hstack" out.mp4
# Trim to mutual overlap window (read from cam_b.MOV.sync.json)
ffmpeg -ss <overlap_in_source[0]> -i cam_b.MOV -t <overlap_dur> ...
wjs-editing-multicam では、polysync edit が各 <input>.sync.json を自動的に読み込みます。これらのフラグを手動で組み合わせる必要はありません。
部分的にしかカバーされないクリップ — polysync sync --partial
よくあるケースとして、メインカメラは75分間をカバーしている一方、Riverside/スマートフォン/ラベリアマイクのレコーダーが中間の30分間しかカバーしていない場合があります。polysync sync REF.MOV NEW.mp4 --partial を実行します。
- 新しい入力と基準を相互相関します(全範囲が重なるモードと同じエンベロープアルゴリズムを使用します)。
- 新しいクリップの
t=0が基準のタイムライン上のどこに当たるかを特定します(delta_secondsは大きな値になることがあります。例:1842.5)。 - ソースのサイドカーのみを書き込みます。黒いパディングも、音声のパディングも、再エンコードもありません。
overlap_in_referenceにより、この入力がカバーする時間が正確にわかります。その範囲外ではメインカメラに切り替えます。
--partial フラグが変更するのは、失敗時の扱いだけです。良好なプローブが3つ未満の場合に失敗する代わりに、正常に処理を続行します(プローブが少ない場合はデルタの中央値を使用し、プローブがない場合は粗いデルタを使用します)。また、基準のサイドカーはスキップします(基準はすでに確立済みの同期セットに属しているものとします)。それ以外はデフォルトモードと同じです。
ドリフト補正を省略する場合
カメラ切り替え編集(一般的なケース)では、1時間で±25ミリ秒の残差は人間の知覚では気づかない程度です。drift_slope: 0.0 を渡し、中間点の delta_seconds のみを使用してください。
長時間(30分超)の同期音声/リップシンクで verification.residual_spread_ms > 40 の場合、後続処理ではソースに atempo = 1 + drift_slope を適用します。ソースファイル自体は変更されません。atempo フィルターは使用時に実行されます。
検証(常に実行)
polysync verify REF.MOV SRC.MOV SRC.sync.json は、両方のオリジナルからネイティブに音声を再抽出し(最も音量の大きいストリームを使用し、ffmpeg のオフセットは使用しません)、複数のプローブによる相関を再度実行します。サイドカーの delta_seconds(および --apply-drift を指定した場合はドリフト傾斜)を、numpy でのインデックス演算として適用します。つまり、基準時刻 bs のプローブは、ローカル時刻 bs - delta にあるソースから取得し、基準インデックス bs 付近を検索します。ピークオフセットが残差です。結果はサイドカーの verification フィールドに書き戻されます。
合格基準 — median_residual_ms < 15 かつ residual_spread_ms < 納品 fps での 1 フレーム。不合格の場合は、ドリフト補正を有効にして再試行してください。
中央値がほぼゼロで、ばらつきのみが基準を満たさない場合、通常は同期ずれではなくノイズです。 広角カメラや B ロール用カメラの遠距離マイク(残響が大きく、SNR が低く、ncoef が約 0.2)では、少数の外れ値プローブによって中央値が数ミリ秒のまま、ばらつきが大きくなることがあります。カット切り替えによる編集では同期が取れています。真の値を示すのは中央値であり、ばらつきの判定は保守的です。長尺映像のリップシンクの場合に限り、原因を追究してください。
よくある落とし穴
- 低 SNR では、生波形の相互相関によって誤ったピークが発生します。 必ず最初にエンベロープを取ってください。これは調整可能な設定ではなく、処理の根本的な前提です。
-itsoffsetの適用方法は音声と動画で異なります。同期を正しく保つには、対象入力に対する最初のフラグでなければなりません。ffmpeg -i src -itsoffset Xは誤りで、ffmpeg -itsoffset X -i srcが正しい指定です。- ヘッダーのない raw PCM(
-f s16le)への多重化では、-itsoffsetは何もしません。 オフセットを保持するコンテナのタイムスタンプがないため、ffmpeg はオフセットを黙って破棄し、先頭に無音を挿入しません。polysync verifyのように raw PCM に抽出する解析では、抽出時に-itsoffsetを使わず、numpy のインデックス演算でオフセットを適用する必要があります。誤った方法を使った場合の症状は、同期自体は完璧なのに、検証残差が数百ミリ秒に散らばり、ncoef ~0になることです。 - 単純な
0:a:0による抽出では、無音のトラックに対して黙って同期してしまいます。 複数トラックのカメラ(Sony FX6 MXF では、a:0/a:1 が -90 dB で、マイクは a:2 にあることがよくあります)には、音量に基づくストリーム選択が必要です。polysync syncとpolysync verifyはどちらも自動的に同じ最も音量の大きいトラックを選びます。再実装する場合は、この不変条件を維持してください。そうしないと残差に意味がなくなります。 - サイドカーのパスは、処理を実行するプロセスの作業ディレクトリではなく、サイドカーファイルのディレクトリを基準にする必要があります。
source/referenceはPath(sidecar).parentを基準に解決してください。 drift_slopeをサイドカーのdelta_secondsに組み込まないでください。 これらが別々のフィールドであるのには理由があります。ドリフトを無視できる単純なコンシューマーもあれば、同期音声に対応したコンシューマーはドリフトを適用できます。混在させると情報が失われます。