一篇对iOS音频比较完善的文章

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一篇对iOS音频比较完善的文章相关的知识,希望对你有一定的参考价值。

转自:http://www.cnblogs.com/ios-mt/p/4268532.html

感谢作者:梦想通

 

 

前言

从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也因此对于iOS下的音频播放实现有了一定的研究。写这个系列的博客目的一方面希望能够抛砖引玉,另一方面也是希望能帮助国内其他的iOS开发者和爱好者少走弯路(我自己就遇到了不少的坑=。=)。

本篇为《iOS音频播放》系列的第一篇,主要将对iOS下实现音频播放的方法进行概述。


基础

先来简单了解一下一些基础的音频知识。

目前我们在计算机上进行音频播放都需要依赖于音频文件,音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,人耳所能听到的声音,最低的频率是从20Hz起一直到最高频率20KHZ,因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,只有采样频率高于声音信号最高频率的两倍时,才能把数字信号表示的声音还原成为原来的声音,所以音频文件的采样率一般在40~50KHZ,比如最常见的CD音质采样率44.1KHZ。

对声音进行采样、量化过程被称为脉冲编码调制(Pulse Code Modulation),简称PCM。PCM数据是最原始的音频数据完全无损,所以PCM数据虽然音质优秀但体积庞大,为了解决这个问题先后诞生了一系列的音频格式,这些音频格式运用不同的方法对音频数据进行压缩,其中有无损压缩(ALAC、APE、FLAC)和有损压缩(MP3、AAC、OGG、WMA)两种。

目前最为常用的音频格式是MP3,MP3是一种有损压缩的音频格式,设计这种格式的目的就是为了大幅度的减小音频的数据量,它舍弃PCM音频数据中人类听觉不敏感的部分,从下面的比较图我们可以明显的看到MP3数据相比PCM数据明显矮了一截(图片引自imp3论坛)。

技术分享上图为pcm数据技术分享上图为mp3数据

MP3格式中的码率(BitRate)代表了MP3数据的压缩质量,现在常用的码率有128kbit/s、160kbit/s、320kbit/s等等,这个值越高声音质量也就越高。MP3编码方式常用的有两种固定码率(Constant bitrate,CBR)和可变码率(Variable bitrate,VBR)。

MP3格式中的数据通常由两部分组成,一部分为ID3用来存储歌名、演唱者、专辑、音轨数等信息,另一部分为音频数据。音频数据部分以帧(frame)为单位存储,每个音频都有自己的帧头,如图所示就是一个MP3文件帧结构图(图片同样来自互联网)。MP3中的每一个帧都有自己的帧头,其中存储了采样率等解码必须的信息,所以每一个帧都可以独立于文件存在和播放,这个特性加上高压缩比使得MP3文件成为了音频流播放的主流格式。帧头之后存储着音频数据,这些音频数据是若干个PCM数据帧经过压缩算法压缩得到的,对CBR的MP3数据来说每个帧中包含的PCM数据帧是固定的,而VBR是可变的。

技术分享


iOS音频播放概述

了解了基础概念之后我们就可以列出一个经典的音频播放流程(以MP3为例):

  1. 读取MP3文件
  2. 解析采样率、码率、时长等信息,分离MP3中的音频帧
  3. 对分离出来的音频帧解码得到PCM数据
  4. 对PCM数据进行音效处理(均衡器、混响器等,非必须)
  5. 把PCM数据解码成音频信号
  6. 把音频信号交给硬件播放
  7. 重复1-6步直到播放完成

在iOS系统中apple对上述的流程进行了封装并提供了不同层次的接口(图片引自官方文档)。

技术分享CoreAudio的接口层次

