天天看点

基于 CoreAudio 的音频编解码(一):音频解码系列文章目录前言基础知识音频解码总结参考资料

系列文章目录

  • 基于 CoreAudio 的音频编解码(一):音频解码
  • 基于 CoreAudio 的音频编解码(二):音频编码

前言

Core Audio 是iOS和MAC系统中的关于数字音频处理的基础,它是应用程序用来处理音频的一组软件框架,所有关于iOS音频开发的接口都是由Core Audio来提供或者经过它提供的接口来进行封装的。

下图是 Core Audio 框架结构,其功能可谓是丰富且强大,几乎涵盖了所有与音频处理相关的内容。

基于 CoreAudio 的音频编解码(一):音频解码系列文章目录前言基础知识音频解码总结参考资料

这篇文章中,我们关注 Core Audio 中编解码能力,也就是上图中

Audio File, Converter and Codec Services

的内容。

基础知识

在开始进入代码环节之前,让我们先熟悉下 Core Audio 中常见的几个哥们。它们将在编解码中扮演重要的角色。

Sample & Frame & Packet

一个 sample(采样) 也就是一个单一的值,它表示音频流在某个时间点、特定声道的值。例如在 0.1s 时左声道的 sample 值为 0.5

一个 frame(帧),它表示所有通道在特点时间点所有值的集合,对于立体声而言,一帧包括两个采样,而对于 5.1 声道,一帧包括 6 个采样。下图说明了 sample 与 frame 直接的关系:

基于 CoreAudio 的音频编解码(一):音频解码系列文章目录前言基础知识音频解码总结参考资料

一个 packet 是一个或者多个 frame 的集合。一个 packet 包含多少个 frame 是由声音格式决定的。例如 PCM 文件中,一个 packet 包含 1 个 frame,而 AAC 格式可能包括 1024 个 frame。

AudioStreamBasicDescription

struct AudioStreamBasicDescription
{
    Float64             mSampleRate;
    AudioFormatID       mFormatID;
    AudioFormatFlags    mFormatFlags;
    UInt32              mBytesPerPacket;
    UInt32              mFramesPerPacket;
    UInt32              mBytesPerFrame;
    UInt32              mChannelsPerFrame;
    UInt32              mBitsPerChannel;
    UInt32              mReserved;
};
           

AudioStreamBasicDescription

描述了音频流的基本信息,包括:

  • mSampleRate, 采样率
  • mFormatID,数据格式类型
  • mFormatFlags, 数据格式的补充说明,例如可以标记是否为交织数据、数据格式是否为 float 等。它非常的神秘,通过一些辅助的函数我们可以获取到正确的值。
  • mFramesPerPacket,即一个 packet 中包含 frame 的数量,如果是 PCM 数据,则为1。压缩格式略有不同,通过一些辅助的函数我们可以获取到正确的值。
  • mChannelsPerFrame,声道数
  • mBitsPerChannel,采样的位深。压缩格式则设置为 0
  • mBytesPerFrame,一帧数据的字节大小。如果是 PCM 格式其计算公式为

    mChannelsPerFrame * mBitsPerChannel / 8

    。压缩格式则设置为 0
  • mBytesPerPacket,一个 packet 的字节大小,如果是 PCM 格式其值与

    mBytesPerFrame

    一致。压缩格式则设置为 0
  • mReserved,总是为 0,用来做数据对齐的

AudioBuffer & AudioBufferList

struct 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

用于存放音频数据的结构体,其中

  • mNumberChannels,声道数
  • mDataByteSize,音频数据数量大小
  • mData,音频数据 buffer 的指针

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,用于解码和编码音频文件。

Show me the code

废话不多说,直接上代码

#include "tool_function.h"
#include <AudioToolbox/ExtendedAudioFile.h>
#include <iostream>
#include <fstream>
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

来释放资源。

总结

介绍了 Core Audio 常见数据结构和一些基本概念,并展示了如何利用

ExtAudioFile

进行音频解码。

完整代码在 CoreAudioExtAudioFileExample

参考资料

  • What Is Core Audio?
  • Supported Audio File and Data Formats in OS X
  • 使用扩展的Audio File读写音频文件
  • Reading MP3 files using ExtAudioFile functions

继续阅读