作者 | 伍新爽,家庭运营中心
Labs 导读
App开发中经常会遇到波浪式动画语音识别转文字的需求,那么实际是如何实现这样的功能的,本文将从技术框架和视觉实现层面进行Speech框架方案的详细介绍。
1Speech框架及使用流程
目前App中的语音识别功能主要分为本地识别及网络在线识别两种情况。网络在线识别依赖于平台对语音的数据处理能力,其识别准确度较高,优点明显,缺点是识别的稳定性及效率略低;而本地识别方案识别的稳定性及效率较高,但识别的准确度不及网络在线识别方式。本文要介绍的Speech框架属于语音本地识别的一种成熟框架,适用于对识别精度要求不高,但识别效率较高的场景。
为了便于功能维护和调用方便,工程中建议对该框架的识别能力进行管理器模块化封装,如下可定义一个Speech框架识别能力的管理器:
@interface HYSpeechVoiceDetectManager : NSObject
-(void)isHasVoiceRecodingAuthority:(authorityReturnBlock)hasAuthorityBlock;
+(void)isHasSpeechRecognizerAuthority:(authorityReturnBlock)hasAuthorityBlock;
- (void)setupTimer;
-(void)startTransfer;
-(void)endTransfer;
从以上代码可知封装函数包含的是整个的语音识别流程:1.判定语音及Speech框架的使用权限(isHasVoiceRecodingAuthority和isHasSpeechRecognizerAuthority) 2.语音识别参数及相关类初始化(setupTimer) 3.开启语音识别并实时接受识别后的文字信息(setupTimer和startTransfer) 4.强制销毁Speech框架数据及重置音频配置数据(endTransfer),下文将按上述四步展开讲述。
1.1 判定语音及Speech框架的使用权限
因为识别能成功的首要条件就是已经获取到语音和Speech框架的使用权限,因此在初始化框架功能时候需要进行权限的获取操作,Speech框架的使用权限获取代码参考如下:
-(void)isHasSpeechRecognizerAuthority{
if (@available(iOS 10.0, *)) {
SFSpeechRecognizerAuthorizationStatus authorizationStatus = [SFSpeechRecognizer authorizationStatus];
if (authorizationStatus == SFSpeechRecognizerAuthorizationStatusNotDetermined) {
[SFSpeechRecognizer requestAuthorization:^(SFSpeechRecognizerAuthorizationStatus status) {
}];
}else if(authorizationStatus == SFSpeechRecognizerAuthorizationStatusDenied || authorizationStatus == SFSpeechRecognizerAuthorizationStatusRestricted) {
} else{
}
}else{
}
}
以上代码中获取到SFSpeechRecognizerAuthorizationStatus后就能获知Sepeech框架权限信息。语音的使用权限获取操作,相关代码参考如下:
-(void)isHasVoiceRecodingAuthority:(authorityReturnBlock)hasAuthorityBlock{
if ([[[UIDevice currentDevice]systemVersion]floatValue] >= 7.0) {
AVAuthorizationStatus videoAuthStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
if (videoAuthStatus == AVAuthorizationStatusNotDetermined) {
} else if(videoAuthStatus == AVAuthorizationStatusRestricted || videoAuthStatus == AVAuthorizationStatusDenied) {
} else{
}
}
}
通过如上代码中的videoAuthStatus可以获知语音权限信息,实际使用时候首先应该获取语音的权限然后在此基础上再获取Sepeech框架权限信息,只有用户在拥有两个权限的前提下才能进入到下一个“语音识别参数及相关类初始化”环节。
1.2 语音识别参数及相关类初始化
Sepeech框架初始化前需要建立一个音频流的输入通道,AVAudioEngine为这个输入通道不可或缺的节点,通过AVAudioEngine可以生成和处理音频信号,执行音频信号的输入操作,AVAudioInputNode 为音频流的拼接通道,该通道可以实时拼接一段一段的语音,以此完成动态化的音频流数据。AVAudioEngine和AVAudioInputNode 配合使用完成音频流数据获取的准备工作,AVAudioEngine执行方法- (void)prepare后初始化工作才能生效,相关代码如下所示:
AVAudioEngine *bufferEngine = [[AVAudioEngine alloc]init];
AVAudioInputNode *buffeInputNode = [bufferEngine inputNode];
SFSpeechAudioBufferRecognitionRequest *bufferRequest = [[SFSpeechAudioBufferRecognitionRequest alloc]init];
AVAudioFormat *format =[buffeInputNode outputFormatForBus:0];
[buffeInputNode installTapOnBus:0 bufferSize:1024 format:format block:^(AVAudioPCMBuffer * _Nonnull buffer, AVAudioTime * _Nonnull when) {
[bufferRequest appendAudioPCMBuffer:buffer];
}];
[bufferEngine prepare];
如上代码中通过buffeInputNode回调接口可以获取到SFSpeechAudioBufferRecognitionRequest完整实时音频数据流信息。
Sepeech框架使用时需要一个关键类-录音器(AVAudioRecorder),该类用来设定语音采集的格式,音频通道,比特率,数据缓存路径等重要信息并完成语音的采集等功能,只有调用AVAudioRecorder的- (BOOL)record方法,语音识别功能才能正常开始,参考如下代码:
[self endTransfer];
NSDictionary *recordSettings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithFloat: 14400.0], AVSampleRateKey,
[NSNumber numberWithInt: kAudioFormatAppleIMA4], AVFormatIDKey,
[NSNumber numberWithInt: 2], AVNumberOfChannelsKey,
[NSNumber numberWithInt: AVAudioQualityMax], AVEncoderAudioQualityKey,
nil];
NSString *monitorPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"monitor.caf"];
NSURL *monitorURL = [NSURL fileURLWithPath:monitorPath];
AVAudioRecorder *monitor = [[AVAudioRecorder alloc] initWithURL:_monitorURL settings:recordSettings error:NULL];
monitor.meteringEnabled = YES;
[monitor record];
如上代码中语音识别初始化过程中的第一行代码应该首先执行endTransfer,该接口主要功能是初始化音频参数为默认状态:强制销毁音频流输入通道,清除语音录音器,清除音频识别任务。其中初始化音频参数属于较为重要的一个步骤,其目的是为了防止工程中其它功能模块对音频输入参数等信息修改引起的语音识别异常,下文将会进行相关细节讲述。
1.3 开启语音识别并实时接受识别后的文字信息
识别转化函数-(void)startTransfer会一直输出语音识别转换的文字信息,该能力主要依赖于SFSpeechRecognizer类,我们可以从回调接口返回的实时参数SFSpeechRecognitionResult _Nullable result中获取到识别转化后的文字信息,需要注意的是只有回调无错时输出的文字信息才是有效的,参考代码如下所示:
SFSpeechAudioBufferRecognitionRequest *bufferRequest = [[SFSpeechAudioBufferRecognitionRequest alloc]init];
SFSpeechRecognizer *bufferRec = [[SFSpeechRecognizer alloc]initWithLocale:[NSLocale localeWithLocaleIdentifier:@"zh_CN"]];
[bufferRec recognitionTaskWithRequest:bufferRequest resultHandler:^(SFSpeechRecognitionResult * _Nullable result, NSError * _Nullable error) {
if (error == nil) {
NSString *voiceTextCache = result.bestTranscription.formattedString;
}else{
}
}];
如上代码中的bufferRec为Sepeech框架中的语音识别器,通过该类即可完成对音频数据进行本地化文字信息转化,其中voiceTextCache即为语音实时转化后的信息。
1.4 强制销毁Speech框架数据及重置音频配置数据
当识别结束的时候,需调用-(void)endTransfer方法完成强制关闭音频流通道,删除语音缓冲文件,停止语音监听器等工作,其中还需要对音频模式及参数进行默认重置处理,相关代码参考如下:
-(void)endTransfer{
[bufferEngine stop];
[buffeInputNode removeTapOnBus:0];
bufferRequest = nil;
bufferTask = nil;
[monitor stop];
[monitor deleteRecording];
NSError *error = nil;
[[AVAudioSession sharedInstance] setActive:NO error:&error];
if (error != nil) {
return;
}
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
if (error != nil) {
return;
}
[[AVAudioSession sharedInstance] setMode:AVAudioSessionModeDefault error:&error];
if (error != nil) {
return;
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
if (error != nil) {
return;
}
}
2语音识别波浪效果实现原理
2.1语音识别动画效果
ios语音识别需求经常会要求实时展示语音识别的动画过程,常见的动画效果是根据语音识别音量大小实时展示不同幅度的平移波浪效果,最终效果如下图所示:
如上最终效果图中一共有32个波浪圆点,其中前6个和后6个属于固定静止的圆点,只有中间20个圆点属于随音量大小类似波浪上下浮动的长柱图。
2.2 语音识别动画实现原理
对于上节图中的动态效果实际实现方式存在两种:一、ios系统传统CoreAnimation框架, 二、动态更新圆点frame。如果采用传统CoreAnimation框架那么对于每一个圆点都要做一个浮动效果的动画,在实现流程和系统开销两方面显得得不偿失,而如果采用简单的动态更新圆点frame的方式则事半功倍,本文内容也将基于动态更新圆点frame的方式进行讲述。
对于上图中的动画效果很容易就会联想到数学中的正弦函数图,因此可以把波浪图的横向定义为正弦X轴(实际上ios的坐标也是基于此概念),纵向即为正弦Y轴(高度为音量对坐标的映射值)。首先对于32个浮动圆点本地初始化对应出32个X轴映射坐标x,坐标间隙等值设定,当实时语音音量voluem数据传输过来后通过正弦函数y=F*sin(pi*x-pi*t)可以算出映射的振幅y,其中语音音量限定了最大的音量数据,该数据和正弦函数振幅数据F强相关,相关实现原理图参考如下:
那么实时语音音量voluem数据是如何获取的?实际上前文中的监听器(AVAudioRecorder)就提供了语音音量数据的封装接口- (float)peakPowerForChannel:(NSUInteger)channelNumber,channelNumber为音频输入/输出的通道号,该函数返回的数据即为分贝数值,取值的范围是 -160~0 ,声音峰值越大,越接近0。当获取到实时语音音量voluem数据后进行相应的量化处理就能得到振幅y的映射值。相关代码可参考如下:
AVAudioRecorder *monitor
[monitor updateMeters];
float power = [monitor peakPowerForChannel:0];
2.3 动态更新圆点的frame代码层面的实现
首先生成32个的圆点view,同时添加到当前图层,相关代码如下所示:
self.sinXViews = [NSMutableArray new];
for (NSUInteger i = 0; i < sinaXNum+12; i++) {
UIView *sinView = [[UIView alloc]initWithFrame:CGRectMake(offsetX, offsetY, 3, 3)];
sinView.layer.cornerRadius = 1.5f;
sinView.layer.masksToBounds = YES;
[self.sinXViews addObject:sinView];
[self addSubview:sinView];
}
其中代码中的sinXViews是缓存32个圆点view的数组,该数组是为了方便重置圆点的frame而设定,offsetX为圆点在图层中的具体坐标,需要根据需求进行详细计算获得。
将32个圆点添加到对应图层后核心需要解决的问题就是如何获取圆点振幅了,因为需求实现的最终效不仅是圆点正弦波浪效果,还需要同时对整个波浪进行右边平移,因此正弦函数y=F*sin(pi*x-pi*t)输入数据应该包含圆点坐标x和格式化的时间戳数据t,具体实现代码参考如下:
-(double)fSinValueWithOriginX:(CGFloat)originX timeS:(NSTimeInterval)timeStamp voluem:(CGFloat)voluem{
CGFloat sinF ;
double sinX ;
double fSin = sinF *(4/(pow(sinX,4)+4))*sin(3.14*sinX-3.14*timeStamp);
return fabs(fSin);
}
其中代码中的sinF是根据视觉映射出的圆点y轴振幅,sinX是根据入参originX及圆点x轴坐标间隔值计算得出,timeStamp为音量实时采集时间戳格式化数据。
经过以上步骤实现就只剩更新圆点frame这一步了,这一步相对简单,代码实现参考如下:
for (NSUInteger i = 0; i < self.sinXViews.count; i++) {
if (i > 5 && i < self.sinaXNum+6) {
UIView *sinView = (UIView *)self.sinXViews[i];
CGRect frame = sinView.frame;
double _viewHeight = [self fSinValueWithOriginX:frame.origin.x timeS:timeStamp voluem:Voluem];
double viewHeight ;
frame.size.height = viewHeight;
if (viewHeight == 0) {
return;
}
frame.origin.y = (self.frame.size.height-viewHeight)/2;
[sinView setFrame:frame];
}
}
从以上代码可知遍历self.sinXViews获取到当前圆点view后先取出frame数据,然后通过计算得到当前时刻圆点的振幅viewHeight,最后用viewHeight去更新当前时刻圆点的frame,此时即完成了某个时刻20个圆点的正弦振动动态效果图。
以上为本人工程中的技术实现经验,现做一个简单的总结归纳,如有错误之处请不吝赐教,谢谢阅读!
参考资料
[1] Speech框架资料:https://developer.apple.com/documentation/speech
[2] ios录音功能资料:https://developer.apple.com/documentation/avfaudio