java学习之音频处理工具FFmpeg
·
FFmpeg + Java 完整教程
目录
1. FFmpeg 简介
FFmpeg 是一套强大的音视频处理工具,支持:
- 音视频格式转换
- 音视频剪辑与拼接
- 提取音频/视频流
- 添加水印/字幕
- 调整音量/速度
- 录音/录屏
官网:https://ffmpeg.org
2. 安装 FFmpeg
macOS
brew install ffmpeg
Linux (Ubuntu/Debian)
sudo apt update
sudo apt install ffmpeg
Windows
下载:https://ffmpeg.org/download.html
验证安装
ffmpeg -version
3. 常用命令
3.1 查看音视频信息
ffmpeg -i input.mp3
3.2 格式转换
# MP3 转 AAC
ffmpeg -i input.mp3 output.aac
# WAV 转 MP3 (指定比特率)
ffmpeg -i input.wav -b:a 192k output.mp3
3.3 音频剪辑
# 从 00:01:30 开始,截取 30 秒
ffmpeg -ss 00:01:30 -i input.mp3 -t 30 -c copy output.mp3
# 从 90 秒开始,截取到 180 秒
ffmpeg -ss 90 -i input.mp3 -to 180 -c copy output.mp3
3.4 音频拼接
# 方法1: 使用 concat filter (推荐)
ffmpeg -i input1.mp3 -i input2.mp3 -i input3.mp3 \
-filter_complex "concat=n=3:v=0:a=1[out]" -map "[out]" output.mp3
# 方法2: 使用 concat demuxer (需要文件列表)
# filelist.txt 内容:
# file '/path/to/input1.mp3'
# file '/path/to/input2.mp3'
ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp3
3.5 音频混合
# 将两个音频混合在一起
ffmpeg -i background.mp3 -i voice.mp3 \
-filter_complex "[0:a][1:a]amix=inputs=2:duration=longest" output.mp3
3.6 调整音量
# 提高音量到 2 倍
ffmpeg -i input.mp3 -filter:a "volume=2.0" output.mp3
# 降低音量到 50%
ffmpeg -i input.mp3 -filter:a "volume=0.5" output.mp3
3.7 提取视频中的音频
ffmpeg -i video.mp4 -vn -acodec copy output.aac
4. Java 集成方式
4.1 核心原理
Java 通过 ProcessBuilder 调用 FFmpeg 命令行:
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg",
"-i", "input.mp3",
"output.wav"
);
Process process = pb.start();
int exitCode = process.waitFor();
4.2 封装工具类
package cn.radio.iov.api.util.ffmpeg;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* FFmpeg 工具类
*/
public class FfmpegUtil {
private final String ffmpegPath;
public FfmpegUtil(String ffmpegPath) {
this.ffmpegPath = ffmpegPath;
}
public FfmpegUtil() {
// 默认从系统路径查找
this.ffmpegPath = "ffmpeg";
}
/**
* 获取音频信息(时长、采样率、比特率等)
*/
public AudioInfo getAudioInfo(Path audioPath) throws IOException, InterruptedException {
List<String> command = List.of(
ffmpegPath,
"-i", audioPath.toString(),
"-f", "null",
"-"
);
ProcessResult result = execute(command, true);
// 解析输出获取信息
return AudioInfo.parse(result.getErrorOutput());
}
/**
* 音频格式转换
*/
public void convertAudio(Path input, Path output, String format) throws IOException, InterruptedException {
List<String> command = List.of(
ffmpegPath,
"-y",
"-i", input.toString(),
output.toString()
);
ProcessResult result = execute(command, false);
if (result.getExitCode() != 0) {
throw new RuntimeException("音频转换失败: " + result.getErrorOutput());
}
}
/**
* 音频剪辑
*/
public void clipAudio(Path input, Path output, double startSeconds, double duration) throws IOException, InterruptedException {
List<String> command = List.of(
ffmpegPath,
"-y",
"-ss", String.valueOf(startSeconds),
"-i", input.toString(),
"-t", String.valueOf(duration),
"-c", "copy",
output.toString()
);
ProcessResult result = execute(command, false);
if (result.getExitCode() != 0) {
throw new RuntimeException("音频剪辑失败: " + result.getErrorOutput());
}
}
/**
* 执行 FFmpeg 命令
*/
private ProcessResult execute(List<String> command, boolean suppressOutput) throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder(command);
if (!suppressOutput) {
pb.inheritIO();
}
Process process = pb.start();
// 读取错误输出(FFmpeg 信息输出到 stderr)
StringBuilder errorOutput = new StringBuilder();
try (var reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
errorOutput.append(line).append("\n");
}
}
int exitCode = process.waitFor();
return new ProcessResult(exitCode, errorOutput.toString());
}
/**
* 进程执行结果
*/
private static class ProcessResult {
private final int exitCode;
private final String errorOutput;
public ProcessResult(int exitCode, String errorOutput) {
this.exitCode = exitCode;
this.errorOutput = errorOutput;
}
public int getExitCode() {
return exitCode;
}
public String getErrorOutput() {
return errorOutput;
}
}
/**
* 音频信息
*/
public static class AudioInfo {
private double duration;
private int sampleRate;
private int bitrate;
private String codec;
public static AudioInfo parse(String output) {
AudioInfo info = new AudioInfo();
// 解析时长: Duration: 00:03:45.23
java.util.regex.Pattern durationPattern =
java.util.regex.Pattern.compile("Duration: (\\d+):(\\d+):(\\d+)\\.(\\d+)");
java.util.regex.Matcher matcher = durationPattern.matcher(output);
if (matcher.find()) {
int hours = Integer.parseInt(matcher.group(1));
int minutes = Integer.parseInt(matcher.group(2));
int seconds = Integer.parseInt(matcher.group(3));
int millis = Integer.parseInt(matcher.group(4));
info.duration = hours * 3600 + minutes * 60 + seconds + millis / 1000.0;
}
// 解析采样率: 44100 Hz
java.util.regex.Pattern samplePattern =
java.util.regex.Pattern.compile("(\\d+) Hz");
java.util.regex.Matcher sampleMatcher = samplePattern.matcher(output);
if (sampleMatcher.find()) {
info.sampleRate = Integer.parseInt(sampleMatcher.group(1));
}
return info;
}
public double getDuration() {
return duration;
}
public int getSampleRate() {
return sampleRate;
}
}
}
5. 实战案例
5.1 音频拆分器
将一个音频按时间段拆分成多个片段:
package cn.radio.iov.api.util.ffmpeg;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
/**
* 音频拆分器
*/
public class AudioSplitter {
private final String ffmpegPath;
public AudioSplitter(String ffmpegPath) {
this.ffmpegPath = ffmpegPath;
}
/**
* 拆分音频
* @param inputAudio 输入音频文件
* @param outputDir 输出目录
* @param clips 片段列表,每个片段包含 startTime, endTime
*/
public void split(Path inputAudio, Path outputDir, List<AudioClip> clips)
throws IOException, InterruptedException {
for (AudioClip clip : clips) {
String outputFile = String.format(
"clip_%02d_%s.mp3",
clip.getOrder(),
System.currentTimeMillis()
);
// 计算时长
double duration = clip.getEndTime() - clip.getStartTime();
ProcessBuilder pb = new ProcessBuilder(
ffmpegPath,
"-y", // 覆盖输出文件
"-ss", String.valueOf(clip.getStartTime()), // 开始时间
"-i", inputAudio.toString(),
"-t", String.valueOf(duration), // 持续时长
"-c", "copy", // 复制编码,不重新编码
outputDir.resolve(outputFile).toString()
);
pb.inheritIO();
Process p = pb.start();
int exitCode = p.waitFor();
if (exitCode != 0) {
throw new RuntimeException("拆分失败: " + clip.getOrder());
}
}
}
/**
* 音频片段
*/
public static class AudioClip {
private int order;
private double startTime; // 秒
private double endTime; // 秒
private String type;
public int getOrder() { return order; }
public double getStartTime() { return startTime; }
public double getEndTime() { return endTime; }
public String getType() { return type; }
// builder 模式
public static class Builder {
private final AudioClip clip = new AudioClip();
public Builder order(int order) { clip.order = order; return this; }
public Builder startTime(double startTime) { clip.startTime = startTime; return this; }
public Builder endTime(double endTime) { clip.endTime = endTime; return this; }
public Builder type(String type) { clip.type = type; return this; }
public AudioClip build() { return clip; }
}
}
}
5.2 音频拼接器
将多个音频拼接成一个:
package cn.radio.iov.api.util.ffmpeg;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
/**
* 音频拼接器
*/
public class AudioMerger {
private final String ffmpegPath;
public AudioMerger(String ffmpegPath) {
this.ffmpegPath = ffmpegPath;
}
/**
* 拼接多个音频
* @param audioUrls 音频地址列表,支持 HTTP(S) URL 或本地路径
* @param outputDir 输出目录
* @return 拼接后的音频文件路径
*/
public Path merge(List<String> audioUrls, Path outputDir)
throws IOException, InterruptedException {
Files.createDirectories(outputDir);
// 创建临时目录
Path tempDir = Files.createTempDirectory("audio_merge_");
try {
List<Path> localPaths = new ArrayList<>();
// 下载/复制所有音频到临时目录
for (int i = 0; i < audioUrls.size(); i++) {
String url = audioUrls.get(i);
Path tempFile = tempDir.resolve("input_" + i + ".mp3");
if (url.startsWith("http")) {
try (var in = new URL(url).openStream()) {
Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING);
}
} else {
Files.copy(Path.of(url), tempFile, StandardCopyOption.REPLACE_EXISTING);
}
localPaths.add(tempFile);
}
// 使用 ffmpeg concat filter 拼接
Path outputFile = outputDir.resolve("merged_" + System.currentTimeMillis() + ".mp3");
mergeWithFfmpeg(localPaths, outputFile);
return outputFile;
} finally {
// 清理临时目录
deleteDirectory(tempDir);
}
}
private void mergeWithFfmpeg(List<Path> inputs, Path output)
throws IOException, InterruptedException {
List<String> command = new ArrayList<>();
command.add(ffmpegPath);
command.add("-y");
// 添加所有输入文件
for (Path input : inputs) {
command.add("-i");
command.add(input.toString());
}
// concat filter: 拼接 n 个音频,输出 1 个音频流
String filter = "concat=n=" + inputs.size() + ":v=0:a=1[out]";
command.add("-filter_complex");
command.add(filter);
command.add("-map");
command.add("[out]");
command.add(output.toString());
ProcessBuilder pb = new ProcessBuilder(command);
pb.inheritIO();
Process p = pb.start();
if (p.waitFor() != 0) {
throw new RuntimeException("音频拼接失败");
}
}
private void deleteDirectory(Path dir) throws IOException {
if (!Files.exists(dir)) return;
var files = dir.toFile().listFiles();
if (files != null) {
for (var f : files) {
if (f.isDirectory()) {
deleteDirectory(f.toPath());
} else {
Files.delete(f.toPath());
}
}
}
Files.delete(dir);
}
}
5.3 使用示例
public class FfmpegDemo {
public static void main(String[] args) throws Exception {
String ffmpegPath = "/opt/homebrew/bin/ffmpeg";
// 1. 音频拆分
AudioSplitter splitter = new AudioSplitter(ffmpegPath);
List<AudioSplitter.AudioClip> clips = List.of(
new AudioSplitter.AudioClip.Builder()
.order(1).startTime(0).endTime(30).type("intro").build(),
new AudioSplitter.AudioClip.Builder()
.order(2).startTime(30).endTime(60).type("content").build()
);
splitter.split(
Path.of("/path/to/input.mp3"),
Path.of("/path/to/output"),
clips
);
// 2. 音频拼接
AudioMerger merger = new AudioMerger(ffmpegPath);
Path merged = merger.merge(
List.of(
"https://example.com/audio1.mp3",
"/path/to/audio2.mp3"
),
Path.of("/path/to/output")
);
System.out.println("合并完成: " + merged);
}
}
6. 常见问题
Q1: FFmpeg 输出到 stderr?
A: FFmpeg 默认将日志输出到 stderr,正常输出到 stdout。
Q2: -ss 参数放在 -i 前还是后?
A:
- 放在
-i前:快速定位(可能不精确) - 放在
-i后:精确定位(但会先解码前面的内容)
Q3: -c copy 和重新编码的区别?
A: -c copy 是复制流,速度快不损失质量,但某些操作(如拼接)需要重新编码。
Q4: concat filter vs concat demuxer?
A:
- concat filter: 重新编码,兼容性好
- concat demuxer: 使用
-c copy快速拼接,要求文件格式/编码完全一致
7. 参考资料
- FFmpeg 官方文档: https://ffmpeg.org/documentation.html
- FFmpeg Wiki: https://trac.ffmpeg.org/wiki
- concat filter 文档: https://ffmpeg.org/ffmpeg-filters.html#concat
更多推荐

所有评论(0)