iOS音频系列(二)--CoreAudio

这一篇主要是CoreAudio官方文档的重点内容的笔记。

通过回调函数与CoreAudio交互

iOS的CoreAudio是通过callback函数与App交互的。其中需要设置回调函数有以下几种情况:

  • CoreAudio会向回调函数给App传入PCM音频数据,然后App需要在回调函数中将音频数据写入文件文件系统。(录音时候)
  • CoreAudio会需要向App请求一些音频数据,App通过从文件系统中读取音频数据,然后通过callback函数传递给CoreAudio。(播放时候)
  • 通过注册属性观察者,监听CoreAudio的属性,注册回调函数

下面是一个使用Audio Queue Services的属性监听器的callback函数的调用模板。

typedef void (*AudioQueuePropertyListenerProc) (
                void *                  inUserData,
                AudioQueueRef           inAQ,
                AudioQueuePropertyID    inID
            );

在实现和使用这个callback函数时候,你需要完成两件事:

  • 实现这个函数。例如,你可以实现property listener callback,根据audio是否在running或者stop状态,去改变更新UI。
  • 注册callback函数时候带上userData数据,在callback函数触发时候使用。

下面是一个property listener callback函数的实现:

static void propertyListenerCallback (
    void                    *inUserData,
    AudioQueueRef           queueObject,
    AudioQueuePropertyID    propertyID
) {
    AudioPlayer *player = (AudioPlayer *) inUserData;
        // gets a reference to the playback object
    [player.notificationDelegate updateUserInterfaceOnAudioQueueStateChange: player];
        // your notificationDelegate class implements the UI update method
}

下面是注册一个callback函数的例子:

AudioQueueAddPropertyListener (
    self.queueObject,                // the object that will invoke your callback
    kAudioQueueProperty_IsRunning,   // the ID of the property you want to listen for
    propertyListenerCallback,        // a reference to your callback function
    self
);

Audio Data Formats

这部分内容是iOS中支持的音频格式,理解以后对后面的音频相关的编程能够理解更加深刻。

iOS中通用的音频数据类型

在CoreAudio中,使用AudioStreamBasicDescriptionAudioStreamPacketDescription这两个类型描述了通用的音频数据类型,包括压缩音频数据,非压缩的音频数据。他们的数据结构如下:

struct AudioStreamBasicDescription {
    Float64 mSampleRate;
    UInt32  mFormatID;
    UInt32  mFormatFlags;
    UInt32  mBytesPerPacket;
    UInt32  mFramesPerPacket;
    UInt32  mBytesPerFrame;
    UInt32  mChannelsPerFrame;
    UInt32  mBitsPerChannel;
    UInt32  mReserved;              //0
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

struct  AudioStreamPacketDescription {
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    UInt32  mDataByteSize;
};
typedef struct AudioStreamPacketDescription AudioStreamPacketDescription;

compressed audio formats use a varying number of bits per sample. For these formats, the value of the mBitsPerChannel member is 0.

Audio Data Packets

前面定义过,将一个或者多个frames称为一个packet。或者说packet是最有意义的一组frames,它在audio file中代表一个有意义的时间单元。使用Core Audio中一般是对packets进行处理的。

每个audio data格式在packets被装配完成以后,它的fromat就被确定了。ASBD数据结构通过mBytesPerPacketmFramesPerPacket描述音频格式的packet信息,其中页包含其他的信息。

在整个Core Audio中可能会用到三种不同的packets:

  • CBR (constant bit rate) formats:例如 linear PCM and IMA/ADPCM,所有的packet使用相同的大小。
  • VBR (variable bit rate) formats:例如 AAC,Apple Lossless,MP3,所有的packets拥有相同的frames,但是每个sample中的bits数目不同。
  • VFR (variable frame rate) formats:packets拥有数目不同的的frames。

在Core Audio中使用VBR或者VFR格式,使用ASPD结构体只能用来描述单个packet。如果是record或者play VBR或者VFR音频文件,需要涉及到多个ASPD结构。

在AudioFileService等接口中,都是通过pakcets工作的。例如AudioFileReadPackets会得到一系列的packets,同时会得到一个数组的AudioStreamPacketDescription

下面是通过packets计算audio data buffer的大小:

- (void) calculateSizesFor: (Float64) seconds {
 
    UInt32 maxPacketSize;
    UInt32 propertySize = sizeof (maxPacketSize);
 
    AudioFileGetProperty (
        audioFileID,
        kAudioFilePropertyPacketSizeUpperBound,
        &propertySize,
        &maxPacketSize
    );
 
    static const int maxBufferSize = 0x10000;   // limit maximum size to 64K
    static const int minBufferSize = 0x4000;    // limit minimum size to 16K
 
    if (audioFormat.mFramesPerPacket) {
        Float64 numPacketsForTime =
            audioFormat.mSampleRate / audioFormat.mFramesPerPacket * seconds;
        [self setBufferByteSize: numPacketsForTime * maxPacketSize];
    } else {
        // if frames per packet is zero, then the codec doesn't know the
        // relationship between packets and time. Return a default buffer size
        [self setBufferByteSize:
            maxBufferSize > maxPacketSize ? maxBufferSize : maxPacketSize];
    }
 
    // clamp buffer size to our specified range
    if (bufferByteSize > maxBufferSize && bufferByteSize > maxPacketSize) {
        [self setBufferByteSize: maxBufferSize];
    } else {
        if (bufferByteSize < minBufferSize) {
            [self setBufferByteSize: minBufferSize];
        }
    }
 
    [self setNumPacketsToRead: self.bufferByteSize / maxPacketSize];
}

Data Format Conversion

将音频数据从一种audio data转换成另外一种audio data。常见的有三中音频格式转换:

  • Decoding an audio format (such as AAC (Advanced Audio Coding)) to linear PCM format.
  • Converting linear PCM data into a different audio format.
  • Converting between different variants of linear PCM (for example, converting 16-bit signed integer linear PCM to 8.24 fixed-point linear PCM).

Sound Files

如果要使用声音文件,你需要使用Audio File Services的接口。一般而且,在iOS中需要与音频文件的创建,操作都离不开Audio File Services。

使用AudioFileGetGlobalInfoSizeAudioFileGetGlobalInfo分别分配info的内存和获取info的内容。你可以获取以下的内容:

  • Readable file types
  • Writable file types
  • For each writable type, the audio data formats you can put into the file

Creating a New Sound File

为了创建一个能够存储音频数据的audio file,你需要进行以下三步:

  • 使用CFURL或者NSURL表示的系统文件的路径
  • 你需要创建的文件的类型的标识identifier,这些identifier定义在Audio File Types枚举中。例如,为了创建一个CAF文件,你需要使用kAudioFileCAFType的identifier。
  • 创建过程中你需要提供音频数据的ASBD结构体。为了获取ASBD,你可以先提供ASBD结构体的部分成员的值,然后通过函数让Audio File Services将剩余的信息填满。

下面是创建一个AudioFile的方法:

AudioFileCreateWithURL (
    audioFileURL,
    kAudioFileCAFType,
    &audioFormat,
    kAudioFileFlags_EraseFile,
    &audioFileID   // the function provides the new file object here
);

Opening a Sound File

为了打开sound file,需要使用AudioFileOpenURL函数,该函数会返回一个唯一ID,供后面使用。

为了获取sound file的一些属性,通常使用AudioFileGetPropertyInfoAudioFileGetProperty,日常使用的属性以下:

  • kAudioFilePropertyFileFormat
  • kAudioFilePropertyDataFormat
  • kAudioFilePropertyMagicCookieData
  • kAudioFilePropertyChannelLayout

Reading From and Writing To a Sound File

iOS中,我们经常需要使用Audio File Services去读写audio data到sound file中。读和写是一对相反的内容,操作的对象都可以是bytes或者packets,但是一般而言都是直接使用的packets。