下面对其中的中高层接口进行功能说明:

  • Audio File Services:读写音频数据,可以完成播放流程中的第2步;
  • Audio File Stream Services:对音频进行解码,可以完成播放流程中的第2步;
  • Audio Converter services:音频数据转换,可以完成播放流程中的第3步;
  • Audio Processing Graph Services:音效处理模块,可以完成播放流程中的第4步;
  • Audio Unit Services:播放音频数据:可以完成播放流程中的第5步、第6步;
  • Extended Audio File Services:Audio File Services和Audio Converter services的结合体;
  • AVAudioPlayer/AVPlayer(AVFoundation):高级接口,可以完成整个音频播放的过程(包括本地文件和网络流播放,第4步除外);
  • Audio Queue Services:高级接口,可以进行录音和播放,可以完成播放流程中的第3、5、6步;
  • OpenAL:用于游戏音频播放,暂不讨论

可以看到apple提供的接口类型非常丰富,可以满足各种类别类需求:

  • 如果你只是想实现音频的播放,没有其他需求AVFoundation会很好的满足你的需求。它的接口使用简单、不用关心其中的细节;

  • 如果你的app需要对音频进行流播放并且同时存储,那么AudioFileStreamer加AudioQueue能够帮到你,你可以先把音频数据下载到本地,一边下载一边用NSFileHandler等接口读取本地音频文件并交给AudioFileStreamer或者AudioFile解析分离音频帧,分离出来的音频帧可以送给AudioQueue进行解码和播放。如果是本地文件直接读取文件解析即可。(这两个都是比较直接的做法,这类需求也可以用AVFoundation+本地server的方式实现,AVAudioPlayer会把请求发送给本地server,由本地server转发出去,获取数据后在本地server中存储并转送给AVAudioPlayer。另一个比较trick的做法是先把音频下载到文件中,在下载到一定量的数据后把文件路径给AVAudioPlayer播放,当然这种做法在音频seek后就回有问题了。);

  • 如果你正在开发一个专业的音乐播放软件,需要对音频施加音效(均衡器、混响器),那么除了数据的读取和解析以外还需要用到AudioConverter来把音频数据转换成PCM数据,再由AudioUnit+AUGraph来进行音效处理和播放(但目前多数带音效的app都是自己开发音效模块来坐PCM数据的处理,这部分功能自行开发在自定义性和扩展性上会比较强一些。PCM数据通过音效器处理完成后就可以使用AudioUnit播放了,当然AudioQueue也支持直接使对PCM数据进行播放。)。下图描述的就是使用AudioFile + AudioConverter + AudioUnit进行音频播放的流程(图片引自官方文档)。

技术分享 

 

iOS音频播放 (二):AudioSession

 

AudioSession这个玩意的主要功能包括以下几点(图片来自官方文档):

  1. 确定你的app如何使用音频(是播放?还是录音?)
  2. 为你的app选择合适的输入输出设备(比如输入用的麦克风,输出是耳机、手机功放或者airplay)
  3. 协调你的app的音频播放和系统以及其他app行为(例如有电话时需要打断,电话结束时需要恢复,按下静音按钮时是否歌曲也要静音等)

技术分享AudioSession

AudioSession相关的类有两个:

  1. AudioToolBox中的AudioSession
  2. AVFoundation中的AVAudioSession

其中AudioSession在SDK 7中已经被标注为depracated,而AVAudioSession这个类虽然iOS 3开始就已经存在了,但其中很多方法和变量都是在iOS 6以后甚至是iOS 7才有的。所以各位可以依照以下标准选择:

  • 如果最低版本支持iOS 5,可以使用AudioSession,也可以使用AVAudioSession
  • 如果最低版本支持iOS 6及以上,请使用AVAudioSession

下面以AudioSession类为例来讲述AudioSession相关功能的使用(很不幸我需要支持iOS 5。。T-T,使用AVAudioSession的同学可以在其头文件中寻找对应的方法使用即可,需要注意的点我会加以说明).

注意:在使用AVAudioPlayer/AVPlayer时可以不用关心AudioSession的相关问题,Apple已经把AudioSession的处理过程封装了,但音乐打断后的响应还是要做的(比如打断后音乐暂停了UI状态也要变化,这个应该通过KVO就可以搞定了吧。。我没试过瞎猜的>_<)。

注意:在使用MPMusicPlayerController时不必关心AudioSession的问题。


