本文参考博客,多谢两位大神:
微信语音文件的解析
从微信中提取语音文件,并转化成文字的全自动化解决方案
iOS端微信的语音格式是aud,这种格式是微信自定义的格式,不能用普通的播放器直接播放,所以需要把aud转化为mp3。
那么aud格式实质上是什么呢?使用hex friend等十六进制编辑器打开一个aud文件,可以看到:
开头是一个0x02的字节,然后就是#!SILK_V3,所以只要把开头的一个字节删掉,就是一个silk文件O(∩_∩)O哈哈~。silk需要先解码为标准音频格式pcm,然后pcm再编码成mp3。
silk->pcm:
这一步使用的是silk-arm-ios,github上有人已经把它编译成静态库,这个静态库只支持arm64架构,也就是说不能在iPhone5以及以下的设备以及模拟器上面运行,不过现在用iPhone5的估计没多少人了吧..。
pcm->mp3:
这一步使用的是lame,也有相应的静态库。
接下来就是调用静态库的方法,把这几部分逻辑串起来:
- 去掉aud文件的第一个字节:
NSData *audData = [NSData dataWithContentsOfFile:audPath];
NSMutableData *audDataM = [NSMutableData dataWithData:audData];
[audDataM replaceBytesInRange:NSMakeRange(0, 1) withBytes:NULL length:0];
- silk转pcm:
- (int)covertSilkToPcmWithSilkPath:(NSString *)silkPath pcmPath:(NSString *)pcmPath {
//得到输入文件和输出文件的路径
bitInFileName = [silkPath cStringUsingEncoding:NSASCIIStringEncoding];
speechOutFileName = [pcmPath cStringUsingEncoding:NSASCIIStringEncoding];
//打开输入文件
inFile = fopen(bitInFileName, "rb");
if( inFile == NULL ) {
printf( "Error: could not open input file %s\n", bitInFileName );
return -999;
}
//验证文件头
{
char header_buf[50];
fread(header_buf, sizeof(char), strlen("#!SILK_V3"), inFile);
header_buf[strlen("#!SILK_V3")] = '\0';
if (strcmp(header_buf, "#!SILK_V3") != 0) {
printf( "Error: Wrong Header %s\n", header_buf );
return -999;
}
}
// 打开输出文件
outFile = fopen(speechOutFileName, "wb");
if (outFile == NULL) {
printf( "Error: could not open output file %s\n", speechOutFileName );
return -999;
}
// 设置采样率
if (sampleRate == 0) {
DecControl.API_sampleRate = 24000;
} else {
DecControl.API_sampleRate = sampleRate;
}
// 获取 Silk 解码器状态的字节大小
ret = SKP_Silk_SDK_Get_Decoder_Size(&decSizeBytes);
if (ret) {
printf( "\nSKP_Silk_SDK_Get_Decoder_Size returned %d", ret );
}
psDec = malloc((size_t) decSizeBytes);
// 初始化解码器
ret = SKP_Silk_SDK_InitDecoder(psDec);
if( ret ) {
printf( "\nSKP_Silk_InitDecoder returned %d", ret );
}
totPackets = 0;
while (1) {
// 读取有效数据大小
counter = fread(&nBytes, sizeof(SKP_int16), 1, inFile);
if (nBytes < 0 || counter < 1) {
break;
}
// 读取有效数据
counter = fread(payload, sizeof(SKP_uint8), (size_t) nBytes, inFile);
if ((SKP_int16) counter < nBytes) {
break;
}
payloadToDec = payload;
outPtr = out;
tot_len = 0;
frames = 0;
do {
// 解码
ret = SKP_Silk_SDK_Decode(psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len);
if( ret ) {
printf( "\nSKP_Silk_SDK_Decode returned %d", ret );
}
frames++;
outPtr += len;
tot_len += len;
if (frames > MAX_INPUT_FRAMES) {
outPtr = out;
tot_len = 0;
frames = 0;
}
} while (DecControl.moreInternalDecoderFrames);
packetSize_ms = tot_len / (DecControl.API_sampleRate / 1000);
totPackets++;
// 将解码后的数据保存到文件
fwrite(out, sizeof(SKP_int16), (size_t) tot_len, outFile);
}
free(psDec);
fclose(outFile);
fclose(inFile);
fileLength = totPackets * 1e-3 * packetSize_ms;
return 0;
}
- pcm->mp3:
- (int)covertPcmToMp3WithPcmPath:(NSString *)pcmPath mp3Path:(NSString *)mp3Path {
int state = -999;
@try {
int read, write;
FILE *pcm = fopen([pcmPath cStringUsingEncoding:NSASCIIStringEncoding], "rb"); //source
fseek(pcm, 4*1024, SEEK_CUR); //skip file header
FILE *mp3 = fopen([mp3Path cStringUsingEncoding:NSASCIIStringEncoding], "wb"); //output
const int PCM_SIZE = 8192;
const int MP3_SIZE = 8192;
short int pcm_buffer[PCM_SIZE*2];
unsigned char mp3_buffer[MP3_SIZE];
lame_t lame = lame_init(); // 初始化
lame_set_num_channels(lame, 2); // 双声道
lame_set_in_samplerate(lame, 12000); // 12k采样率
lame_set_brate(lame, 50); // 压缩的比特率为50
lame_set_quality(lame, 1); // mp3音质,很好
lame_init_params(lame);
do {
read = (int)fread(pcm_buffer, 2*sizeof(short int), PCM_SIZE, pcm);
if (read == 0)
write = lame_encode_flush(lame, mp3_buffer, MP3_SIZE);
else
write = lame_encode_buffer_interleaved(lame, pcm_buffer, read, mp3_buffer, MP3_SIZE);
fwrite(mp3_buffer, write, 1, mp3);
} while (read != 0);
lame_close(lame);
fclose(mp3);
fclose(pcm);
state = 0;
}
@catch (NSException *exception) {
state = -999;
}
@finally {
return state;
}
}
虽然吴航大神说把这几部分逻辑串起来一点都不难,但我还是花了一天时间...
所以我把代码封装了一下,后来者只需要调用一个方法就能成功转化了😆。github地址见文末。
首先把AudioTool文件夹拖入项目中,再添加libSKP_SILK_SDK.a和libmp3lame.a的链接,编译后会报错:
/AudioTool/libSKP_SILK_SDK.a(SKP_Silk_dec_API.o)' does not contain bitcode.
You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE),
obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64
因为这个libSKP_SILK_SDK.a不支持bitcode,所以要把bitcode关掉:
你如果不是用Xcode开发,或许需要这个:
XXX_LDFLAGS += -lmp3lame -lSKP_SILK_SDK
XXX_LDFLAGS += -read_only_relocs suppress
导入头文件"WXAudioManager.h",然后调用如下方法:
/*
audPath:aud文件路径
mp3Path:转化后mp3文件的存放路径
*/
- (NSError *)covertAudToMp3WithAudPath:(NSString *)audPath mp3Path:(NSString *)mp3Path;
这是一个耗时操作,需要在子线程调用。
如果转化成功,会在指定mp3Path生成一个mp3文件,并且返回nil。如果转化失败会返回错误原因和错误码,具体的错误还需要看lame.h和SKP_Silk_errors.h
如果你想调整转化后的音质和大小,可以在PCMEncoder.m里改变下面的参数:
lame_t lame = lame_init(); // 初始化
lame_set_num_channels(lame, 2); // 双声道
lame_set_in_samplerate(lame, 12000); // 12k采样率
lame_set_brate(lame, 50); // 压缩的比特率为50
lame_set_quality(lame, 1); // mp3音质,很好
lame_init_params(lame);
测试用的aud文件大小是7KB,转化后要达到和原来差不多的音质,mp3文件大小要达到24KB,这就是微信为什么要用aud的原因了吧。