Spaces:
Runtime error
Runtime error
Add side-chain background music ducking operation
Browse files- app/operations/mix_audio.py +54 -0
app/operations/mix_audio.py
CHANGED
|
@@ -138,3 +138,57 @@ async def add_audio(
|
|
| 138 |
"duration_seconds": duration,
|
| 139 |
},
|
| 140 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
"duration_seconds": duration,
|
| 139 |
},
|
| 140 |
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
async def duck_background_music(
|
| 144 |
+
ffmpeg: FFmpegService,
|
| 145 |
+
inputs: Sequence[InputMedia],
|
| 146 |
+
params: dict[str, Any],
|
| 147 |
+
output_dir: Path,
|
| 148 |
+
) -> OperationResult:
|
| 149 |
+
"""Mix background music under the video's narration using side-chain compression."""
|
| 150 |
+
require_inputs(inputs, 2)
|
| 151 |
+
video, music = inputs[0], inputs[1]
|
| 152 |
+
duration = _video_duration(inputs)
|
| 153 |
+
if not _has_audio(video):
|
| 154 |
+
raise InputError("Background-music ducking requires narration audio in the source video")
|
| 155 |
+
music_volume = bounded_number(params, "music_volume", 0.22, 0, 2)
|
| 156 |
+
voice_volume = bounded_number(params, "voice_volume", 1, 0, 4)
|
| 157 |
+
threshold = bounded_number(params, "threshold", 0.03, 0.001, 1)
|
| 158 |
+
ratio = bounded_number(params, "ratio", 8, 1, 20)
|
| 159 |
+
attack = bounded_number(params, "attack", 20, 1, 2000)
|
| 160 |
+
release = bounded_number(params, "release", 300, 10, 5000)
|
| 161 |
+
bitrate = _audio_bitrate(params)
|
| 162 |
+
duration_value = f"{duration:.6f}"
|
| 163 |
+
filters = (
|
| 164 |
+
f"[0:a]apad=whole_dur={duration_value},atrim=duration={duration_value},"
|
| 165 |
+
f"volume={voice_volume:.6g}[voice];"
|
| 166 |
+
f"[1:a]apad=whole_dur={duration_value},atrim=duration={duration_value},"
|
| 167 |
+
f"volume={music_volume:.6g}[music];"
|
| 168 |
+
f"[music][voice]sidechaincompress=threshold={threshold:.6g}:"
|
| 169 |
+
f"ratio={ratio:.6g}:attack={attack:.6g}:release={release:.6g}[ducked];"
|
| 170 |
+
f"[voice][ducked]amix=inputs=2:duration=first:normalize=0[mixed]"
|
| 171 |
+
)
|
| 172 |
+
output = output_path(output_dir, "ducked-audio", "mp4")
|
| 173 |
+
args: list[str | Path] = []
|
| 174 |
+
for media in inputs:
|
| 175 |
+
args.extend(["-i", media.temp_path])
|
| 176 |
+
args.extend([
|
| 177 |
+
"-filter_complex", filters, "-map", "0:v:0", "-map", "[mixed]",
|
| 178 |
+
"-c:v", "copy", "-c:a", "aac", "-b:a", bitrate,
|
| 179 |
+
"-movflags", "+faststart", output,
|
| 180 |
+
])
|
| 181 |
+
return await execute(
|
| 182 |
+
ffmpeg, args, output, "video.duck_background_music",
|
| 183 |
+
{
|
| 184 |
+
"preserve_video": True,
|
| 185 |
+
"mix_mode": "sidechain_ducking",
|
| 186 |
+
"music_volume": music_volume,
|
| 187 |
+
"voice_volume": voice_volume,
|
| 188 |
+
"threshold": threshold,
|
| 189 |
+
"ratio": ratio,
|
| 190 |
+
"attack_ms": attack,
|
| 191 |
+
"release_ms": release,
|
| 192 |
+
"duration_seconds": duration,
|
| 193 |
+
},
|
| 194 |
+
)
|