Java 音频文件切割与合并
需求说明
| 操作 |
说明 |
示例 |
| 切割 |
按指定时间点截取音频,保留某时刻之后的数据 |
一个 2 分钟的 MP3,保留第 30 秒之后的部分 |
| 合并 |
将一段音频拼接到另一段音频之前 |
在歌曲播放前插入广告/前奏 |
技术选型
本工具基于 JavaFX 8 快速实现本地化功能。根据操作类型选择不同的第三方库:
| 操作 |
格式 |
技术方案 |
原因 |
| 切割 |
MP3 |
JAudioTagger |
直接读取 MP3 帧头信息(比特率、时长),按字节偏移截取 |
| 切割 |
WAV |
Java Sound API(javax.sound.sampled) |
WAV 是 PCM 无压缩格式,可按采样帧精确跳转 |
| 合并 |
MP3/WAV |
JavaCV(FFmpeg 封装) |
处理格式转换、码率统一、元数据复制等复杂场景 |
环境准备
Maven 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>net.jthink</groupId> <artifactId>jaudiotagger</artifactId> <version>3.0.1</version> </dependency>
<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
|
public File cutMusicByTime(File music, String savePath, long time) { long beginTime = time * 1000;
MP3File mp3 = new MP3File(music); MP3AudioHeader header = (MP3AudioHeader) mp3.getAudioHeader(); int length = header.getTrackLength(); long endTime = length * 1000; long bitRateKbps = header.getBitRateAsNumber();
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");
for (long i = 0; i < firstFrameByte; i++) { dRaf.write(sRaf.read()); } sRaf.seek(beginByte); 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
|
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();
FFmpegFrameRecorder recorder = new FFmpegRecorder(outputPath, grabberB.getAudioChannels()); recorder.setSampleRate(grabberB.getSampleRate()); recorder.setAudioBitrate(grabberB.getAudioBitrate());
for (Map.Entry<String, String> entry : grabberB.getMetadata().entrySet()) { recorder.setMetadata(entry.getKey(), entry.getValue()); }
recorder.start();
Frame frameA; Frame frameB;
while ((frameA = grabberA.grab()) != null) { recorder.record(frameA); } 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
|
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
|
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();
if (!format1.matches(format2)) { audioInputStream1 = AudioSystem.getAudioInputStream(format2, audioInputStream1); format1 = audioInputStream1.getFormat(); }
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 帧误差 |
采样级精确 |