Java 音频文件切割与合并

需求说明

操作 说明 示例
切割 按指定时间点截取音频,保留某时刻之后的数据 一个 2 分钟的 MP3,保留第 30 秒之后的部分
合并 将一段音频拼接到另一段音频之前 在歌曲播放前插入广告/前奏

技术选型

本工具基于 JavaFX 8 快速实现本地化功能。根据操作类型选择不同的第三方库:

操作 格式 技术方案 原因
切割 MP3 JAudioTagger 直接读取 MP3 帧头信息(比特率、时长),按字节偏移截取
切割 WAV Java Sound APIjavax.sound.sampled WAV 是 PCM 无压缩格式,可按采样帧精确跳转
合并 MP3/WAV JavaCV(FFmpeg 封装) 处理格式转换、码率统一、元数据复制等复杂场景

环境准备

Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- JAudioTagger:用于读取/写入 MP3 元数据和帧头信息 -->
<dependency>
<groupId>net.jthink</groupId>
<artifactId>jaudiotagger</artifactId>
<version>3.0.1</version>
</dependency>

<!-- JavaCV:FFmpeg 的 Java 封装,用于音频格式转换和合并 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.9</version>
</dependency>

JAudioTagger 不暴露 MP3 音频格式控制的底层接口,因此合并操作改用 FFmpeg(通过 JavaCV 调用)。

核心功能实现

一、MP3 切割

原理:MP3 文件由 ID3 头 + 音频帧数据组成。通过 MP3AudioHeader 获取比特率(bitrate)和总时长,计算出目标时间点对应的字节偏移量,然后直接拷贝该范围内的音频帧数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 按指定时间切割 MP3 文件
*
* @param music 原始 MP3 文件
* @param savePath 输出路径
* @param time 保留起始时间(秒),丢弃此时刻之前的所有数据
* @return 切割后的文件
*/
public File cutMusicByTime(File music, String savePath, long time) {
// 起始时间转毫秒
long beginTime = time * 1000;

// 读取 MP3 帧头信息
MP3File mp3 = new MP3File(music);
MP3AudioHeader header = (MP3AudioHeader) mp3.getAudioHeader();
int length = header.getTrackLength(); // 总时长(秒)
long endTime = length * 1000; // 结束时间(毫秒)
long bitRateKbps = header.getBitRateAsNumber(); // 比特率(Kbps)

// 计算字节偏移:bps → bps → bytes/ms × ms
long beginBitRateBpm = (bitRateKbps * 1024L / 8L / 1000L) * beginTime;
long firstFrameByte = header.getMp3StartByte(); // 音频数据起始字节位置
long beginByte = firstFrameByte + beginBitRateBpm; // 切割起点
long endByte = beginByte + (bitRateKbps * 1024L / 8L / 1000L) * (endTime - beginTime);

// 执行文件拷贝
File dFile = new File(savePath);
RandomAccessFile dRaf = new RandomAccessFile(dFile, "rw");
RandomAccessFile sRaf = new RandomAccessFile(music, "rw");

// 1. 写入 MP3 头部(ID3 标签等)
for (long i = 0; i < firstFrameByte; i++) {
dRaf.write(sRaf.read());
}
// 2. 跳转到起始位置
sRaf.seek(beginByte);
// 3. 写入截取的音频帧数据
for (long i = 0; i <= endByte - beginByte; i++) {
dRaf.write(sRaf.read());
}

sRaf.close();
dRaf.close();
return dFile;
}

二、MP3 合并

原理:使用 FFmpeg 将两段音频依次编码写入同一个输出文件,自动处理格式统一和码率适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 合并两个音频文件(A 拼接在 B 之前)
*
* @param fileAPath 前置音频路径(如广告/前奏)
* @param fileBPath 主音频路径(如歌曲)
* @param outputPath 输出文件路径
*/
public static void convertAudioParameters(String fileAPath, String fileBPath, String outputPath)
throws FrameGrabber.Exception, FrameRecorder.Exception {

FFmpegFrameGrabber grabberA = new FFmpegFrameGrabber(fileAPath);
FFmpegFrameGrabber grabberB = new FFmpegFrameGrabber(fileBPath);
grabberA.start();
grabberB.start();

// 以 B 文件的音频参数为准初始化录音器
FFmpegFrameRecorder recorder = new FFmpegRecorder(outputPath, grabberB.getAudioChannels());
recorder.setSampleRate(grabberB.getSampleRate()); // 统一采样率
recorder.setAudioBitrate(grabberB.getAudioBitrate()); // 统一码率

// 复制 B 文件的元数据(标题、艺术家等)
for (Map.Entry<String, String> entry : grabberB.getMetadata().entrySet()) {
recorder.setMetadata(entry.getKey(), entry.getValue());
}

recorder.start();

Frame frameA;
Frame frameB;

// 先写入 A(前置音频)
while ((frameA = grabberA.grab()) != null) {
recorder.record(frameA);
}
// 再写入 B(主音频)
while ((frameB = grabberB.grab()) != null) {
recorder.record(frameB);
}

grabberA.stop();
grabberB.stop();
recorder.stop();
}

