basyx commited on
Commit
e04cdb1
·
verified ·
1 Parent(s): f5282b9

Add side-chain background music ducking operation

Browse files
Files changed (1) hide show
  1. 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
+ )