diff --git a/domain/task.py b/domain/task.py index cce0294..eb55aa0 100644 --- a/domain/task.py +++ b/domain/task.py @@ -541,6 +541,14 @@ class Task: """获取片段列表""" return self.payload.get('segments', []) + def get_global_audio_fade_in_ms(self) -> int: + """获取全局音频淡入时长(毫秒),0 表示不淡入""" + return int(self.payload.get('globalAudioFadeInMs', 0)) + + def get_global_audio_fade_out_ms(self) -> int: + """获取全局音频淡出时长(毫秒),0 表示不淡出""" + return int(self.payload.get('globalAudioFadeOutMs', 0)) + def get_audio_profile(self) -> AudioProfile: """获取音频配置""" return AudioProfile.from_dict(self.payload.get('audioProfile')) diff --git a/handlers/prepare_audio.py b/handlers/prepare_audio.py index eaa311f..ce4ba74 100644 --- a/handlers/prepare_audio.py +++ b/handlers/prepare_audio.py @@ -110,12 +110,16 @@ class PrepareJobAudioHandler(BaseHandler): # 2. 构建音频混音命令 output_file = os.path.join(work_dir, 'audio_full.aac') + global_fade_in_ms = task.get_global_audio_fade_in_ms() + global_fade_out_ms = task.get_global_audio_fade_out_ms() cmd = self._build_audio_command( bgm_file=bgm_file, sfx_files=sfx_files, output_file=output_file, total_duration_sec=total_duration_sec, - audio_profile=audio_profile + audio_profile=audio_profile, + global_fade_in_ms=global_fade_in_ms, + global_fade_out_ms=global_fade_out_ms ) # 3. 执行 FFmpeg @@ -157,7 +161,9 @@ class PrepareJobAudioHandler(BaseHandler): sfx_files: List[Dict], output_file: str, total_duration_sec: float, - audio_profile: AudioProfile + audio_profile: AudioProfile, + global_fade_in_ms: int = 0, + global_fade_out_ms: int = 0 ) -> List[str]: """ 构建音频混音命令 @@ -168,6 +174,8 @@ class PrepareJobAudioHandler(BaseHandler): output_file: 输出文件路径 total_duration_sec: 总时长(秒) audio_profile: 音频配置 + global_fade_in_ms: 全局音频淡入时长(毫秒),0 不应用 + global_fade_out_ms: 全局音频淡出时长(毫秒),0 不应用 Returns: FFmpeg 命令参数列表 @@ -175,8 +183,23 @@ class PrepareJobAudioHandler(BaseHandler): sample_rate = audio_profile.sample_rate channels = audio_profile.channels + # 构建全局 afade 滤镜(作用于最终混合音频,在 amix 之后) + global_fade_filters = self._build_global_fade_filters( + total_duration_sec, global_fade_in_ms, global_fade_out_ms + ) + # 情况1:无 BGM 也无叠加音效 -> 生成静音 if not bgm_file and not sfx_files: + if global_fade_filters: + return [ + 'ffmpeg', '-y', '-hide_banner', + '-f', 'lavfi', + '-i', f'anullsrc=r={sample_rate}:cl=stereo', + '-t', str(total_duration_sec), + '-af', ','.join(global_fade_filters), + '-c:a', 'aac', '-b:a', '128k', + output_file + ] return [ 'ffmpeg', '-y', '-hide_banner', '-f', 'lavfi', @@ -188,10 +211,14 @@ class PrepareJobAudioHandler(BaseHandler): # 情况2:仅 BGM,无叠加音效 if not sfx_files: + af_arg = [] + if global_fade_filters: + af_arg = ['-af', ','.join(global_fade_filters)] return [ 'ffmpeg', '-y', '-hide_banner', '-i', bgm_file, '-t', str(total_duration_sec), + *af_arg, '-c:a', 'aac', '-b:a', '128k', '-ar', str(sample_rate), '-ac', str(channels), output_file @@ -261,10 +288,21 @@ class PrepareJobAudioHandler(BaseHandler): # dropout_transition=0 表示输入结束时不做渐变 mix_inputs = "[bgm]" + "".join(sfx_labels) num_inputs = 1 + len(sfx_files) - filter_parts.append( - f"{mix_inputs}amix=inputs={num_inputs}:duration=first:" - f"dropout_transition=0:normalize=0[out]" - ) + + # 如果有全局 fade,amix 输出到中间标签后再追加 afade;否则直接输出到 [out] + if global_fade_filters: + filter_parts.append( + f"{mix_inputs}amix=inputs={num_inputs}:duration=first:" + f"dropout_transition=0:normalize=0[mixed]" + ) + filter_parts.append( + f"[mixed]{','.join(global_fade_filters)}[out]" + ) + else: + filter_parts.append( + f"{mix_inputs}amix=inputs={num_inputs}:duration=first:" + f"dropout_transition=0:normalize=0[out]" + ) filter_complex = ';'.join(filter_parts) @@ -277,3 +315,33 @@ class PrepareJobAudioHandler(BaseHandler): ] return cmd + + @staticmethod + def _build_global_fade_filters( + total_duration_sec: float, + global_fade_in_ms: int, + global_fade_out_ms: int + ) -> List[str]: + """ + 构建全局音频淡入/淡出滤镜列表 + + 在 amix 混音输出之后追加,作用于最终混合音频。 + 与片段级 audioSpecJson 中的 fadeInMs/fadeOutMs(仅作用于单个叠加音效)独立。 + + Args: + total_duration_sec: 总时长(秒) + global_fade_in_ms: 全局淡入时长(毫秒),0 不应用 + global_fade_out_ms: 全局淡出时长(毫秒),0 不应用 + + Returns: + afade 滤镜字符串列表,可能为空 + """ + filters = [] + if global_fade_in_ms > 0: + fade_in_sec = global_fade_in_ms / 1000.0 + filters.append(f"afade=t=in:st=0:d={fade_in_sec}") + if global_fade_out_ms > 0: + fade_out_sec = global_fade_out_ms / 1000.0 + fade_out_start = total_duration_sec - fade_out_sec + filters.append(f"afade=t=out:st={fade_out_start}:d={fade_out_sec}") + return filters