美文网首页
AVFoundation-播放和录制音频

AVFoundation-播放和录制音频

作者: Yasic | 来源:发表于2018-04-17 16:58 被阅读88次

    1 音频会话

    1.1 分类 category

    iOS 利用音频会话(audio session)实现可管理的音频环境,音频会话提供简单实用的方法使 OS 得知应用程序应该如何与 iOS 音频环境进行交互。AVFoundation 定义了 7 种分类来描述音频行为

    分类 作用 是否允许混音 音频输入输出模式 是否支持后台 是否遵循静音切换
    Ambient 游戏、效率应用程序 支持 O 不支持 不支持 遵循
    Solo Ambient(default) 游戏、效率应用程序 不支持 O 不支持 遵循
    Playback 音频和视频播放器 可选 O 支持 不遵循
    Record 录音机、音频捕捉 不支持 I 支持 不遵循
    Play and Record VoIP、语音聊天 可选 I/O 支持 不遵循
    Audio Processing 离线会话和处理 F 不能播放和录制 不遵循
    Multi-Route 使用外部硬件的高级 A/V 应用程序 F I/O 不遵循

    同时可以用 options 和 modes 进一步自定义开发。

    1.1.1 options

    options 有以下选项

    • AVAudioSessionCategoryOptionMixWithOthers

    支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute,AVAudioSessionCategoryAmbient 自动设置了此选项,AVAudioSessionCategoryOptionDuckOthers 和AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 也自动设置了此选项。如果使用这个选项激活会话,应用程序的音频不会中断从其他应用程序(如音乐应用程序)的音频,否则激活会话会打断其他音频会话。

    • AVAudioSessionCategoryOptionDuckOthers

    支持 AVAudioSessionCategoryAmbient,AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, 和 AVAudioSessionCategoryMultiRoute。设置此选项能够在播放音频时低音量听到后台播放的其他音频。整个选项周期与会话激活周期一致。

    • AVAudioSessionCategoryOptionAllowBluetooth

    支持 AVAudioSessionCategoryRecord,AVAudioSessionCategoryPlayAndRecord;允许蓝牙免提设备启用。当应用使用 setPreferredInput:error: 方法选择了蓝牙无线设备作为输入时,也会自动选择相应的蓝牙设备作为输出,使用 MPVolumeView 对象将蓝牙设备作为输出时,输入也会相应改变。

    • AVAudioSessionCategoryOptionDefaultToSpeaker

    支持 AVAudioSessionCategoryPlayAndRecord;在没有其他的音频路径(如耳机)可以使用的情况下设置这个选项,会议音频将通过设备的内置扬声器播放。当不设置此选项,并且没有其他的音频输出可用或选择时,音频将通过接收器播放。只有 iPhone 设备都配备有一个接收器; iPad 和 iPod touch 设备,此选项没有任何效果

    当你的 iPhone 接有多个外接音频设备时(耳塞,蓝牙耳机等),AudioSession 将遵循 last-in wins 的原则来选择外接设备,即声音将被导向最后接入的设备。

    当没有接入任何音频设备时,一般情况下声音会默认从扬声器出来,但有一个例外的情况:在 PlayAndRecord 这个 category 下,听筒会成为默认的输出设备。如果你想要改变这个行为,可以提供 MPVolumeView 来让用户切换到扬声器,也可通过 overrideOutputAudioPort 方法来 programmingly 切换到扬声器,也可以修改 category option 为AVAudioSessionCategoryOptionDefaultToSpeaker。

    • AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers

    支持 AVAudioSessionCategoryPlayAndRecord, AVAudioSessionCategoryPlayback, and AVAudioSessionCategoryMultiRoute,设置此选项能使应用程序的音频会话与其他会话混合,但是会中断使用了 AVAudioSessionModeSpokenAudio 模式的会话。其他应用的音频会在此会话启动后暂停,并在此会话关闭后重新恢复。

    在用到 AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers 选项时,中断了其他应用的音频后,自己的应用音频结束播放时,若想恢复其他应用的音频,需要在关闭音频会话的时候设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation 选项

    [session setActive:NO
           withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
                 error:<#Your error object, or nil for testing#>];
    
    • AVAudioSessionCategoryOptionAllowAirPlay

    支持 AVAudioSessionCategoryPlayAndRecord,允许会话在 AirPlay 设备上执行。

    1.1.2 mode

    mode 用于定制化 audio sessions,如果将分类的 mode 设置不合理会执行默认的模式行为,如将 AVAudioSessionCategoryMultiRoute 类别设置 AVAudioSessionModeGameChat 模式。

    • AVAudioSessionModeDefault 默认音频会话模式

    • AVAudioSessionModeVoiceChat 如果应用需要执行例如 VoIP 类型的双向语音通信则选择此模式

    • AVAudioSessionModeVideoChat 如果应用正在进行在线视频会议,请指定此模式

    • AVAudioSessionModeGameChat 该模式由Game Kit 提供给使用 Game Kit 的语音聊天服务的应用程序设置

    • AVAudioSessionModeVideoRecording 如果应用正在录制电影,则选此模式

    • AVAudioSessionModeMeasurement 如果您的应用正在执行音频输入或输出的测量,请指定此模式

    • AVAudioSessionModeMoviePlayback 如果您的应用正在播放电影内容,请指定此模式

    • AVAudioSessionModeSpokenAudio 当需要持续播放语音,同时希望在其他程序播放短语音时暂停播放此应用语音,选取此模式

    1.2 配置音频会话

    首先获得指向 AVAudioSession 的单例指针,设置合适的分类,最后激活会话。

        AVAudioSession *session = [AVAudioSession sharedInstance];
    
        NSError *error;
        if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
            NSLog(@"Category Error: %@", [error localizedDescription]);
        }
    
        if (![session setActive:YES error:&error]) {
            NSLog(@"Activation Error: %@", [error localizedDescription]);
        }
    

    2. 播放音频

    AVAudioPlayer 构建于 Core Audio 的 C-based Audio Queue Services 最顶层,局限性在于无法从网络流播放音频,不能访问原始音频样本,不能满足非常低的时延。

    2.1 创建 AVAudioPlayer

    可以通过 NSData 或本地音频文件的 NSURL 两种方式创建 AVAudioPlayer。

        NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"rock" withExtension:@"mp3"];
        self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
        if (self.player) {
            [self.player prepareToPlay];
        }
    

    创建出 AVAudioPlayer 后建议调用 prepareToPlay 方法,这个方法会取得需要的音频硬件并预加载 Audio Queue 的缓冲区,当然如果不主动调用,执行 play 方法时也会默认调用,但是会造成轻微播放的延时。

    2.2 对播放进行控制

    AVAudioPlayer 的 play 可以播放音频,stop 和 pause 都可以暂停播放,但是 stop 会撤销调用 prepareToPlay 所做的设置。

    • 修改播放器的音量:播放器音量独立于系统音量,音量或播放增益定义为 0.0(静音)到 1.0(最大音量)之间的浮点值
    • 修改播放器的 pan 值:允许使用立体声播放声音,pan 值从 -1.0(极左)到 1.0(极右),默认值 0.0(居中)
    • 调整播放率:0.5(半速)到 2.0(2 倍速)
    • 设置 numberOfLoops 实现无缝循环:-1 表示无限循环(音频循环可以是未压缩的线性 PCM 音频,也可以是 AAC 之类的压缩格式音频,MP3 格式不推荐循环)
    • 音频计量:当播放发生时从播放器读取音量力度的平均值和峰值

    2.3 实践

    2.3.1 播放音频

            NSTimeInterval delayTime = [self.players[0] deviceCurrentTime] + 0.01;
            for (AVAudioPlayer *player in self.players) {
                [player playAtTime:delayTime];
            }
            self.playing = YES;
    

    对于多个需要播放的音频,如果希望同步播放效果,则需要捕捉当前设备时间并添加一个小延时,从而具有一个从开始播放时间计算的参照时间。deviveCurrentTime 是一个独立于系统事件的音频设备的时间值,当有多于 audioPlayer 处于 play 或者 pause 状态时 deviveCurrentTime 会单调增加,没有时置位为 0。playAtTime 的参数 time 要求必须是基于 deviveCurrentTime 且大于等于 deviveCurrentTime 的时间。

    2.3.2 暂停播放

            for (AVAudioPlayer *player in self.players) {
                [player stop];
                player.currentTime = 0.0f;
            }
    

    暂停时需要将 audioPlayer 的 currentTime 值设置为 0.0,当音频正在播放时,这个值用于标识当前播放位置的偏移,不播放音频时标识重新播放音频的起始偏移。

    2.3.4 修改音量、pan值、播放速率和循环

    player.enableRate = YES;
    player.rate = rate;
    player.volume = volume;
    player.pan = pan;
    player.numberOfLoops = -1;
    

    2.4 配置音频会话

    如果希望应用程序播放音频时屏蔽静音切换动作,需要设置会话分类为 AVAudioSessionCategoryPlayback,但是如果希望按下锁屏后还可以播放,就需要在 plist 里加入一个 Required background modes 类型的数组,在其中添加 App plays audio or streams audio/video using AirPlay。

    2.5 处理中断事件

    中断事件是指电话呼入、闹钟响起、弹出 FaceTime 等,中断事件发生时系统会调用 AVAudioPlayer 的 AVAudioPlayerDelegate 类型的 delegate 的下列方法

    - (void)audioPlayerBeginInterruption:(AVAudioPlayer *)player NS_DEPRECATED_IOS(2_2, 8_0);
    - (void)audioPlayerEndInterruption:(AVAudioPlayer *)player withOptions:(NSUInteger)flags NS_DEPRECATED_IOS(6_0, 8_0);
    

    中断结束调用的方法会带入一个 options 参数,如果是 AVAudioSessionInterruptionOptionShouldResume 则表明可以恢复播放音频了。

    2.6 处理线路改变

    在 iOS 设备上添加或移除音频输入、输出线路时会引发线路改变,最佳实践是,插入耳机时播放动作不改动,拔出耳机时应当暂停播放。

    首先需要监听通知

            NSNotificationCenter *nsnc = [NSNotificationCenter defaultCenter];
            [nsnc addObserver:self
                     selector:@selector(handleRouteChange:)
                         name:AVAudioSessionRouteChangeNotification
                       object:[AVAudioSession sharedInstance]];
    

    然后判断是旧设备不可达事件,进一步取出旧设备的描述,判断旧设备是否是耳机,再做暂停播放处理。

    - (void)handleRouteChange:(NSNotification *)notification {
    
        NSDictionary *info = notification.userInfo;
    
        AVAudioSessionRouteChangeReason reason =
            [info[AVAudioSessionRouteChangeReasonKey] unsignedIntValue];
    
        if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
    
            AVAudioSessionRouteDescription *previousRoute =
                info[AVAudioSessionRouteChangePreviousRouteKey];
    
            AVAudioSessionPortDescription *previousOutput = previousRoute.outputs[0];
            NSString *portType = previousOutput.portType;
    
            if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
                [self stop];
                [self.delegate playbackStopped];
            }
        }
    }
    

    这里 AVAudioSessionPortHeadphones 只包含了有线耳机,无线蓝牙耳机需要判断 AVAudioSessionPortBluetoothA2DP 值。

    3. 录制音频

    AVAudioRecorder 用于负责录制音频。

    3.1 创建 AVAudioRecorder

    创建 AVAudioRecorder 需要以下信息

    • 用于写入音频的本地文件 URL
    • 用于配置录音会话键值信息的字典
    • 用于捕捉错误的 NSError
            NSString *tmpDir = NSTemporaryDirectory();
            NSString *filePath = [tmpDir stringByAppendingPathComponent:@"memo.caf"];
            NSURL *fileURL = [NSURL fileURLWithPath:filePath];
    
            NSDictionary *settings = @{
                                       AVFormatIDKey : @(kAudioFormatAppleIMA4),
                                       AVSampleRateKey : @44100.0f,
                                       AVNumberOfChannelsKey : @1,
                                       AVEncoderBitDepthHintKey : @16,
                                       AVEncoderAudioQualityKey : @(AVAudioQualityMedium)
                                       };
    
            NSError *error;
            self.recorder = [[AVAudioRecorder alloc] initWithURL:fileURL settings:settings error:&error];
            if (self.recorder) {
                self.recorder.delegate = self;
                self.recorder.meteringEnabled = YES;
                [self.recorder prepareToRecord];
            } else {
                NSLog(@"Error: %@", [error localizedDescription]);
            }
    

    prepareToRecord 方法执行底层 Audio Queue 初始化必要过程,并在指定位置创建文件。

    3.2 通用设置参数

    • 音频格式

    AVFormatIDKey 键对应写入内容的音频格式,它有以下可选值

    kAudioFormatLinearPCM
    kAudioFormatMPEG4AAC
    kAudioFormatAppleLossless
    kAudioFormatAppleIMA4
    kAudioFormatiLBC
    kAudioFormatULaw
    

    kAudioFormatLinearPCM 会将未压缩的音频流写入文件,文件体积大。kAudioFormatMPEG4AAC 和 kAudioFormatAppleIMA4 的压缩格式会显著缩小文件,并保证高质量音频内容。但是要注意,制定的音频格式与文件类型应该兼容,例如 wav 格式对应 kAudioFormatLinearPCM 值。

    • 采样率

    AVSampleRateKey 指示采样率,即对输入的模拟音频信号每一秒内的采样数。常用值 8000,16000,22050,44100。

    • 通道数

    AVNumberOfChannelsKey 指示定义记录音频内容的通道数,除非使用外部硬件录制,否则通常选择单声道。

    • 编码位元深度

    AVEncoderBitDepthHintKey 指示编码位元深度,从 8 到 32。

    • 音频质量

    AVEncoderAudioQualityKey 指示音频质量,可选值有 AVAudioQualityMin, AVAudioQualityLow, AVAudioQualityMedium, AVAudioQualityHigh, AVAudioQualityMax。

    3.3 实践

    3.3.1 配置音频会话

    录音和播放应用应当使用 AVAudioSessionCategoryPlayAndRecord 分类来配置会话。

        AVAudioSession *session = [AVAudioSession sharedInstance];
    
        NSError *error;
        if (![session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]) {
            NSLog(@"Category Error: %@", [error localizedDescription]);
        }
    
        if (![session setActive:YES error:&error]) {
            NSLog(@"Activation Error: %@", [error localizedDescription]);
        }
    

    注意录音前需要申请麦克风权限。

    3.3.2 录音控制

    对录音过程的控制如下

    [self.recorder record];
    [self.recorder pause];
    [self.recorder stop];
    

    其中选择了 stop 录音即停止,此时 AVAudioRecorder 会调用其遵循 AVAudioRecorderDelegate 协议的代理的 - (void)audioRecorderDidFinishRecording:(AVAudioRecorder *)recorder successfully:(BOOL)flag 方法。

    3.3.3 录音保存

    在初始化 AVAudioRecorder 时指定了临时文件目录作为存储音频的位置,音频录制结束时需要保存到 Document 目录下

        NSTimeInterval timestamp = [NSDate timeIntervalSinceReferenceDate];
        NSString *filename = [NSString stringWithFormat:@"%@-%f.m4a", name, timestamp];
    
        NSString *docsDir = [self documentsDirectory];
        NSString *destPath = [docsDir stringByAppendingPathComponent:filename];
    
        NSURL *srcURL = self.recorder.url;
        NSURL *destURL = [NSURL fileURLWithPath:destPath];
    
        NSError *error;
        BOOL success = [[NSFileManager defaultManager] copyItemAtURL:srcURL toURL:destURL error:&error];
        if (success) {
            handler(YES, [THMemo memoWithTitle:name url:destURL]);
            [self.recorder prepareToRecord];
        } else {
            handler(NO, error);
        }
    

    这里调用了 NSFileManager 的 copyItemAtURL 方法将文件内容拷贝到 Document 目录下。

    3.3.4 展示时间

    记录音频时需要展示时间提示用户当前录制时间,AVAudioRecorder 的 currentTime 属性可以获知当前时间,将其格式化后即可进行展示

    - (NSString *)formattedCurrentTime {
        NSUInteger time = (NSUInteger)self.recorder.currentTime;
        NSInteger hours = (time / 3600);
        NSInteger minutes = (time / 60) % 60;
        NSInteger seconds = time % 60;
    
        NSString *format = @"%02i:%02i:%02i";
        return [NSString stringWithFormat:format, hours, minutes, seconds];
    }
    

    但是需要实时展示时间的话,不能通过 KVO 来解决,只能加入到 NSTimer 中,每 0.5s 执行一次。

        [self.timer invalidate];
        self.timer = [NSTimer timerWithTimeInterval:0.5
                                             target:self
                                           selector:@selector(updateTimeDisplay)
                                           userInfo:nil
                                            repeats:YES];
        [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    

    3.3.5 可视化音频信号

    AVAudioRecorder 和 AVAudioPlayer 都有两个方法获取当前音频的平均分贝和峰值分贝数据。

    - (float)averagePowerForChannel:(NSUInteger)channelNumber; /* returns average power in decibels for a given channel */
    - (float)peakPowerForChannel:(NSUInteger)channelNumber; /* returns peak power in decibels for a given channel */
    

    返回值从 -160dB(静音) 到 0dB(最大分贝)。

    获取值之前要在初始化播放器或记录器时设置 meteringEnabled 为 YES。

    首先需要将 -160 到 0 的分贝值转为 0 到 1 范围内,需要用到下面这个类

    @implementation THMeterTable {
        float _scaleFactor;
        NSMutableArray *_meterTable;
    }
    
    - (id)init {
        self = [super init];
        if (self) {
            float dbResolution = MIN_DB / (TABLE_SIZE - 1);
    
            _meterTable = [NSMutableArray arrayWithCapacity:TABLE_SIZE];
            _scaleFactor = 1.0f / dbResolution;
    
            float minAmp = dbToAmp(MIN_DB);
            float ampRange = 1.0 - minAmp;
            float invAmpRange = 1.0 / ampRange;
    
            for (int i = 0; i < TABLE_SIZE; i++) {
                float decibels = i * dbResolution;
                float amp = dbToAmp(decibels);
                float adjAmp = (amp - minAmp) * invAmpRange;
                _meterTable[i] = @(adjAmp);
            }
        }
        return self;
    }
    
    float dbToAmp(float dB) {
        return powf(10.0f, 0.05f * dB);
    }
    
    - (float)valueForPower:(float)power {
        if (power < MIN_DB) {
            return 0.0f;
        } else if (power >= 0.0f) {
            return 1.0f;
        } else {
            int index = (int) (power * _scaleFactor);
            return [_meterTable[index] floatValue];
        }
    }
    
    @end
    

    接下来可以实时获取到分贝平均值和峰值

    - (THLevelPair *)levels {
        [self.recorder updateMeters];
        float avgPower = [self.recorder averagePowerForChannel:0];
        float peakPower = [self.recorder peakPowerForChannel:0];
        float linearLevel = [self.meterTable valueForPower:avgPower];
        float linearPeak = [self.meterTable valueForPower:peakPower];
        return [THLevelPair levelsWithLevel:linearLevel peakLevel:linearPeak];
    }
    

    可以看到获取峰值和均值前必须调用 updateMeters 方法。

    相关文章

      网友评论

          本文标题:AVFoundation-播放和录制音频

          本文链接:https://www.haomeiwen.com/subject/ncdpkftx.html