- 基于 CoreAudio 的音频编解码(一):音频解码
- 基于 CoreAudio 的音频编解码(二):音频编码
Core Audio 是iOS和MAC系统中的关于数字音频处理的基础,它是应用程序用来处理音频的一组软件框架,所有关于iOS音频开发的接口都是由Core Audio来提供或者经过它提供的接口来进行封装的。
下图是 Core Audio 框架结构,其功能可谓是丰富且强大,几乎涵盖了所有与音频处理相关的内容。
这篇文章中,我们关注 Core Audio 中编解码能力,也就是上图中 Audio File, Converter and Codec Services
的内容。
在开始进入代码环节之前,让我们先熟悉下 Core Audio 中常见的几个哥们。它们将在编解码中扮演重要的角色。
一个 sample(采样) 也就是一个单一的值,它表示音频流在某个时间点、特定声道的值。例如在 0.1s 时左声道的 sample 值为 0.5
一个 frame(帧),它表示所有通道在特点时间点所有值的集合,对于立体声而言,一帧包括两个采样,而对于 5.1 声道,一帧包括 6 个采样。下图说明了 sample 与 frame 直接的关系:
一个 packet 是一个或者多个 frame 的集合。一个 packet 包含多少个 frame 是由声音格式决定的。例如 PCM 文件中,一个 packet 包含 1 个 frame,而 AAC 格式可能包括 1024 个 frame。
struct AudioStreamBasicDescription
{
Float64 mSampleRate;
AudioFormatID mFormatID;
AudioFormatFlags mFormatFlags;
UInt32 mBytesPerPacket;
UInt32 mFramesPerPacket;
UInt32 mBytesPerFrame;
UInt32 mChannelsPerFrame;
UInt32 mBitsPerChannel;
UInt32 mReserved;
};
AudioStreamBasicDescription
描述了音频流的基本信息,包括:
mChannelsPerFrame * mBitsPerChannel / 8
。压缩格式则设置为 0mBytesPerFrame
一致。压缩格式则设置为 0struct AudioBuffer
{
UInt32 mNumberChannels;
UInt32 mDataByteSize;
void* __nullable mData;
};
struct AudioBufferList
{
UInt32 mNumberBuffers;
AudioBuffer mBuffers[1]; // this is a variable length array of mNumberBuffers elements
};
AudioBuffer
用于存放音频数据的结构体,其中
AudioBufferList
表示一组 AudioBuffer
,当数据为交织类型是,创建 AudioBufferList
较为方便,如果数据为非交织类型时,还需要进行一些额外的设置。下面的展示了如何正确创建 AudioBufferList
const int num_channels = 2; // stereo audio
AudioBufferList* buffer_list = (AudioBufferList*)malloc(sizeof(AudioBufferList) + (num_channels - 1) * sizeof(AudioBuffer));
if(isInterleaveData()){
buffer_list->mNumberBuffers = 1;
buffer_list->mBuffers[0].mNumberChannels = num_channels; // stereo audio
buffer_list->mBuffers[0].mData = buffer; // pointer of interleave audio buffer
buffer_list->mBuffers[0].mDataByteSize = num_bytes_of_buffer;
}else{
buffer_list->mNumberBuffers = 2; // has two AudioBuffer
buffer_list->mBuffers[0].mNumberChannels = 1; // left channel
buffer_list->mBuffers[0].mData = left_channel_buffer; // pointer of left audio buffer
buffer_list->mBuffers[0].mDataByteSize = num_bytes_of_left_channel_buffer
buffer_list->mBuffers[1].mNumberChannels = 1; // right channel
buffer_list->mBuffers[1].mData = right_channel_buffer; // pointer of right audio buffer
buffer_list->mBuffers[1].mDataByteSize = num_bytes_of_right_channel_buffer;
}
free(buffer_list);
铺垫了这么多,让我们进入正题,如何利用 Core Audio 进行解码?
Core Audio 提供了很多 API,它们有 Low-Level 的,也有 High-Level 的,它们都能实现音频解码,其中最为方便的是 ExtAudioFile
。
ExtAudioFile
提供了 high-level 的音频文件访问能力,通过封装 AudioFile
和 AudioConverter
的 API,它提供了一套简洁的 API,用于解码和编码音频文件。
废话不多说,直接上代码
#include "tool_function.h"
#include
#include
#include
using namespace std;
int main(int argc, char* argv[])
{
if(argc < 2){
cerr << "Usage: decode /full/path/to/audiofile\n";
return -1;
}
CFURLRef audio_url = createCFURLWithStdString(string(argv[1]));
ON_SCOPE_EXIT([audio_url](){CFRelease(audio_url);});
ExtAudioFileRef file;
auto status = ExtAudioFileOpenURL(audio_url, &file);
assert(status == noErr);
SInt64 file_frame_length;
UInt32 size = sizeof(file_frame_length);
status = ExtAudioFileGetProperty(file, kExtAudioFileProperty_FileLengthFrames,
&size, &file_frame_length);
assert(status == noErr);
cout << "file frame length:" << file_frame_length << endl;
// get same basic information about input file
AudioStreamBasicDescription input_asbd;
size = sizeof(input_asbd);
ExtAudioFileGetProperty(file, kExtAudioFileProperty_FileDataFormat,
&size, &input_asbd);
UInt32 format4cc = CFSwapInt32HostToBig(input_asbd.mFormatID);
cout << "file format:" << (char*)(&format4cc) << endl;
cout << "is compressed: " << ((input_asbd.mBitsPerChannel == 0) ? "YES":"NO") << endl;
cout << "sample rate:" << input_asbd.mSampleRate << endl;
cout << "number of channels:" << input_asbd.mChannelsPerFrame << endl;
// set output file format
AudioStreamBasicDescription output_asbd;
FillOutASBDForLPCM(output_asbd, 44100, 2, 32, 32, true, false, false);
size = sizeof(output_asbd);
status = ExtAudioFileSetProperty(file, kExtAudioFileProperty_ClientDataFormat,
size,&output_asbd);
assert(status == noErr);
// now, we can read pcm from file
SInt64 frame_offset = 0;
UInt32 num_frame_read = 1024;
AudioBufferList buffer_list;
buffer_list.mNumberBuffers = 1;
buffer_list.mBuffers[0].mNumberChannels = output_asbd.mChannelsPerFrame;
buffer_list.mBuffers[0].mDataByteSize = num_frame_read * output_asbd.mBytesPerFrame;
buffer_list.mBuffers[0].mData = malloc(buffer_list.mBuffers[0].mDataByteSize);
fstream out("out.raw", std::ios::out | std::ios::binary);
for(;frame_offset < file_frame_length;){
status = ExtAudioFileRead(file, &num_frame_read, &buffer_list);
assert(status == noErr);
const auto num_read_bytes = num_frame_read * output_asbd.mBytesPerFrame;
out.write((char*)(buffer_list.mBuffers[0].mData), num_read_bytes);
frame_offset += num_frame_read;
}
free(buffer_list.mBuffers[0].mData);
ExtAudioFileDispose(file);
return 0;
}
首先,我们需要打开音频文件,其中 createCFURLWithStdString
是一个辅助函数,可以将 std::string
类型转换为 CFURLRef
类型。接着,通过 ExtAudioFileOpenURL
打开文件。
CFURLRef audio_url = createCFURLWithStdString(string(argv[1]));
ON_SCOPE_EXIT([audio_url](){CFRelease(audio_url);});
ExtAudioFileRef file;
auto status = ExtAudioFileOpenURL(audio_url, &file);
打开文件后,我们可能想知道输入音频的一些属性,例如音频长度、采样率、声道数等等。这些属性都可以通过 ExtAudioFileGetProperty
获取。在示例代码中,获取了音频的长度,以及输入音频的 AudioStreamBasicDescription
。
SInt64 file_frame_length;
UInt32 size = sizeof(file_frame_length);
status = ExtAudioFileGetProperty(file, kExtAudioFileProperty_FileLengthFrames,
&size, &file_frame_length);
AudioStreamBasicDescription input_asbd;
size = sizeof(input_asbd);
ExtAudioFileGetProperty(file, kExtAudioFileProperty_FileDataFormat,
&size, &input_asbd);
UInt32 format4cc = CFSwapInt32HostToBig(input_asbd.mFormatID);
cout << "file format:" << (char*)(&format4cc) << endl;
cout << "is compressed: " << ((input_asbd.mBitsPerChannel == 0) ? "YES":"NO") << endl;
cout << "sample rate:" << input_asbd.mSampleRate << endl;
cout << "number of channels:" << input_asbd.mChannelsPerFrame << endl;
接下来的一步非常重要,通过设置 kExtAudioFileProperty_ClientDataFormat
用来表明所期望得到的数据类型。很明显,我们想要的是 PCM 数据,最好是 float 的类型的,因此先构建期望类型的 AudioStreamBasicDescription
,并通过 ExtAudioFileSetProperty
进行设置。这一步的作用,类似于 ffmpeg 中 SwrContex,它对音频进行重采样,输出用户所期望的音频格式。关于 ffmpeg 解码请参考 基于 FFMPEG 的音频编解码(二):音频解码。
FillOutASBDForLPCM
是 Core Audio 提供的 inline 函数,方便用于设置 pcm 数据格式。它的具体接口说明可以在 CoreAudioTypes.h 中找到。这里需要关注最后一个参数 inIsNonInterleaved
为 false
,表明数据为交织的。
AudioStreamBasicDescription output_asbd;
FillOutASBDForLPCM(output_asbd, 44100, 2, 32, 32, true, false, false);
size = sizeof(output_asbd);
status = ExtAudioFileSetProperty(file, kExtAudioFileProperty_ClientDataFormat,
size,&output_asbd);
接着,创建 AudioBufferList
用于存放读取的音频数据,由于 output_asbd
中 inIsNonInterleaved=false
,说明音频数据为交织的,因此仅需要一个 AudioBuffer
即可。
UInt32 num_frame_read = 1024;
AudioBufferList buffer_list;
buffer_list.mNumberBuffers = 1;
buffer_list.mBuffers[0].mNumberChannels = output_asbd.mChannelsPerFrame;
buffer_list.mBuffers[0].mDataByteSize = num_frame_read * output_asbd.mBytesPerFrame;
buffer_list.mBuffers[0].mData = malloc(buffer_list.mBuffers[0].mDataByteSize);
万事俱备,就可以开始读取音频数据了。在 for
循环中,一个 block 一个 block 的读取数据,并将读取的 PCM 数据写入文件中。
fstream out("out.raw", std::ios::out | std::ios::binary);
for(;frame_offset < file_frame_length;){
status = ExtAudioFileRead(file, &num_frame_read, &buffer_list);
const auto num_read_bytes = num_frame_read * output_asbd.mBytesPerFrame;
out.write((char*)(buffer_list.mBuffers[0].mData), num_read_bytes);
frame_offset += num_frame_read;
}
最后,千万记得调用 ExtAudioFileDispose
来释放资源。
ExtAudioFileDispose(file);
介绍了 Core Audio 常见数据结构和一些基本概念,并展示了如何利用 ExtAudioFile
进行音频解码。
完整代码在 CoreAudioExtAudioFileExample