feat: per-join crossfade durations + acrossfade curve in build_crossfade_merge_command
This commit is contained in:
+45
-17
@@ -244,32 +244,60 @@ def build_audio_clip_command(input_path: str, start: float, duration: float,
|
||||
]
|
||||
|
||||
|
||||
def build_crossfade_merge_command(clips: list[str], crossfade: float,
|
||||
out_path: str) -> list[str]:
|
||||
"""ffmpeg command that concatenates *clips* in order into *out_path*,
|
||||
crossfading each join by *crossfade* seconds (0 = butt-join via concat).
|
||||
Re-encoded per the output extension."""
|
||||
_ACROSSFADE_CURVES = frozenset({
|
||||
"tri", "qsin", "hsin", "esin", "log", "ipar", "qua", "cub", "squ", "cbr",
|
||||
"par", "exp", "iqsin", "ihsin", "dese", "desi", "losi", "sinc", "isinc",
|
||||
"nofade",
|
||||
})
|
||||
|
||||
|
||||
def build_crossfade_merge_command(clips: list[str], crossfade,
|
||||
out_path: str, curves=None) -> list[str]:
|
||||
"""ffmpeg command concatenating *clips* into *out_path*. *crossfade* is a
|
||||
single duration (applied to every join) or a per-join list of length
|
||||
len(clips)-1; 0 = butt-join. *curves* is a single acrossfade curve name or a
|
||||
per-join list (default 'tri', ffmpeg's default, emitted implicitly)."""
|
||||
if not clips:
|
||||
raise ValueError("no clips to merge")
|
||||
n = len(clips)
|
||||
ext = os.path.splitext(out_path)[1].lower()
|
||||
codec = _AUDIO_CODEC_BY_EXT.get(ext, [])
|
||||
cmd = [_bin("ffmpeg"), "-y"]
|
||||
for c in clips:
|
||||
cmd += ["-i", c]
|
||||
if len(clips) == 1:
|
||||
if n == 1:
|
||||
return cmd + ["-vn", *codec, out_path]
|
||||
if crossfade > 0:
|
||||
# Chain acrossfade: [0][1]->[a1]; [a1][2]->[a2]; …; last label = [out].
|
||||
# default acrossfade curve is 'tri' (linear); per-join curves deferred
|
||||
parts, prev = [], "0"
|
||||
for i in range(1, len(clips)):
|
||||
label = "out" if i == len(clips) - 1 else f"a{i}"
|
||||
parts.append(f"[{prev}][{i}]acrossfade=d={round(crossfade, 3)}[{label}]")
|
||||
prev = label
|
||||
fc = ";".join(parts)
|
||||
# normalize per-join params (n-1 joins)
|
||||
xfs = [float(crossfade)] * (n - 1) if isinstance(crossfade, (int, float)) \
|
||||
else [float(x) for x in crossfade]
|
||||
if curves is None or isinstance(curves, str):
|
||||
cvs = [curves or "tri"] * (n - 1)
|
||||
else:
|
||||
inputs = "".join(f"[{i}]" for i in range(len(clips)))
|
||||
fc = f"{inputs}concat=n={len(clips)}:v=0:a=1[out]"
|
||||
cvs = [str(c) for c in curves]
|
||||
if len(xfs) != n - 1:
|
||||
raise ValueError(f"crossfade list must have {n - 1} entries, got {len(xfs)}")
|
||||
if len(cvs) != n - 1:
|
||||
raise ValueError(f"curves list must have {n - 1} entries, got {len(cvs)}")
|
||||
for cv in cvs:
|
||||
if cv not in _ACROSSFADE_CURVES:
|
||||
raise ValueError(f"unknown acrossfade curve: {cv!r}")
|
||||
# Chain per join: [0][1]->[a1]; [a1][2]->[a2]; …; last label = [out].
|
||||
# A join uses acrossfade when its duration>0, else per-pair concat=n=2.
|
||||
# The curve suffix is emitted only for non-'tri' curves ('tri' is ffmpeg's
|
||||
# default, so omitting it keeps the default command strings byte-identical).
|
||||
parts, prev = [], "0"
|
||||
for i in range(1, n):
|
||||
j = i - 1
|
||||
label = "out" if i == n - 1 else f"a{i}"
|
||||
xf = xfs[j] if j < len(xfs) else 0.0
|
||||
cv = cvs[j] if j < len(cvs) else "tri"
|
||||
if xf > 0:
|
||||
suffix = f":c1={cv}:c2={cv}" if cv != "tri" else ""
|
||||
parts.append(f"[{prev}][{i}]acrossfade=d={round(xf, 3)}{suffix}[{label}]")
|
||||
else:
|
||||
parts.append(f"[{prev}][{i}]concat=n=2:v=0:a=1[{label}]")
|
||||
prev = label
|
||||
fc = ";".join(parts)
|
||||
return cmd + ["-filter_complex", fc, "-map", "[out]", *codec, out_path]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user