初始化AudioSession

使用AudioSession类首先需要调用初始化方法:

1
2
3
4
extern OSStatus AudioSessionInitialize(CFRunLoopRef inRunLoop,
                                       CFStringRef inRunLoopMode,
                                       AudioSessionInterruptionListener inInterruptionListener,
                                       void *inClientData);

前两个参数一般填NULL表示AudioSession运行在主线程上(但并不代表音频的相关处理运行在主线程上,只是AudioSession),第三个参数需要传入一个AudioSessionInterruptionListener类型的方法,作为AudioSession被打断时的回调,第四个参数则是代表打断回调时需要附带的对象(即回到方法中的inClientData,如下所示,可以理解为UIView animation中的context)。

1
typedef void (*AudioSessionInterruptionListener)(void * inClientData, UInt32 inInterruptionState);

这才刚开始,坑就来了。这里会有两个问题:

第一,AudioSessionInitialize可以被多次执行,但AudioSessionInterruptionListener只能被设置一次,这就意味着这个打断回调方法是一个静态方法,一旦初始化成功以后所有的打断都会回调到这个方法,即便下一次再次调用AudioSessionInitialize并且把另一个静态方法作为参数传入,当打断到来时还是会回调到第一次设置的方法上。

这种场景并不少见,例如你的app既需要播放歌曲又需要录音,当然你不可能知道用户会先调用哪个功能,所以你必须在播放和录音的模块中都调用AudioSessionInitialize注册打断方法,但最终打断回调只会作用在先注册的那个模块中,很蛋疼吧。。。所以对于AudioSession的使用最好的方法是生成一个类单独进行管理,统一接收打断回调并发送自定义的打断通知,在需要用到AudioSession的模块中接收通知并做相应的操作。

Apple也察觉到了这一点,所以在AVAudioSession中首先取消了Initialize方法,改为了单例方法sharedInstance。在iOS 5上所有的打断都需要通过设置id<AVAudioSessionDelegate> delegate并实现回调方法来实现,这同样会有上述的问题,所以在iOS 5使用AVAudioSession下仍然需要一个单独管理AudioSession的类存在。在iOS 6以后Apple终于把打断改成了通知的形式。。这下科学了。

第二,AudioSessionInitialize方法的第四个参数inClientData,也就是回调方法的第一个参数。上面已经说了打断回调是一个静态方法,而这个参数的目的是为了能让回调时拿到context(上下文信息),所以这个inClientData需要是一个有足够长生命周期的对象(当然前提是你确实需要用到这个参数),如果这个对象被dealloc了,那么回调时拿到的inClientData会是一个野指针。就这一点来说构造一个单独管理AudioSession的类也是有必要的,因为这个类的生命周期和AudioSession一样长,我们可以把context保存在这个类中。


监听RouteChange事件

如果想要实现类似于“拔掉耳机就把歌曲暂停”的功能就需要监听RouteChange事件:

1
2
3
4
5
6
7
8
extern OSStatus AudioSessionAddPropertyListener(AudioSessionPropertyID inID,
                                                AudioSessionPropertyListener inProc,
                                                void *inClientData);
                                              
typedef void (*AudioSessionPropertyListener)(void * inClientData,
                                             AudioSessionPropertyID inID,
                                             UInt32 inDataSize,
                                             const void * inData);

调用上述方法,AudioSessionPropertyID参数传kAudioSessionProperty_AudioRouteChange,AudioSessionPropertyListener参数传对应的回调方法。inClientData参数同AudioSessionInitialize方法。

同样作为静态回调方法还是需要统一管理,接到回调时可以把第一个参数inData转换成CFDictionaryRef并从中获取kAudioSession_AudioRouteChangeKey_Reason键值对应的value(应该是一个CFNumberRef),得到这些信息后就可以发送自定义通知给其他模块进行相应操作(例如kAudioSessionRouteChangeReason_OldDeviceUnavailable就可以用来做“拔掉耳机就把歌曲暂停”)。

1
2
3
4
5
6
7
8
9
10
11
//AudioSession的AudioRouteChangeReason枚举
enum {
      kAudioSessionRouteChangeReason_Unknown = 0,
      kAudioSessionRouteChangeReason_NewDeviceAvailable = 1,
      kAudioSessionRouteChangeReason_OldDeviceUnavailable = 2,
      kAudioSessionRouteChangeReason_CategoryChange = 3,
      kAudioSessionRouteChangeReason_Override = 4,
      kAudioSessionRouteChangeReason_WakeFromSleep = 6,
      kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7,
      kAudioSessionRouteChangeReason_RouteConfigurationChange = 8
  };
1
2
3
4
5
6
7
8
9
10
11
12
//AVAudioSession的AudioRouteChangeReason枚举
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
  AVAudioSessionRouteChangeReasonUnknown = 0,
  AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
  AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
  AVAudioSessionRouteChangeReasonCategoryChange = 3,
  AVAudioSessionRouteChangeReasonOverride = 4,
  AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
  AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
  AVAudioSessionRouteChangeReasonRouteConfigurationChange NS_ENUM_AVAILABLE_IOS(7_0) = 8
}

注意:iOS 5下如果使用了AVAudioSession由于AVAudioSessionDelegate中并没有定义相关的方法,还是需要用这个方法来实现监听。iOS 6下直接监听AVAudioSession的通知就可以了。


这里附带两个方法的实现,都是基于AudioSession类的(使用AVAudioSession的同学帮不到你们啦)。

1、判断是否插了耳机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+ (BOOL)usingHeadset
{
#if TARGET_IPHONE_SIMULATOR
    return NO;
#endif

    CFStringRef route;
    UInt32 propertySize = sizeof(CFStringRef);
    AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route);

    BOOL hasHeadset = NO;
    if((route == NULL) || (CFStringGetLength(route) == 0))
    {
        // Silent Mode
    }
    else
    {
        /* Known values of route:
         * "Headset"
         * "Headphone"
         * "Speaker"
         * "SpeakerAndMicrophone"
         * "HeadphonesAndMicrophone"
         * "HeadsetInOut"
         * "ReceiverAndMicrophone"
         * "Lineout"
         */
        NSString* routeStr = (__bridge NSString*)route;
        NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"];
        NSRange headsetRange = [routeStr rangeOfString : @"Headset"];

        if (headphoneRange.location != NSNotFound)
        {
            hasHeadset = YES;
        }
        else if(headsetRange.location != NSNotFound)
        {
            hasHeadset = YES;
        }
    }

    if (route)
    {
        CFRelease(route);
    }

    return hasHeadset;
}

2、判断是否开了Airplay(来自StackOverflow):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (BOOL)isAirplayActived
{
    CFDictionaryRef currentRouteDescriptionDictionary = nil;
    UInt32 dataSize = sizeof(currentRouteDescriptionDictionary);
    AudioSessionGetProperty(kAudioSessionProperty_AudioRouteDescription, &dataSize, &currentRouteDescriptionDictionary);

    BOOL airplayActived = NO;
    if (currentRouteDescriptionDictionary)
    {
        CFArrayRef outputs = CFDictionaryGetValue(currentRouteDescriptionDictionary, kAudioSession_AudioRouteKey_Outputs);
        if(outputs != NULL && CFArrayGetCount(outputs) > 0)
        {
            CFDictionaryRef currentOutput = CFArrayGetValueAtIndex(outputs, 0);
            //Get the output type (will show airplay / hdmi etc
            CFStringRef outputType = CFDictionaryGetValue(currentOutput, kAudioSession_AudioRouteKey_Type);

            airplayActived = (CFStringCompare(outputType, kAudioSessionOutputRoute_AirPlay, 0) == kCFCompareEqualTo);
        }
        CFRelease(currentRouteDescriptionDictionary);
    }
    return airplayActived;
}

设置类别

下一步要设置AudioSession的Category,使用AudioSession时调用下面的接口

1
2
3
extern OSStatus AudioSessionSetProperty(AudioSessionPropertyID inID,
                                        UInt32 inDataSize,
                                        const void *inData);

