iOS 开发之使用 Speex 格式实现简单的语音聊天功能(二)

秦鸿羽
2023-12-01

今天我们继续上一篇博客“IOS开发之使用Speex格式实现简单的语音聊天功能(一)”继续往下讲,主要是讲述一下PlayManager与RecorderManager两个类的功能。

首先要讲的是RecorderManager,该类的主要功能就是负责对用户的语音进行录制,和停止录制。

 

#import <Foundation/Foundation.h>
#import "Encapsulator.h"

@protocol RecordingDelegate <NSObject>

- (void)recordingFinishedWithFileName:(NSString *)filePath time:(NSTimeInterval)interval;
- (void)recordingTimeout;
- (void)recordingStopped;  //录音机停止采集声音
- (void)recordingFailed:(NSString *)failureInfoString;

@optional
- (void)levelMeterChanged:(float)levelMeter;

@end

@interface RecorderManager : NSObject <EncapsulatingDelegate> {
    
    Encapsulator *encapsulator;
    NSString *filename;
    NSDate *dateStartRecording;
    NSDate *dateStopRecording;
    NSTimer *timerLevelMeter;
    NSTimer *timerTimeout;
}

@property (nonatomic, weak)  id<RecordingDelegate> delegate;
@property (nonatomic, strong) Encapsulator *encapsulator;
@property (nonatomic, strong) NSDate *dateStartRecording, *dateStopRecording;
@property (nonatomic, strong) NSTimer *timerLevelMeter;
@property (nonatomic, strong) NSTimer *timerTimeout;

+ (RecorderManager *)sharedManager;

- (void)startRecording;

- (void)stopRecording;

- (void)cancelRecording;

- (NSTimeInterval)recordedTimeInterval;

@end

 

 

 

该类的主要结构就是上面头文件中所展示的,包括一个委托RecordingDelegate,以及RecorderManager本身的类函数。该类使用了单例模式,通过调用函数shareManager即可获取它实例本身。

startRecording函数负责录音工作:

 

- (void)startRecording {
    if ( ! mAQRecorder) {
        
        mAQRecorder = new AQRecorder();
        
        OSStatus error = AudioSessionInitialize(NULL, NULL, interruptionListener, (__bridge void *)self);
        if (error) printf("ERROR INITIALIZING AUDIO SESSION! %d\n", (int)error);
        else
        {
            UInt32 category = kAudioSessionCategory_PlayAndRecord;
            error = AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(category), &category);
            if (error) printf("couldn't set audio category!");
            
            //添加属性监听,一旦有属性改变则调用其中的propListener函数
            error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener, (__bridge void *)self);
            if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", (int)error);
            UInt32 inputAvailable = 0;
            UInt32 size = sizeof(inputAvailable);
            
            // we do not want to allow recording if input is not available
            error = AudioSessionGetProperty(kAudioSessionProperty_AudioInputAvailable, &size, &inputAvailable);
            if (error) printf("ERROR GETTING INPUT AVAILABILITY! %d\n", (int)error);
            
            // we also need to listen to see if input availability changes
            error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioInputAvailable, propListener, (__bridge void *)self);
            if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", (int)error);
            
            error = AudioSessionSetActive(true); 
            if (error) printf("AudioSessionSetActive (true) failed");
        }
        
    }
    
    //获取音频存放地址
    filename = [NSString stringWithString:[Encapsulator defaultFileName]];
    NSLog(@"filename:%@",filename);
    
    if ( ! self.encapsulator) {
        self.encapsulator = [[Encapsulator alloc] initWithFileName:filename];
        self.encapsulator.delegete = self;
    }
    else {
        [self.encapsulator resetWithFileName:filename];
    }
    
    if ( ! mAQRecorder->IsRunning()) {
        NSLog(@"audio session category : %@", [[AVAudioSession sharedInstance] category]);
        Boolean recordingWillBegin = mAQRecorder->StartRecord(encapsulator);
        if ( ! recordingWillBegin) {
            if ([self.delegate respondsToSelector:@selector(recordingFailed:)]) {
                [self.delegate recordingFailed:@"程序错误,无法继续录音,请重启程序试试"];
            }
            return;
        }
    }

    self.dateStartRecording = [NSDate date];
    
    if (!levelMeterStates)
    {
        levelMeterStates = (AudioQueueLevelMeterState *)malloc(sizeof(AudioQueueLevelMeterState) * 1);
    }
    self.timerLevelMeter = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(updateLevelMeter:) userInfo:nil repeats:YES];
    self.timerTimeout = [NSTimer scheduledTimerWithTimeInterval:60 target:self selector:@selector(timeoutCheck:) userInfo:nil repeats:NO];
}

 

 

 