  • 读写VBR数据,只能使用packet
  • 直接使用packet,更加容易计算时间

iPhone Audio File Formats

iOS支持的sound file格式如下:

Format name Format filename extensions
AIFF .aif, .aiff
CAF .caf
MPEG-1, layer 3 .mp3
MPEG-2 or MPEG-4 ADTS .aac
MPEG-4 .m4a, .mp4
WAV .wav

iOS中的native format是CAF file format。


Audio Sessions: Cooperating with Core Audio

在iOS中,app在运行过程中有可能接到电话,如果此时正在播放sound,系统会做一定的处理。

AudioSession就是在这种情况的中间人,每个app都会有一个audio session。在播放或者录音时候需要session在做正确的事情,需要我们自己弄清楚以下的情况:

  • app收到系统的中断时候应该如何响应,比如收到phone call?
  • 你是否需要app的sound和其他后台运行的app的sounds混合播放,或者需要独占播放?
  • 你需要app如何响应远音频路径的响应,比如拔插耳机时候

AudioSession提供了三种类型的接口:

  • Categories: A category is a key that identifies a set of audio behaviors for your application. By setting a category, you indicate your audio intentions to iOS, such as whether your audio should continue when the screen locks.
  • Interruptions and route changes :Your audio session posts notifications when your audio is interrupted, when an interruption ends, and when the hardware audio route changes. These notifications let you respond to changes in the larger audio environment—such as an interruption due to in an incoming phone call—gracefully.
  • Hardware characteristics:You can query the audio session to discover characteristics of the device your application is running on, such as hardware sample rate, number of hardware channels, and whether audio input is available.

Audio Session Default Behavior

Audio Session拥有一些默认的行为策略:

