Duck soundtrack when using clip audio

This commit is contained in:
hbrain 2026-05-28 16:39:02 +00:00
parent 2ca263dd58
commit af3fd51226
2 changed files with 44 additions and 8 deletions

View file

@ -229,7 +229,7 @@ The most common visual timing/styling tweaks are exposed as constants near the t
| `INTRO_LOGO_EXTRA_HOLD` | Extra seconds the synthesized intro still remains before the first crossfade. | `6.0` | | `INTRO_LOGO_EXTRA_HOLD` | Extra seconds the synthesized intro still remains before the first crossfade. | `6.0` |
| `INTRO_LOGO_FADE_OFFSET` | Minimum time (s) the logo stays fully visible before the first xfade is allowed to start. | `3.0` | | `INTRO_LOGO_FADE_OFFSET` | Minimum time (s) the logo stays fully visible before the first xfade is allowed to start. | `3.0` |
| `INTRO_LOGO_DEFAULT_ALPHA` | Default opacity for the intro logo (1 = fully opaque, 0 = invisible). | `1.0` | | `INTRO_LOGO_DEFAULT_ALPHA` | Default opacity for the intro logo (1 = fully opaque, 0 = invisible). | `1.0` |
| `INTRO_TITLE_FADE_DELAY` | Seconds after start before the main title begins fading in. | `2.0` | | `INTRO_TITLE_FADE_DELAY` | Seconds after start before the main title begins fading in. | `3.0` |
| `INTRO_TITLE_FADE_LENGTH` | Seconds the fade-in/out lasts. | `1.0` | | `INTRO_TITLE_FADE_LENGTH` | Seconds the fade-in/out lasts. | `1.0` |
| `INTRO_TITLE_VISIBLE` | Seconds the title stays fully visible between fade in/out. | `4.0` | | `INTRO_TITLE_VISIBLE` | Seconds the title stays fully visible between fade in/out. | `4.0` |
| `DEFAULT_IMAGE_DURATION` | Default fully visible time per still image before fades are added. | `6.0` | | `DEFAULT_IMAGE_DURATION` | Default fully visible time per still image before fades are added. | `6.0` |
@ -239,6 +239,7 @@ The most common visual timing/styling tweaks are exposed as constants near the t
| `DEFAULT_AUDIO_FADE_OUT` | Default audio fade-out duration. | `10.0` | | `DEFAULT_AUDIO_FADE_OUT` | Default audio fade-out duration. | `10.0` |
| `DEFAULT_VIDEO_FADE` | Default video fade-in/out duration for the first/last real clips. | `1.0` | | `DEFAULT_VIDEO_FADE` | Default video fade-in/out duration for the first/last real clips. | `1.0` |
| `INTRO_VIDEO_FADE_ENABLED` | Whether the synthesized intro/logo pad gets the global fade-in effect (`False` keeps it fully visible from frame 0). | `False` | | `INTRO_VIDEO_FADE_ENABLED` | Whether the synthesized intro/logo pad gets the global fade-in effect (`False` keeps it fully visible from frame 0). | `False` |
| `CLIP_AUDIO_FADE` | Seconds used to fade video clip audio in/out while ducking the soundtrack. | `0.75` |
Adjust these constants and rerun `movmaker.py` to change the intro behavior without touching the rest of the pipeline. Adjust these constants and rerun `movmaker.py` to change the intro behavior without touching the rest of the pipeline.
@ -253,6 +254,7 @@ Adjust these constants and rerun `movmaker.py` to change the intro behavior with
- includes video clips inline - includes video clips inline
- crossfades between media items over `DEFAULT_CROSSFADE_DURATION` seconds by default (3s) - crossfades between media items over `DEFAULT_CROSSFADE_DURATION` seconds by default (3s)
- loops/trims music to match the video - loops/trims music to match the video
- when a clip's "Use video file audio" option is enabled, movmaker crossfades (`CLIP_AUDIO_FADE` seconds) from the soundtrack to that clip's audio, ducks the music while it plays, then fades the music back in
- if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query is `cinematic punk rock` - if no audio file exists in the input folder, searches Jamendo using `--music-genre` and downloads one random Creative Commons track that allows non-commercial reuse and derivative works; default query is `cinematic punk rock`
- reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env` - reads the Jamendo client ID from `JAMENDO_CLIENT_ID` or `~/.config/movmaker/jamendo.env`
- fades music in over `DEFAULT_AUDIO_FADE_IN` seconds (2s) and out over `DEFAULT_AUDIO_FADE_OUT` seconds (10s) by default - fades music in over `DEFAULT_AUDIO_FADE_IN` seconds (2s) and out over `DEFAULT_AUDIO_FADE_OUT` seconds (10s) by default

View file

@ -50,6 +50,11 @@ DEFAULT_AUDIO_FADE_IN = 2.0
DEFAULT_AUDIO_FADE_OUT = 10.0 DEFAULT_AUDIO_FADE_OUT = 10.0
DEFAULT_VIDEO_FADE = 1.0 DEFAULT_VIDEO_FADE = 1.0
INTRO_VIDEO_FADE_ENABLED = False INTRO_VIDEO_FADE_ENABLED = False
CLIP_AUDIO_FADE = 0.75
CLIP_AUDIO_SIDECHAIN_THRESHOLD = 0.04
CLIP_AUDIO_SIDECHAIN_RATIO = 8.0
CLIP_AUDIO_SIDECHAIN_ATTACK = 5.0
CLIP_AUDIO_SIDECHAIN_RELEASE = 250.0
JAMENDO_ENV = Path.home() / ".config" / "movmaker" / "jamendo.env" JAMENDO_ENV = Path.home() / ".config" / "movmaker" / "jamendo.env"
LOG_PATH: Path | None = None LOG_PATH: Path | None = None
@ -1109,7 +1114,7 @@ def build_video(
f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]" f"fontsize={stamp_size}:fontcolor=white:borderw=1:bordercolor=black[{final_label}]"
) )
audio_mix_labels: list[str] = [] bg_audio_label: str | None = None
if audio: if audio:
audio_index = len(clips) audio_index = len(clips)
audio_fade_out_start = max(0.0, total_duration - audio_fade) audio_fade_out_start = max(0.0, total_duration - audio_fade)
@ -1117,7 +1122,8 @@ def build_video(
f"[{audio_index}:a:0]atrim=0:{total_duration:.3f},asetpts=PTS-STARTPTS," f"[{audio_index}:a:0]atrim=0:{total_duration:.3f},asetpts=PTS-STARTPTS,"
f"afade=t=in:st=0:d={audio_fade_in:.3f},afade=t=out:st={audio_fade_out_start:.3f}:d={audio_fade:.3f}[bg_audio]" f"afade=t=in:st=0:d={audio_fade_in:.3f},afade=t=out:st={audio_fade_out_start:.3f}:d={audio_fade:.3f}[bg_audio]"
) )
audio_mix_labels.append("[bg_audio]") bg_audio_label = "bg_audio"
clip_audio_labels: list[str] = []
clip_audio_input = clip_audio_input_start clip_audio_input = clip_audio_input_start
for idx, clip_audio in enumerate(clip_audio_paths): for idx, clip_audio in enumerate(clip_audio_paths):
if clip_audio is None: if clip_audio is None:
@ -1138,7 +1144,7 @@ def build_video(
if segment <= 0: if segment <= 0:
clip_audio_input += 1 clip_audio_input += 1
continue continue
fade_len = min(fade, segment / 2) fade_len = min(CLIP_AUDIO_FADE, segment / 2)
delay_ms = max(0, int(start * 1000)) delay_ms = max(0, int(start * 1000))
label = f"clip_audio_{idx}" label = f"clip_audio_{idx}"
filters.append( filters.append(
@ -1146,10 +1152,38 @@ def build_video(
f"afade=t=in:st=0:d={fade_len:.3f},afade=t=out:st={max(0.0, segment - fade_len):.3f}:d={fade_len:.3f}," f"afade=t=in:st=0:d={fade_len:.3f},afade=t=out:st={max(0.0, segment - fade_len):.3f}:d={fade_len:.3f},"
f"adelay={delay_ms}:all=1[{label}]" f"adelay={delay_ms}:all=1[{label}]"
) )
audio_mix_labels.append(f"[{label}]") clip_audio_labels.append(label)
clip_audio_input += 1 clip_audio_input += 1
if audio_mix_labels: clip_mix_label: str | None = None
filters.append("".join(audio_mix_labels) + f"amix=inputs={len(audio_mix_labels)}:duration=longest:normalize=0[aout]") if clip_audio_labels:
if len(clip_audio_labels) == 1:
clip_mix_label = clip_audio_labels[0]
else:
inputs = "".join(f"[{name}]" for name in clip_audio_labels)
filters.append(inputs + f"amix=inputs={len(clip_audio_labels)}:duration=longest:normalize=0[clip_mix]")
clip_mix_label = "clip_mix"
final_bg_label = bg_audio_label
final_clip_label = clip_mix_label
if final_bg_label and final_clip_label:
sc_label = f"{final_clip_label}_sc"
mix_label = f"{final_clip_label}_main"
filters.append(f"[{final_clip_label}]asplit=2[{sc_label}][{mix_label}]")
filters.append(
f"[{final_bg_label}][{sc_label}]sidechaincompress=threshold={CLIP_AUDIO_SIDECHAIN_THRESHOLD}:"
f"ratio={CLIP_AUDIO_SIDECHAIN_RATIO}:attack={CLIP_AUDIO_SIDECHAIN_ATTACK}:"
f"release={CLIP_AUDIO_SIDECHAIN_RELEASE}[bg_ducked]"
)
final_bg_label = "bg_ducked"
final_clip_label = mix_label
if final_bg_label and final_clip_label:
filters.append(f"[{final_bg_label}][{final_clip_label}]amix=inputs=2:duration=longest:normalize=0[aout]")
elif final_bg_label:
filters.append(f"[{final_bg_label}]anull[aout]")
elif final_clip_label:
filters.append(f"[{final_clip_label}]anull[aout]")
cmd += ["-filter_complex", ";".join(filters), "-map", f"[{final_label}]"] cmd += ["-filter_complex", ";".join(filters), "-map", f"[{final_label}]"]
metadata = { metadata = {
@ -1168,7 +1202,7 @@ def build_video(
if value: if value:
cmd += ["-metadata", f"{key}={value}"] cmd += ["-metadata", f"{key}={value}"]
if audio_mix_labels: if final_bg_label or final_clip_label:
cmd += ["-map", "[aout]", "-c:a", "aac", "-b:a", "192k", "-shortest"] cmd += ["-map", "[aout]", "-c:a", "aac", "-b:a", "192k", "-shortest"]
else: else:
cmd += ["-an"] cmd += ["-an"]