三、WAV 切割

原理:WAV 是未压缩的 PCM 格式,包含采样率、通道数、位深度等格式信息。利用 AudioInputStream.skip() 可精确跳转到指定时间的采样帧位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 按指定时间切割 WAV 文件
*
* @param inputFile 源 WAV 文件
* @param savePath 输出路径
* @param time 保留起始时间(秒)
* @return 切割后的文件
*/
public File cutMusicByTime(File inputFile, String savePath, long time) {
AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(inputFile);

int bytesPerFrame = audioInputStream.getFormat().getFrameSize();
int frameRate = (int) audioInputStream.getFormat().getFrameRate();

// 按采样帧跳转到目标时间点
audioInputStream.skip(time * frameRate * bytesPerFrame);

// 计算剩余帧数
long lengthToRead = audioInputStream.getFrameLength() * bytesPerFrame;
AudioInputStream shortenedStream = new AudioInputStream(
audioInputStream,
audioInputStream.getFormat(),
lengthToRead / bytesPerFrame
);

File outputFile = new File(savePath);
AudioSystem.write(shortenedStream, AudioFileFormat.Type.WAVE, outputFile);

// 拷贝元数据(如需保留)
copyAlbumInformation(inputFile, outputFile);

return outputFile;
}

private static void copyAlbumInformation(File sourceFile, File outputFile)
throws UnsupportedAudioFileException, IOException, TagException,
ReadOnlyFileException, CannotWriteException, CannotReadException, InvalidAudioFrameException {
AudioFile sourceAudioFile = AudioFileIO.read(sourceFile);
Tag sourceTag = sourceAudioFile.getTag();
if (sourceTag != null) {
AudioFile outputAudioFile = AudioFileIO.read(outputFile);
Tag outputTag = outputAudioFile.getTag();
Iterator<TagField> fields = sourceTag.getFields();
while (fields.hasNext()) {
outputTag.setField(fields.next());
}
outputAudioFile.commit();
}
}

四、WAV 合并

原理:WAV 是线性 PCM 数据流,两个同格式 WAV 文件可直接拼接。若格式不一致则先做格式转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 合并两个 WAV 文件(file1 + file2 → output)
*
* @param inputFile1 第一个 WAV 文件
* @param inputFile2 第二个 WAV 文件(拼接到后面)
* @param outputFilePath 输出文件路径
*/
public void mergeWAV(File inputFile1, File inputFile2, String outputFilePath)
throws IOException, UnsupportedAudioFileException {

AudioInputStream audioInputStream1 = AudioSystem.getAudioInputStream(inputFile1);
AudioInputStream audioInputStream2 = AudioSystem.getAudioInputStream(inputFile2);

AudioFormat format1 = audioInputStream1.getFormat();
AudioFormat format2 = audioInputStream2.getFormat();

// 格式不一致时做转换(以 file2 的格式为准)
if (!format1.matches(format2)) {
audioInputStream1 = AudioSystem.getAudioInputStream(format2, audioInputStream1);
format1 = audioInputStream1.getFormat();
}

// 使用 SequenceInputStream 串联两个流
AudioInputStream appendedFiles = new AudioInputStream(
new SequenceInputStream(audioInputStream1, audioInputStream2),
format1,
audioInputStream1.getFrameLength() + audioInputStream2.getFrameLength()
);

AudioSystem.write(appendedFiles, AudioFileFormat.Type.WAVE, new File(outputFilePath));
}

总结

功能 MP3 WAV
切割方式 按比特率计算字节偏移,拷贝帧数据 skip(时间×采样率×帧大小) 跳转
合并方式 FFmpeg 重编码(支持格式转换) SequenceInputStream 直接拼接(要求格式一致)
核心依赖 JAudioTagger Java Sound API(JDK 内置)
精度 受限于帧边界,可能存在 ±1 帧误差 采样级精确