  • 当用户将静音开关静音时,audio就会静音。
  • 当用户锁屏(手动,自动)时候,audio就会静音。
  • 当你app的audio启动时,其他app正在使用的audio就会静音。

audio session的这个特定的默认的行为策略被称为kAudioSessionCategory_SoloAmbientSound。同时,它还包括其他的多种策略选择。

Interruptions: Deactivation and Activation

默认的audio session的一个典型的特征是,audio会在中断以后自动恢复活动。Audio session有两个重要的状态:activeinactive。只有当Audio session处于active状态时候,app才能使用audio。

在app启动以后,你的默认的audio session就会是active状态。然而,如果一个电话被打进来,你的session就会立刻被置为inactive,然后app中的audio就会停止。这个电话就被称为一个中断,如果用户选择忽略电话,app就会继续运行。但是此时你的audio session依然会是inactive状态,audio也就不会工作。

如果你使用 Audio Queue Services操作audio,我们就需要给中断注册listener回调函数,手动去重启audio session。具体内容可以见Audio Session Programming Guide

Determining if Audio Input is Available

一个录音的app只有在设备的音频硬件可用的时候才能录音。为了检查这个属性,需要使用audio session的kAudioSessionProperty_AudioInputAvailable属性。

UInt32 audioInputIsAvailable;
UInt32 propertySize = sizeof (audioInputIsAvailable);
 
AudioSessionGetProperty (
    kAudioSessionProperty_AudioInputAvailable,
    &propertySize,
    &audioInputIsAvailable // A nonzero value on output means that
                           // audio input is available
);

Using Your Audio Session

你的app同时只能有一个audio session策略,你的所有的audio都需要遵循这个active策略的特点。如何响应中断,在Audio Session Programming Guide`中有更加详细的内容。

如果要测试Audio session,需要使用真机


Playback using the AVAudioPlayer Class

AVAudioPlayer提供了简单的OC接口用于audio播放。如果非网络stream,或者需要精确控制,apple推荐使用这个类,它可以用于播放iOS支持的任何audio format,同时这个类并不需要去设置audio session,因为它会在中断发生以后自动恢复播放,除非你需要指定特地的行为。

它可以完成以下工作:

  • Play sounds of any duration
  • Play sounds from files or memory buffers
  • Loop sounds
  • Play multiple sounds simultaneously
  • Control relative playback level for each sound you are playing
  • Seek to a particular point in a sound file, which supports such application features as fast forward and rewind
  • Obtain data that you can use for audio level metering

下面就是使用AVAudioPlayer的具体流程:

  1. Configuring an AVAudioPlayer object
NSString *soundFilePath =
                [[NSBundle mainBundle] pathForResource: @"sound"
                                                ofType: @"wav"];
 
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: soundFilePath];
 
AVAudioPlayer *newPlayer =
                [[AVAudioPlayer alloc] initWithContentsOfURL: fileURL
                                                       error: nil];
[fileURL release];
 
self.player = newPlayer;
[newPlayer release];
 
[self.player prepareToPlay];
[self.player setDelegate: self];

你的delegate对象用于处理interruptions或者音频播放停止以后的操作。

  1. Implementing an AVAudioPlayer delegate method
- (void) audioPlayerDidFinishPlaying: (AVAudioPlayer *) player
                        successfully: (BOOL) flag {
    if (flag == YES) {
        [self.button setTitle: @"Play" forState: UIControlStateNormal];
    }
}
  1. Controlling an AVAudioPlayer object
- (IBAction) playOrPause: (id) sender {
 
    // if already playing, then pause
    if (self.player.playing) {
        [self.button setTitle: @"Play" forState: UIControlStateHighlighted];
        [self.button setTitle: @"Play" forState: UIControlStateNormal];
        [self.player pause];
 
    // if stopped or paused, start playing
    } else {
        [self.button setTitle: @"Pause" forState: UIControlStateHighlighted];
        [self.button setTitle: @"Pause" forState: UIControlStateNormal];
        [self.player play];
    }
}

Recording and Playback using Audio Queue Services

Audio Queue Services是一个更加直观的record和play audio的方式。同时它还有更多个高级功能,可以使用这个服务完成更多的工作,比如对LPCM数据进行压缩等等。它和AVAudioPlayer是iOS中唯二可以播放压缩后音频格式的接口。使用Audio Queue Service播放和录音都是通过回调方法完成的。

Creating an Audio Queue Object

为了创建Audio Queue对象,它分成两类:

  • AudioQueueNewInput用于录音
  • AudioQueueNewOutput用于播放

使用audio queue object播放audio file,需要一些几个步骤:

  1. 创建数据结构用于管理audio queue需要的信息,例如audio format,audio fileID等。
  2. 定义callback函数,用于管理audio queue buffers。callback会使用Audio File Service去读取audio file用来播放
  3. 使用AudioQueueNewOutput用来播放audio file。

Creating an audio queue object具体的代码如下:

static const int kNumberBuffers = 3;
// Create a data structure to manage information needed by the audio queue
struct myAQStruct {
    AudioFileID                     mAudioFile;
    CAStreamBasicDescription        mDataFormat;
    AudioQueueRef                   mQueue;
    AudioQueueBufferRef             mBuffers[kNumberBuffers];
    SInt64                          mCurrentPacket;
    UInt32                          mNumPacketsToRead;
    AudioStreamPacketDescription    *mPacketDescs;
    bool                            mDone;
};
// Define a playback audio queue callback function
static void AQTestBufferCallback(
    void                   *inUserData,
    AudioQueueRef          inAQ,
    AudioQueueBufferRef    inCompleteAQBuffer
) {
    myAQStruct *myInfo = (myAQStruct *)inUserData;
    if (myInfo->mDone) return;
    UInt32 numBytes;
    UInt32 nPackets = myInfo->mNumPacketsToRead;
 
    AudioFileReadPackets (
        myInfo->mAudioFile,
        false,
        &numBytes,
        myInfo->mPacketDescs,
        myInfo->mCurrentPacket,
        &nPackets,
        inCompleteAQBuffer->mAudioData
    );
    if (nPackets > 0) {
        inCompleteAQBuffer->mAudioDataByteSize = numBytes;
        AudioQueueEnqueueBuffer (
            inAQ,
            inCompleteAQBuffer,
            (myInfo->mPacketDescs ? nPackets : 0),
            myInfo->mPacketDescs
        );
        myInfo->mCurrentPacket += nPackets;
    } else {
        AudioQueueStop (
            myInfo->mQueue,
            false
        );
        myInfo->mDone = true;
    }
}
// Instantiate an audio queue object
AudioQueueNewOutput (
    &myInfo.mDataFormat,
    AQTestBufferCallback,
    &myInfo,
    CFRunLoopGetCurrent(),
    kCFRunLoopCommonModes,
    0,
    &myInfo.mQueue
);

Controlling Audio Queue Playback Level

Audio Queue对象提供了两个方式控制音频level。第一种是直接使用AudioQueueSetParameter以及kAudioQueueParam_Volume参数,就可以设置,设置完成以后会立即生效。

Float32 volume = 1;
AudioQueueSetParameter (
    myAQstruct.audioQueueObject,
    kAudioQueueParam_Volume,
    volume
);

也可以通过AudioQueueEnqueueBufferWithParameters给audio queue buffer设置。这种方式只有在audio queue buffer 开始播放时候才起作用。

Indicating Audio Queue Playback Level

你也可以直接查询audio queue的kAudioQueueProperty_CurrentLevelMeterDB属性,得到的值是一组AudioQueueLevelMeterState结构体(一个channel一个数组),具体的结构体是AudioQueueLevelMeterState,显示如下:

typedef struct AudioQueueLevelMeterState {
    Float32     mAveragePower;
    Float32     mPeakPower;
};  AudioQueueLevelMeterState;

System Sounds: Alerts and Sound Effects

如果你需要播放的音频时间少于30s,那么可以使用System Sound Services。调用AudioServicesPlaySystemSound函数可以立即播放一个sound file。你也可以调用AudioServicesPlayAlertSound播放alert声音。这两个方法都会在手机静音的情况下振动。

当然,你也在调用AudioServicesPlaySystemSound方法使用kSystemSoundID_Vibrate属性,显示的触发振动。

为了使用AudioServicesPlaySystemSound方法播放sound,首先需要将sound file注册到系统中,得到一个sound ID,然后才能播放。

下面一段代码显示了使用System Sound Services去play sound:

#include <AudioToolbox/AudioToolbox.h>
#include <CoreFoundation/CoreFoundation.h>
 
// Define a callback to be called when the sound is finished
// playing. Useful when you need to free memory after playing.
static void MyCompletionCallback (
    SystemSoundID  mySSID,
    void * myURLRef
) {
        AudioServicesDisposeSystemSoundID (mySSID);
        CFRelease (myURLRef);
        CFRunLoopStop (CFRunLoopGetCurrent());
}
 
int main (int argc, const char * argv[]) {
    // Set up the pieces needed to play a sound.
    SystemSoundID    mySSID;
    CFURLRef        myURLRef;
    myURLRef = CFURLCreateWithFileSystemPath (
        kCFAllocatorDefault,
        CFSTR ("../../ComedyHorns.aif"),
        kCFURLPOSIXPathStyle,
        FALSE
    );
 
    // create a system sound ID to represent the sound file
    OSStatus error = AudioServicesCreateSystemSoundID (myURLRef, &mySSID);
 
    // Register the sound completion callback.
    // Again, useful when you need to free memory after playing.
    AudioServicesAddSystemSoundCompletion (
        mySSID,
        NULL,
        NULL,
        MyCompletionCallback,
        (void *) myURLRef
    );
 
    // Play the sound file.
    AudioServicesPlaySystemSound (mySSID);
 
    // Invoke a run loop on the current thread to keep the application
    // running long enough for the sound to play; the sound completion
    // callback later stops this run loop.
    CFRunLoopRun ();
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容