如果我需要的功能是播放,执行如下代码

1
2
3
4
UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,
                         sizeof(sessionCategory),
                         &sessionCategory);

使用AVAudioSession时调用下面的接口

1
2
3
4
/* set session category */
- (BOOL)setCategory:(NSString *)category error:(NSError **)outError;
/* set session category with options */
- (BOOL)setCategory:(NSString *)category withOptions: (AVAudioSessionCategoryOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);

至于Category的类型在官方文档中都有介绍,我这里也只罗列一下具体就不赘述了,各位在使用时可以依照自己需要的功能设置Category。

1
2
3
4
5
6
7
8
9
//AudioSession的AudioSessionCategory枚举
enum {
      kAudioSessionCategory_AmbientSound               = ambi,
      kAudioSessionCategory_SoloAmbientSound           = solo,
      kAudioSessionCategory_MediaPlayback              = medi,
      kAudioSessionCategory_RecordAudio                = reca,
      kAudioSessionCategory_PlayAndRecord              = plar,
      kAudioSessionCategory_AudioProcessing            = proc  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//AudioSession的AudioSessionCategory字符串
/*  Use this category for background sounds such as rain, car engine noise, etc.  
 Mixes with other music. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAmbient;
  
/*  Use this category for background sounds.  Other music will stop playing. */
AVF_EXPORT NSString *const AVAudioSessionCategorySoloAmbient;

/* Use this category for music tracks.*/
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayback;

/*  Use this category when recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryRecord;

/*  Use this category when recording and playing back audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryPlayAndRecord;

/*  Use this category when using a hardware codec or signal processor while
 not playing or recording audio. */
AVF_EXPORT NSString *const AVAudioSessionCategoryAudioProcessing;

启用

有了Category就可以启动AudioSession了,启动方法:

1
2
3
4
5
6
7
8
//AudioSession的启动方法
extern OSStatus AudioSessionSetActive(Boolean active);
extern OSStatus AudioSessionSetActiveWithFlags(Boolean active, UInt32 inFlags);

//AVAudioSession的启动方法
- (BOOL)setActive:(BOOL)active error:(NSError **)outError;
- (BOOL)setActive:(BOOL)active withFlags:(NSInteger)flags error:(NSError **)outError NS_DEPRECATED_IOS(4_0, 6_0);
- (BOOL)setActive:(BOOL)active withOptions:(AVAudioSessionSetActiveOptions)options error:(NSError **)outError NS_AVAILABLE_IOS(6_0);

启动方法调用后必须要判断是否启动成功,启动不成功的情况经常存在,例如一个前台的app正在播放,你的app正在后台想要启动AudioSession那就会返回失败。

一般情况下我们在启动和停止AudioSession调用第一个方法就可以了。但如果你正在做一个即时语音通讯app的话(类似于微信、易信)就需要注意在deactive AudioSession的时候需要使用第二个方法,inFlags参数传入kAudioSessionSetActiveFlag_NotifyOthersOnDeactivationAVAudioSession给options参数传入AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation)。当你的app deactive自己的AudioSession时系统会通知上一个被打断播放app打断结束(就是上面说到的打断回调),如果你的app在deactive时传入了NotifyOthersOnDeactivation参数,那么其他app在接到打断结束回调时会多得到一个参数kAudioSessionInterruptionType_ShouldResume否则就是ShouldNotResume(AVAudioSessionInterruptionOptionShouldResume),根据参数的值可以决定是否继续播放。

大概流程是这样的:

  1. 一个音乐软件A正在播放;
  2. 用户打开你的软件播放对话语音,AudioSession active;
  3. 音乐软件A音乐被打断并收到InterruptBegin事件;
  4. 对话语音播放结束,AudioSession deactive并且传入NotifyOthersOnDeactivation参数;
  5. 音乐软件A收到InterruptEnd事件,查看Resume参数,如果是ShouldResume控制音频继续播放,如果是ShouldNotResume就维持打断状态;

官方文档中有一张很形象的图来阐述这个现象:

技术分享

然而现在某些语音通讯软件和某些音乐软件却无视了NotifyOthersOnDeactivationShouldResume的正确用法,导致我们经常接到这样的用户反馈:

你们的app在使用xx语音软件听了一段话后就不会继续播放了,但xx音乐软件可以继续播放啊。

好吧,上面只是吐槽一下。请无视我吧。

2014.7.14补充,7.19更新:

发现即使之前已经调用过AudioSessionInitialize方法,在某些情况下被打断之后可能出现AudioSession失效的情况,需要再次调用AudioSessionInitialize方法来重新生成AudioSession。否则调用AudioSessionSetActive会返回560557673(其他AudioSession方法也雷同,所有方法调用前必须首先初始化AudioSession),转换成string后为”!ini”即kAudioSessionNotInitialized,这个情况在iOS 5.1.x上比较容易发生,iOS 6.x 和 7.x也偶有发生(具体的原因还不知晓好像和打断时直接调用AudioOutputUnitStop有关,又是个坑啊)。

所以每次在调用AudioSessionSetActive时应该判断一下错误码,如果是上述的错误码需要重新初始化一下AudioSession。

附上OSStatus转成string的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <Endian.h>

NSString * OSStatusToString(OSStatus status)
{
    size_t len = sizeof(UInt32);
    long addr = (unsigned long)&status;
    char cstring[5];

    len = (status >> 24) == 0 ? len - 1 : len;
    len = (status >> 16) == 0 ? len - 1 : len;
    len = (status >>  8) == 0 ? len - 1 : len;
    len = (status >>  0) == 0 ? len - 1 : len;

    addr += (4 - len);

    status = EndianU32_NtoB(status);        // strings are big endian

    strncpy(cstring, (char *)addr, len);
    cstring[len] = 0;

    return [NSString stringWithCString:(char *)cstring encoding:NSMacOSRomanStringEncoding];
}

打断处理

正常启动AudioSession之后就可以播放音频了,下面要讲的是对于打断的处理。之前我们说到打断的回调在iOS 5下需要统一管理,在收到打断开始和结束时需要发送自定义的通知。

使用AudioSession时打断回调应该首先获取kAudioSessionProperty_InterruptionType,然后发送一个自定义的通知并带上对应的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void MyAudioSessionInterruptionListener(void *inClientData, UInt32 inInterruptionState)
{
    AudioSessionInterruptionType interruptionType = kAudioSessionInterruptionType_ShouldNotResume;
    UInt32 interruptionTypeSize = sizeof(interruptionType);
    AudioSessionGetProperty(kAudioSessionProperty_InterruptionType,
                            &interruptionTypeSize,
                            &interruptionType);

    NSDictionary *userInfo = @{MyAudioInterruptionStateKey:@(inInterruptionState),
                               MyAudioInterruptionTypeKey:@(interruptionType)};

    [[NSNotificationCenter defaultCenter] postNotificationName:MyAudioInterruptionNotification object:nil userInfo:userInfo];
}

收到通知后的处理方法如下(注意ShouldResume参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)interruptionNotificationReceived:(NSNotification *)notification
{
    UInt32 interruptionState = [notification.userInfo[MyAudioInterruptionStateKey] unsignedIntValue];
    AudioSessionInterruptionType interruptionType = [notification.userInfo[MyAudioInterruptionTypeKey] unsignedIntValue];
    [self handleAudioSessionInterruptionWithState:interruptionState type:interruptionType];
}

- (void)handleAudioSessionInterruptionWithState:(UInt32)interruptionState type:(AudioSessionInterruptionType

以上是关于一篇对iOS音频比较完善的文章的主要内容,如果未能解决你的问题,请参考以下文章

iOS开发系列文章(持续转载中……) 感谢作者,直接连接到作者文章的

Vue2的核心原理剖析

AudioKit 教程:入门

iOS 带通滤波器

Nacos系列第一篇-Nacos之Spring Discovery

环形队列