关于 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
使用音频互相关,为同一事件的每个多源录制计算一个时间偏移量,并在每个原始文件旁生成 .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(先指定参考机位,每次使用相同的参考机位)。下文介绍算法、辅助文件架构,以及该包内置的注意事项;请阅读这些内容,以便解读输出并选择标志。
设计原则——使用辅助文件,而非重新编码
此技能的早期版本会通过裁剪并重新编码生成 *_synced.MOV 文件,将偏移量写入文件。我们已移除该做法:
- 磁盘空间——3 台摄像机拍摄 75 分钟的 4K 素材会占用 60+ GB。重新编码生成同步副本只会让占用翻倍,不会增加任何信息。
- 质量——每次重新编码都会造成有损压缩。原始文件才是事实来源;辅助文件则是可逆元数据。
- 速度——在 Apple Silicon 上,每个文件生成
_synced.MOV需要 10+ 分钟;生成辅助文件只需几秒。 - 可组合性——任何下游工具(
polysync edit、NLE 导入、ffmpeg 单行命令)都能读取辅助文件并自行应用偏移量。不会被特定工具的文件格式绑定。
不适用场景
- 单机位素材——没有可与之同步的对象。若要将单个源文件拆分为片段,请使用 video-segmentation。
- 在 NLE 时间线上已对齐的素材——不要与剪辑软件对着干。
- 若要进行同步后的自动剪辑、剪切或画中画渲染,请使用 wjs-editing-multicam(该工具会读取这些辅助文件)。
为何使用基于包络的方法,而非原始波形
当两个麦克风的增益或房间响应不同时,原始 PCM 互相关会产生较弱的峰值和错误匹配——使用辅助摄像机时几乎总是如此。对数能量包络能捕捉对话和音乐的动态变化,无论频率响应如何,两个麦克风都能记录到这些变化。不要跳过包络步骤——这正是此技能在低信噪比下仍然可靠的关键。
算法
- 从每个输入中提取 8 kHz、16 位的单声道 PCM。音频流会按响度自动选择(
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 窗口)。使用截止频率为 0.05 Hz 的二阶 Butterworth 高通滤波器,并通过 filtfilt 滤波,以消除缓慢漂移和增益偏移。
- 对包络进行端到端 FFT 互相关,得到粗略偏移量(约 10 ms)。
- 在样本级进行精细校准:在粗略对齐位置附近,从 A 中取 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,也不会重新编码。
Sidecar 架构(<input>.sync.json)
每个原始输入对应一个 sidecar,写入其旁边。纯 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 | 此 sidecar 所描述的原始文件名。相对于 sidecar 所在目录。绝不指向重新编码后的文件。 |
reference | string | 时间线对齐所依据的输入文件。该参考文件自己的 sidecar 会在此处列出它自身。 |
delta_seconds | float | source 的 t=0 在参考时间线中的位置。**若为正值,表示 source 晚于参考文件开始;传给 ffmpeg 时使用 -itsoffset <delta>。**也可以为负值(例如,source 来自提前开始录制的摄像机)。 |
drift_slope | float | 线性时钟漂移斜率(无量纲,约为 10⁻⁵)。0.0 表示未测得漂移。下游仅在同步声音/长时长唇形同步时对 source 应用 atempo = 1 + drift_slope——摄像机切换剪辑时忽略。 |
overlap_in_reference | [start, end](秒) | source 和参考文件均有覆盖的时间窗口,以参考时间线表示。用它将输出裁剪到双方均有效的时间范围。 |
overlap_in_source | [start, end](秒) | 同一时间窗口,以 source 的本地时间线表示。overlap_in_reference[0] - delta_seconds = overlap_in_source[0]。 |
verification | object | 运行 polysync verify 的输出——用于判定“同步是否收敛”。median_residual_ms 应为几毫秒;若 residual_spread_ms 大于交付帧率下的一帧时长,表示需要进行漂移校正,但校正被跳过。 |
下游如何使用 sidecar
-itsoffset 是 ffmpeg 的逐输入参数,且必须放在 -i 之前。始终从 sidecar 读取 source 的 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)。 - 仅写入 source sidecar——不添加黑色视频、不填充音频、不重新编码。
overlap_in_reference会准确告知使用者此输入的覆盖时段;在该时段之外,回退到主摄像机。
--partial 标志只改变失败处理方式:在探测结果较少时,以中位数 delta 进行宽松处理;没有探测结果时,则使用粗略 delta,而不是在少于 3 个有效探测结果时失败;同时跳过参考文件 sidecar(假定参考文件已属于一个既有同步集)。其他行为与默认模式完全相同。
何时跳过漂移校正
对于摄像机切换剪辑(最常见的情况),一小时内 ±25 ms 的残差低于人类感知阈值——设置 drift_slope: 0.0,只使用中点 delta_seconds。
对于长时长(>30 min)的同步声音/唇形同步,如果 verification.residual_spread_ms > 40,下游会对 source 应用 atempo = 1 + drift_slope。source 文件仍不会被修改——atempo 滤镜会在使用时运行。
验证(始终运行)
polysync verify REF.MOV SRC.MOV SRC.sync.json 会原生地从两个原始文件重新提取音频(选择音量最大的音轨,不使用 ffmpeg 偏移),并再次运行多探针相关性分析。它会将 sidecar 中的 delta_seconds(以及使用 --apply-drift 时的漂移斜率)作为 numpy 中的索引运算应用:在参考时间 bs 处的探针取自本地时间 bs - delta 对应的源文件位置,然后在参考索引 bs 附近进行搜索;峰值偏移即为残差。结果会写回 sidecar 的 verification 字段。
通过标准——median_residual_ms < 15 且 residual_spread_ms < 交付帧率下的 1 帧。未通过时,启用漂移校正后重试。
只有 spread 未通过而 median 接近零时,通常是噪声,并非不同步。 宽景/B-roll 摄像机上的远场麦克风(混响大、SNR 低、ncoef 约为 0.2)会产生少数离群探针,从而拉高 spread,但 median 仍只有几毫秒。对于镜头切换式剪辑,这已经对齐——median 才是真实情况;spread 门槛较为保守。只有长篇口型同步时才需要追查。
常见问题
- 低 SNR 下,原始波形互相关会产生假峰值。 始终先提取包络——这不是可调参数,而是整个方法的前提。
-itsoffset对音频和视频的语义不同——要确保同步正确,它必须是该输入的第一个标志。ffmpeg -i src -itsoffset X是错误的;ffmpeg -itsoffset X -i src才是正确的。- 将流复用为无头原始 PCM(
-f s16le)时,-itsoffset不起作用。 不存在可承载偏移的容器时间戳,因此 ffmpeg 会静默丢弃偏移,也不会插入前导静音。任何将音频提取为原始 PCM 的分析(例如polysync verify)都必须在 numpy 中通过索引运算应用偏移,绝不能在提取时使用-itsoffset。错误操作的表现:即使同步本身完全正确,验证残差仍会散布在数百毫秒的范围内,且ncoef ~0。 - 简单地提取
0:a:0会在不报错的情况下与无声轨道同步。 多音轨摄像机(Sony FX6 MXF:a:0/a:1 通常为 -90 dB,麦克风在 a:2)需要根据响度选择音轨。polysync sync和polysync verify都会自动选择同一个音量最大的音轨——如果你要重新实现,必须保持这一不变量,否则残差将毫无意义。 - Sidecar 路径必须相对于 sidecar 文件所在目录,而不是使用它的进程的工作目录。应将
source/reference路径相对于Path(sidecar).parent解析。 - 不要将
drift_slope合并进 sidecar 的delta_seconds。 它们分设字段自有原因——简单的使用方可以忽略漂移,而支持同步声音的使用方可以应用它。混在一起会丢失信息。