mp3 拆分
1. 项目背景与需求分析
目标:
将一个大的 MP3 文件按用户指定的大小拆分为多个小文件。拆分后,每个文件必须满足以下条件:
- 拆分点处不能破坏 MP3 帧结构;
- 每个拆分文件均为有效的 MP3 文件,可供语音识别模型处理;
- 返回拆分后文件的路径(以字符串数组形式返回给 Java 层)。
关键问题:
MP3 帧结构:
MP3 文件由一系列帧组成,每个帧包含帧头和音频数据。帧头中包含同步字、比特率、采样率、填充位等信息。直接按字节拆分可能会截断帧,从而导致文件无法播放或被识别模型错误处理。ID3v2 标签处理:
MP3 文件开头可能存在 ID3v2 标签,用于存储元数据。拆分时需要判断并跳过这些数据,直接处理后续的音频帧。拆分策略:
以完整帧为最小单位进行拆分,累计帧数据的大小。当累计大小即将超过用户指定的 size 时,结束当前文件并新建一个拆分文件继续写入。由于必须以完整帧为单位,拆分文件的大小可能略大于用户给定的 size,但能保证文件的完整性。依赖要求:
尽可能减少外部依赖(如不使用 FFmpeg 等库),因此采用自定义实现方式解析 MP3 帧。
2. 方案设计与实现步骤
2.1 MP3 帧解析
帧头同步字:
每个有效 MP3 帧的头信息以 0xFF 开头,接着至少 3 个比特为 1(0xE0)。通过检测同步字,可以判断是否处于帧头位置。解析版本、层、比特率和采样率:
根据 MP3 帧头格式,解析出 MPEG 版本、Layer(层)、比特率索引和采样率索引。本文档中示例代码只支持 MPEG-1 Layer III 格式。计算帧长度:
帧长度计算公式如下(对于 MPEG-1 Layer III):frame_length = (144 * bitrate) / samplerate + padding
其中
padding
为 0 或 1,由填充位决定。
2.2 ID3v2 标签处理
- 检测与跳过:
读取文件开头 10 个字节,如果前三个字节为 "ID3",则说明存在 ID3v2 标签。接着根据标签头中存储的大小信息(以同步安全整数存储)计算标签大小,并跳过整个标签区域,确保后续处理的是音频帧数据。
2.3 按帧拆分逻辑
逐帧读取:
从文件中依次读取每个 MP3 帧(先读 4 字节帧头,再计算整帧长度,回退到帧头起始处,读取完整帧数据)。拆分判断:
记录当前拆分文件已写入的字节数current_size
。如果再写入当前帧将导致文件大小超过用户指定的size
,则关闭当前文件,创建新文件,然后再写入该帧数据。文件名处理:
根据原始文件路径,去掉扩展名后生成输出文件名。例如:/path/to/file.mp3
拆分后生成/path/to/file_part1.mp3
,/path/to/file_part2.mp3
等。返回值:
在 JNI 函数结束时,将所有生成的拆分文件路径以 Java 字符串数组的形式返回给上层调用。
2.4 测试方案
C 语言测试:
可在 C 的main
函数中调用拆分函数(或模拟 JNI 调用),传入测试文件路径与拆分大小,验证拆分后的文件是否生成且数据正确。Java 单元测试:
编写 JUnit 单元测试,通过调用NativeMedia.splitMp3
方法,检查返回的文件路径数组,验证拆分后的文件是否存在且能被播放器或语音识别模型正确处理。
3. 完整代码示例
下面是基于上述设计的完整 C 代码示例,代码经过测试后可正确拆分 MP3 文件(假设输入为 MPEG-1 Layer III 格式):
native_mp3_split.c
#include "com_litongjava_media_NativeMedia.h"
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <stringapiset.h>
#endif
typedef struct {
int version;
int layer;
int bitrate; // bps
int samplerate; // Hz
int frame_length;
} MP3FrameInfo;
int parse_mp3_frame(const unsigned char *header, MP3FrameInfo *info) {
// Verify sync word
if (header[0] != 0xFF || (header[1] & 0xE0) != 0xE0) {
return 0;
}
// MPEG version
int version_bits = (header[1] >> 3) & 0x03;
if (version_bits == 0x03) info->version = 3; // MPEG-1
else if (version_bits == 0x02) info->version = 2; // MPEG-2
else return 0;
// Layer
int layer_bits = (header[1] >> 1) & 0x03;
if (layer_bits == 0x01) info->layer = 3;
else return 0;
// Bitrate index
int bitrate_index = (header[2] >> 4) & 0x0F;
const int bitrate_table[16] = {
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0
};
info->bitrate = bitrate_table[bitrate_index] * 1000;
// Sample rate
int samplerate_index = (header[2] >> 2) & 0x03;
const int samplerate_table[4] = {44100, 48000, 32000, 0};
info->samplerate = samplerate_table[samplerate_index];
if (info->bitrate == 0 || info->samplerate == 0) return 0;
// Calculate frame length
info->frame_length = (144 * info->bitrate) / info->samplerate;
if ((header[2] >> 1) & 0x01) info->frame_length++; // Padding
return 1;
}
JNIEXPORT jobjectArray JNICALL
Java_com_litongjava_media_NativeMedia_splitMp3(JNIEnv *env, jclass clazz, jstring srcPath, jlong size) {
const char *src_path = (*env)->GetStringUTFChars(env, srcPath, NULL);
if (!src_path) return NULL;
// Prepare output filenames
char base_path[1024];
strncpy(base_path, src_path, sizeof(base_path) - 1);
base_path[sizeof(base_path) - 1] = '\0';
// Remove extension
char *dot = strrchr(base_path, '.');
if (dot && strcasecmp(dot, ".mp3") == 0) *dot = '\0';
#ifdef _WIN32
int wlen = MultiByteToWideChar(CP_UTF8, 0, src_path, -1, NULL, 0);
wchar_t *wsrc_path = malloc(wlen * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, src_path, -1, wsrc_path, wlen);
FILE *input = _wfopen(wsrc_path, L"rb");
free(wsrc_path);
#else
FILE *input = fopen(src_path, "rb");
#endif
if (!input) {
(*env)->ReleaseStringUTFChars(env, srcPath, src_path);
return NULL;
}
// Skip ID3v2 tag
unsigned char id3_header[10];
if (fread(id3_header, 1, 10, input) == 10 &&
memcmp(id3_header, "ID3", 3) == 0) {
long tag_size = (id3_header[6] << 21) | (id3_header[7] << 14) |
(id3_header[8] << 7) | id3_header[9];
fseek(input, tag_size + 10, SEEK_SET);
} else {
fseek(input, 0, SEEK_SET);
}
int split_count = 0;
size_t current_size = 0;
FILE *output = NULL;
char output_path[1024];
const size_t max_size = (size_t) size;
// Frame reading buffer
unsigned char header[4];
MP3FrameInfo frame_info;
while (fread(header, 1, 4, input) == 4) {
if (!parse_mp3_frame(header, &frame_info)) {
fseek(input, -3, SEEK_CUR);
continue;
}
if (frame_info.frame_length <= 4) {
fseek(input, -3, SEEK_CUR);
continue;
}
if (!output || current_size + frame_info.frame_length > max_size) {
if (output) fclose(output);
split_count++;
snprintf(output_path, sizeof(output_path), "%s_part%d.mp3", base_path, split_count);
#ifdef _WIN32
int wlen = MultiByteToWideChar(CP_UTF8, 0, output_path, -1, NULL, 0);
wchar_t *woutput_path = malloc(wlen * sizeof(wchar_t));
MultiByteToWideChar(CP_UTF8, 0, output_path, -1, woutput_path, wlen);
output = _wfopen(woutput_path, L"wb");
free(woutput_path);
#else
output = fopen(output_path, "wb");
#endif
if (!output) break;
current_size = 0;
}
fseek(input, -4, SEEK_CUR);
unsigned char *frame = malloc(frame_info.frame_length);
if (!frame || fread(frame, 1, frame_info.frame_length, input) != frame_info.frame_length) {
free(frame);
break;
}
fwrite(frame, 1, frame_info.frame_length, output);
free(frame);
current_size += frame_info.frame_length;
}
if (output) fclose(output);
fclose(input);
(*env)->ReleaseStringUTFChars(env, srcPath, src_path);
// Build result array
jclass stringClass = (*env)->FindClass(env, "java/lang/String");
jobjectArray result = (*env)->NewObjectArray(env, split_count, stringClass, NULL);
for (int i = 0; i < split_count; i++) {
char path[1024];
snprintf(path, sizeof(path), "%s_part%d.mp3", base_path, i + 1);
jstring str = (*env)->NewStringUTF(env, path);
(*env)->SetObjectArrayElement(env, result, i, str);
(*env)->DeleteLocalRef(env, str);
}
return result;
}
4. 测试方法说明
4.1 在 C 的 main 函数中测试
可以编写一个简单的 main
函数,通过传入测试用的 MP3 文件路径和指定拆分大小,调用上述拆分逻辑。注意:
- 需要预先准备一个 MPEG-1 Layer III 格式的 MP3 文件。
- 通过检查生成的
_partX.mp3
文件是否存在且能正常播放,验证拆分逻辑正确性。
4.2 Java 单元测试
在 Java 层,可编写 JUnit 测试用例调用 NativeMedia.splitMp3
方法,验证返回的路径数组。测试步骤如下:
- 准备一个测试 MP3 文件,并确保其格式正确;
- 调用
NativeMedia.splitMp3(测试文件路径, 指定大小)
; - 遍历返回的文件路径数组,检查每个拆分文件是否存在;
- 根据需要可使用第三方工具或播放器对生成文件进行进一步验证。
package com.litongjava.media;
import org.junit.Test;
public class NativeMediaTest {
public static void main(String[] args) {
NativeMedia.splitMp3("1234", 0);
}
@Test
public void testSplitMp3() {
String testFile = "E:\\code\\java\\project-litongjava\\yt-dlp-java\\downloads\\490920099690696704\\01.mp3";
//long splitSize = 25 * 1024 * 1024; // 25MB
long splitSize = 10 * 1024 * 1024; // 10MB
String[] result = NativeMedia.splitMp3(testFile, splitSize);
for (String string : result) {
System.out.println(string);
}
}
}
5. 总结
本文档详细介绍了在 Java JNI 环境下实现 MP3 拆分的方案,关键在于正确解析 MP3 帧和处理 ID3v2 标签,从而保证拆分后文件的完整性和有效性。示例代码展示了如何:
- 跳过 ID3v2 标签;
- 解析 MP3 帧头并计算帧长度;
- 按帧拆分 MP3 文件,并生成拆分后的文件路径数组返回给 Java 层;
- 结合 C 端与 Java 端测试方法,确保功能正确。
通过以上方案和代码,开发者可以根据具体需求做进一步扩展和优化(例如支持更多 MP3 格式、处理异常情况等),最终实现一个高效且体积较小的 MP3 拆分模块。