在startRecording函数中,先实例化类AQRecorder,该类是用C++实现的,主要用作录制音频文件(核心类)。随后调用函数AudioSessionInitialize初始化音频,并添加回调函数interruptionListener,若监听被打断则停止AQRecorder类的录制工作。若返回值不是error则对音频添加相应的属性,并且回调函数为propListener,若监听的属性不正确,就停止录音。当然了,所有的音频都是以文件为单位的,在startRecording函数中利用[EncapsulatordefaultFileName]来获取音频的存放地址,Encapsulator类也是一个很重要的类,它封装了ogg,极大的方便了我们调用它里面的函数来实现录音。

 

函数- (void)stopRecording的作用大家想必都知道就是停止录音,但是不取消音频;

 

函数- (void)cancelRecording的作用则是停止录音并且取消;

函数recordedTimeInterval则是获取录音时间,单位为float;

 

 

接下来,我们来说一下PlayerManager这个类,该类主要负责解析音频文件并播放音频。类结构如下:

 

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import "Decapsulator.h"

@protocol PlayingDelegate <NSObject>

- (void)playingStoped;

@end

@interface PlayerManager : NSObject <DecapsulatingDelegate, AVAudioPlayerDelegate> {
    Decapsulator *decapsulator;
    AVAudioPlayer *avAudioPlayer;
    
}
@property (nonatomic, strong) Decapsulator *decapsulator;
@property (nonatomic, strong) AVAudioPlayer *avAudioPlayer;
@property (nonatomic, weak)  id<PlayingDelegate> delegate;

+ (PlayerManager *)sharedManager;

- (void)playAudioWithFileName:(NSString *)filename delegate:(id<PlayingDelegate>)newDelegate;
- (void)stopPlaying;

@end


可以看到该类与RecordingManager一样,也使用了单例并且其中有委托对象PlayingDelegate,函数playingStoped用于暂停播放。

 

该类有两个主要函数"playAudioWithFileName"与“stopPlaying”前者可以根据音频的文件名字来播放音频,后者则是停止播放。

 

- (void)playAudioWithFileName:(NSString *)filename delegate:(id<PlayingDelegate>)newDelegate {
    if ( ! filename) {
        return;
    }
    if ([filename rangeOfString:@".spx"].location != NSNotFound) {
        [[AVAudioSession sharedInstance] setActive:YES error:nil];
        
        [self stopPlaying];
        self.delegate = newDelegate;
        
        self.decapsulator = [[Decapsulator alloc] initWithFileName:filename];
        self.decapsulator.delegate = self;
        [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
        [self.decapsulator play];
        //开启距离监听
        [self startProximityMonitering];
    }
    else if ([filename rangeOfString:@".mp3"].location != NSNotFound) {
        if ( ! [[NSFileManager defaultManager] fileExistsAtPath:filename]) {
            NSLog(@"要播放的文件不存在:%@", filename);
            [self.delegate playingStoped];
            [newDelegate playingStoped];
            return;
        }
        [self.delegate playingStoped];
        self.delegate = newDelegate;
        
        NSError *error;
        self.avAudioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL URLWithString:filename] error:&error];
        if (self.avAudioPlayer) {
            [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
            [[AVAudioSession sharedInstance] setActive:YES error:nil];
            self.avAudioPlayer.delegate = self;
            [self.avAudioPlayer play];
            [self startProximityMonitering];
        }
        else {
            [self.delegate playingStoped];
        }
    }
    else {
        [self.delegate playingStoped];
    }
}


该函数首先判断音频文件是.spx格式的还是.mp3格式,随后通过解析类Decapsulator来解析音频并播放之(与Encapsulator类对应)。再类中还实现了距离监听的功能,当用户脸靠近手机时则会按掉屏幕打开听他,当远离时则打开扬声器屏幕变亮。

 

 

总而言之,对好奇想尝试一下开发ios语音的人来说,这几个类绝对是个福音,封装的很好,调用起来也很方便。若想用作开发语音聊天的产品,则需要我们要加深理解他背后压缩与解压缩的的原理了。

 

 

 

 

 

 

 

 类似资料: