Royalty-Free Music Sources
| Source | Website | Features |
|---|---|---|
| Pixabay Music | https://pixabay.com/music/ | Completely free, no attribution required |
| Free Music Archive | https://freemusicarchive.org/ | CC licensed, some require attribution |
| Uppbeat | https://uppbeat.io/ | Free version has a monthly limit |
| Mixkit | https://mixkit.co/free-stock-music/ | Completely free |
Recommended: Pixabay Music (no attribution required, free for commercial use)
Music Style Selection
| Video Type | Recommended Style | Keyword Search |
|---|---|---|
| Tech Journey | Epic, Cinematic | "epic cinematic", "technology" |
| Product Introduction | Corporate, Upbeat | "corporate", "business" |
| Tutorial Video | Ambient, Soft | "ambient", "background" |
| Launch Event | Inspiring, Motivational | "inspiring", "motivational" |
| Creative Content | Modern, Electronic | "modern", "electronic" |
FFmpeg Audio Mixing
Basic Mixing (Voiceover Priority)
# 将 BGM 与配音混合,BGM 音量降低
# 配音密集时推荐 volume=0.05,配音稀疏时可用 0.08-0.10
ffmpeg -i voiceover.mp3 -i bgm.mp3 \
-filter_complex "[1:a]volume=0.05[bgm];[0:a][bgm]amix=inputs=2:duration=first[out]" \
-map "[out]" mixed_audio.mp3
With Fade-In/Fade-Out Effects
# BGM 淡入3秒,淡出3秒
ffmpeg -i voiceover.mp3 -i bgm.mp3 \
-filter_complex "
[1:a]volume=0.05,afade=t=in:st=0:d=3,afade=t=out:st=82:d=3[bgm];
[0:a][bgm]amix=inputs=2:duration=first[out]
" \
-map "[out]" mixed_audio.mp3
Complete Audio Processing Workflow
# 1. 处理 BGM(循环、淡入淡出、降低音量)
ffmpeg -stream_loop -1 -i bgm_original.mp3 \
-t 85 \
-af "volume=0.05,afade=t=in:st=0:d=3,afade=t=out:st=82:d=3" \
bgm_processed.mp3
# 2. 混合配音和 BGM
ffmpeg -i voiceover.mp3 -i bgm_processed.mp3 \
-filter_complex "[0:a][1:a]amix=inputs=2:duration=first[out]" \
-map "[out]" final_audio.mp3
# 3. 音量标准化
ffmpeg -i final_audio.mp3 \
-af "loudnorm=I=-16:TP=-1.5:LRA=11" \
final_audio_normalized.mp3
Volume Balance Recommendations
Core Principles
Voiceover Clarity Takes Priority: BGM must exist as "background" and must never overpower the voiceover. This is especially important when the vocal frequency range (85-255Hz fundamental + harmonics) overlaps with the BGM.
| Audio Type | Relative Volume | Description |
|---|---|---|
| Voiceover | 1.0 (100%) | Primary audio, keep at original volume |
| BGM (dense voiceover) | 0.05-0.08 | Ensure the voiceover is clearly distinguishable |
| BGM (sparse voiceover) | 0.08-0.12 | When there are many gaps in the voiceover |
| BGM (no voiceover sections) | 0.15-0.25 | Intro/outro/transitions |
| BGM (pure music sections) | 0.30-0.50 | Can be increased when there is no voiceover |
Volume Selection Decision Tree
视频有配音吗?
├── 是 → 配音密度高吗(>70%时间有语音)?
│ ├── 是 → BGM volume = 0.05-0.06 (推荐 0.05)
│ └── 否 → BGM volume = 0.08-0.10
└── 否 → BGM volume = 0.25-0.40
Testing Method
Before final rendering, extract a 30-second segment containing voiceover for testing:
# 快速测试不同音量
for vol in 0.04 0.05 0.06 0.08; do
ffmpeg -y -i bgm.mp3 -i voiceover.mp3 \
-filter_complex "[0:a]volume=$vol[bgm];[1:a][bgm]amix=inputs=2[out]" \
-map "[out]" -t 30 test_vol_$vol.mp3
done
Dynamic Volume Control (Advanced)
# 配音出现时自动降低 BGM(侧链压缩效果)
ffmpeg -i voiceover.mp3 -i bgm.mp3 \
-filter_complex "
[0:a]asplit=2[vo][vo_sidechain];
[1:a]volume=0.3[bgm];
[bgm][vo_sidechain]sidechaincompress=threshold=0.02:ratio=8:attack=50:release=500[bgm_ducked];
[vo][bgm_ducked]amix=inputs=2:duration=first[out]
" \
-map "[out]" ducked_audio.mp3
Smart BGM Length Calculation
Problem: The Drawbacks of Mindless Looping
A simple -stream_loop -1 will cause:
- BGM cut off at unnatural positions
- Music rhythm does not match video content
- Unnatural listening experience at loop seams
Solution: Scene-Aware BGM Selection
Strategy 1: Choose Music of an Appropriate Length
PreferBGM whose duration is close to or slightly longer than the video:
| Video Duration | Recommended BGM Duration | Strategy |
|---|---|---|
| < 60s | 60-90s | Single-track trimming |
| 60-120s | 90-180s | Single-track trimming + fade-out |
| 120-300s | 180-360s | Single track or carefully designed seams |
| > 300s | Multiple tracks spliced together | Switch at scene transitions |
Strategy 2: Calculate Based on Scene Structure
# 场景时间配置
SCENES = {
"opening": {"start": 0, "duration": 8}, # 片头
"scene1": {"start": 8, "duration": 14}, # 场景1
"scene2": {"start": 22, "duration": 16}, # 场景2
"scene3": {"start": 38, "duration": 14}, # 场景3
"scene4": {"start": 52, "duration": 20}, # 场景4
"closing": {"start": 72, "duration": 13}, # 片尾
}
def calculate_bgm_strategy(scenes: dict, bgm_duration: float, video_duration: float):
"""
计算 BGM 使用策略
Returns:
dict: 包含 trim_end, need_loop, loop_point 等信息
"""
if bgm_duration >= video_duration:
# BGM 够长,直接裁剪
return {
"strategy": "single_trim",
"trim_end": video_duration,
"need_loop": False
}
# BGM 不够长,找最佳循环点(场景边界)
scene_boundaries = sorted([s["start"] for s in scenes.values()])
# 找到最接近 BGM 时长的场景边界
best_loop_point = min(
scene_boundaries,
key=lambda x: abs(x - bgm_duration) if x <= bgm_duration else float('inf')
)
return {
"strategy": "scene_aware_loop",
"loop_point": best_loop_point,
"bgm_duration": bgm_duration,
"loops_needed": math.ceil(video_duration / best_loop_point)
}
Strategy 3: Dynamic Volume Adapted to Scenes
Use different BGM volumes for different scenes:
# 场景音量配置
SCENE_VOLUMES = {
"opening": 0.15, # 片头无配音,音量较高
"scene1": 0.05, # 有配音场景
"scene2": 0.05,
"scene3": 0.05,
"scene4": 0.05,
"closing": 0.12, # 片尾配音较少
}
def generate_volume_filter(scenes: dict, volumes: dict, fps: int = 30):
"""生成 FFmpeg 音量自动化滤镜"""
points = []
for name, scene in scenes.items():
start_frame = scene["start"] * fps
vol = volumes.get(name, 0.05)
points.append(f"volume=enable='between(t,{scene['start']},{scene['start']+scene['duration']})':volume={vol}")
return ",".join(points)
FFmpeg Scene-Aware Mixing
# 使用 aeval 实现场景动态音量
# 片头(0-8s): 0.15, 正片(8-72s): 0.05, 片尾(72-85s): 0.12
ffmpeg -i voiceover.mp3 -i bgm.mp3 \
-filter_complex "
[1:a]volume='if(lt(t,8),0.15,if(lt(t,72),0.05,0.12))':eval=frame,
afade=t=in:st=0:d=3,
afade=t=out:st=82:d=3[bgm];
[0:a][bgm]amix=inputs=2:duration=first[out]
" \
-map "[out]" mixed_audio.mp3
Best Practices
- Prioritize BGM with a suitable duration: avoid looping; choose music with a duration >= the video length
- If looping is necessary: set the loop point at scene transitions rather than at the end of the music
- Fade in/out at scene boundaries: Use crossfade at loop seams
- Dynamic volume: Intro and outro can be slightly higher, lower during dense voiceover sections
Remotion Dynamic Volume Control (Recommended)
In addition to using FFmpeg preprocessing, dynamic volume control can also be implemented directly in Remotion. This approach is more flexible and easier to debug.
Basic usage: scene-aware volume
import { Audio, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
import { SCENES } from "./config/scenes";
// 场景音量配置
const SCENE_VOLUMES = {
opening: 0.15, // 片头无配音,音量较高
scene1: 0.05, // 有配音场景
scene2: 0.05,
scene3: 0.05,
scene4: 0.05,
closing: 0.12, // 片尾配音较少
};
// 动态音量函数
const getDynamicVolume = (frame: number, fps: number): number => {
const currentTime = frame / fps;
// 遍历场景找到当前所在场景
for (const [sceneName, scene] of Object.entries(SCENES)) {
const sceneEnd = scene.start + scene.duration;
if (currentTime >= scene.start && currentTime < sceneEnd) {
return SCENE_VOLUMES[sceneName as keyof typeof SCENE_VOLUMES] ?? 0.05;
}
}
return 0.05; // 默认音量
};
// 在组件中使用
export const VideoWithDynamicBGM: React.FC = () => {
const { fps } = useVideoConfig();
return (
<>
{/* 配音 - 固定音量 */}
<Audio src={staticFile("audio/voiceover.mp3")} volume={1} />
{/* BGM - 动态音量 */}
<Audio
src={staticFile("audio/bgm.mp3")}
volume={(frame) => getDynamicVolume(frame, fps)}
/>
</>
);
};
Advanced usage: smooth transitions
import { interpolate } from "remotion";
const getSmoothVolume = (frame: number, fps: number): number => {
const currentTime = frame / fps;
// 片头 (0-8s): 0.15 -> 片头结束前0.5s开始降低
if (currentTime < 7.5) return 0.15;
if (currentTime < 8) {
return interpolate(currentTime, [7.5, 8], [0.15, 0.05]);
}
// 正片 (8-71.5s): 0.05
if (currentTime < 71.5) return 0.05;
// 正片结束 -> 片尾 (71.5-72s): 平滑过渡到 0.12
if (currentTime < 72) {
return interpolate(currentTime, [71.5, 72], [0.05, 0.12]);
}
// 片尾 (72-85s): 0.12
return 0.12;
};
Component template
// components/DynamicBGM.tsx
import React from "react";
import { Audio, staticFile, useVideoConfig } from "remotion";
interface DynamicBGMProps {
/** BGM 文件路径 (相对于 public/) */
src: string;
/** 场景音量配置 */
sceneVolumes: Record<string, number>;
/** 场景时间配置 */
scenes: Record<string, { start: number; duration: number }>;
/** 是否启用平滑过渡 (默认 true) */
smoothTransition?: boolean;
/** 过渡时长 (秒, 默认 0.5) */
transitionDuration?: number;
}
export const DynamicBGM: React.FC<DynamicBGMProps> = ({
src,
sceneVolumes,
scenes,
smoothTransition = true,
transitionDuration = 0.5,
}) => {
const { fps } = useVideoConfig();
const getVolume = (frame: number): number => {
const currentTime = frame / fps;
for (const [sceneName, scene] of Object.entries(scenes)) {
const sceneEnd = scene.start + scene.duration;
if (currentTime >= scene.start && currentTime < sceneEnd) {
return sceneVolumes[sceneName] ?? 0.05;
}
}
return 0.05;
};
return (
<Audio
src={staticFile(src)}
volume={(frame) => getVolume(frame)}
/>
);
};
FFmpeg vs Remotion comparison
| Aspect | FFmpeg preprocessing | Remotion dynamic control |
|---|---|---|
| Debugging convenience | ⚠️ Requires reprocessing audio | ✅ Real-time preview |
| Flexibility | ⚠️ Fixed once | ✅ Adjust anytime |
| Rendering performance | ✅ No extra overhead | ⚠️ Minor overhead |
| File size | ⚠️ Premixed files are large | ✅ Separate storage |
| Recommended scenario | Final delivery | Development and debugging |
Recommended workflow:
- During development, use Remotion dynamic control for quick debugging
- Once final parameters are determined, use FFmpeg preprocessing to generate the delivery version
Python script template
#!/usr/bin/env python3
"""
背景音乐处理脚本
"""
import subprocess
from pathlib import Path
def process_bgm(
bgm_path: str,
voiceover_path: str,
output_path: str,
video_duration: float,
bgm_volume: float = 0.05, # 默认低音量,确保配音清晰
fade_duration: float = 3.0
):
"""
处理背景音乐并与配音混合
Args:
bgm_path: BGM 文件路径
voiceover_path: 配音文件路径
output_path: 输出文件路径
video_duration: 视频总时长(秒)
bgm_volume: BGM 相对音量 (0.0-1.0)
fade_duration: 淡入淡出时长(秒)
"""
fade_out_start = video_duration - fade_duration
# 构建 filter_complex
filter_complex = f"""
[1:a]aloop=loop=-1:size=2e+09,atrim=0:{video_duration},
volume={bgm_volume},
afade=t=in:st=0:d={fade_duration},
afade=t=out:st={fade_out_start}:d={fade_duration}[bgm];
[0:a][bgm]amix=inputs=2:duration=first[out]
""".replace("\n", "").replace(" ", "")
cmd = [
"ffmpeg", "-y",
"-i", voiceover_path,
"-i", bgm_path,
"-filter_complex", filter_complex,
"-map", "[out]",
output_path
]
subprocess.run(cmd, check=True)
print(f"✓ 混合音频输出: {output_path}")
def normalize_audio(input_path: str, output_path: str):
"""音量标准化到 -16 LUFS"""
cmd = [
"ffmpeg", "-y",
"-i", input_path,
"-af", "loudnorm=I=-16:TP=-1.5:LRA=11",
output_path
]
subprocess.run(cmd, check=True)
print(f"✓ 标准化输出: {output_path}")
# 使用示例
if __name__ == "__main__":
process_bgm(
bgm_path="public/audio/bgm_epic.mp3",
voiceover_path="public/audio/synced_voiceover.mp3",
output_path="public/audio/mixed_audio.mp3",
video_duration=85,
bgm_volume=0.05, # 配音密集时推荐 0.05
fade_duration=3.0
)
normalize_audio(
input_path="public/audio/mixed_audio.mp3",
output_path="public/audio/final_audio.mp3"
)
Remotion integration
Use the mixed audio in a Remotion component:
import { Audio, staticFile } from "remotion";
export const FinalVideo: React.FC = () => {
return (
<>
{/* 使用混合后的音频(配音 + BGM) */}
<Audio src={staticFile("audio/final_audio.mp3")} volume={1} />
{/* 或者分开控制 */}
<Audio src={staticFile("audio/synced_voiceover.mp3")} volume={1} />
<Audio
src={staticFile("audio/bgm.mp3")}
volume={(f) => {
// 淡入淡出控制
const fps = 30;
if (f < 3 * fps) return (f / (3 * fps)) * 0.12;
if (f > 82 * fps) return ((85 * fps - f) / (3 * fps)) * 0.12;
return 0.12;
}}
/>
</>
);
};
Workflow
1. 确定视频风格 → 选择音乐关键词
2. 从免版权网站下载 BGM
3. 处理 BGM(循环/裁剪到视频时长)
4. 添加淡入淡出效果
5. 与配音混合(配音优先)
6. 音量标准化
7. 集成到视频
Common issues
| Issue | Solution |
|---|---|
| BGM too loud and interferes with voiceover | Lower volume to 0.04-0.06 (0.05 recommended during dense voiceover) |
| BGM not long enough | Prefer longer music; if looping is necessary, splice at scene boundaries |
| Fade in/out too abrupt | Increase fade_duration to 4-5 seconds |
| Inconsistent volume after mixing | Use the loudnorm filter to normalize |
| Style mismatch | Choose a more suitable BGM again |
| Loop seam is obvious | Use crossfade or set the loop point at a scene transition |