iOS基于Speech框架的語音識別波浪動圖實現(xiàn)
作者 | 伍新爽,家庭運營中心
Labs 導讀
App開發(fā)中經(jīng)常會遇到波浪式動畫語音識別轉(zhuǎn)文字的需求,那么實際是如何實現(xiàn)這樣的功能的,本文將從技術框架和視覺實現(xiàn)層面進行Speech框架方案的詳細介紹。
1Speech框架及使用流程
目前App中的語音識別功能主要分為本地識別及網(wǎng)絡在線識別兩種情況。網(wǎng)絡在線識別依賴于平臺對語音的數(shù)據(jù)處理能力,其識別準確度較高,優(yōu)點明顯,缺點是識別的穩(wěn)定性及效率略低;而本地識別方案識別的穩(wěn)定性及效率較高,但識別的準確度不及網(wǎng)絡在線識別方式。本文要介紹的Speech框架屬于語音本地識別的一種成熟框架,適用于對識別精度要求不高,但識別效率較高的場景。
為了便于功能維護和調(diào)用方便,工程中建議對該框架的識別能力進行管理器模塊化封裝,如下可定義一個Speech框架識別能力的管理器:
@interface HYSpeechVoiceDetectManager : NSObject
-(void)isHasVoiceRecodingAuthority:(authorityReturnBlock)hasAuthorityBlock;
+(void)isHasSpeechRecognizerAuthority:(authorityReturnBlock)hasAuthorityBlock;
- (void)setupTimer;
-(void)startTransfer;
-(void)endTransfer;
從以上代碼可知封裝函數(shù)包含的是整個的語音識別流程:1.判定語音及Speech框架的使用權(quán)限(isHasVoiceRecodingAuthority和isHasSpeechRecognizerAuthority) 2.語音識別參數(shù)及相關類初始化(setupTimer) 3.開啟語音識別并實時接受識別后的文字信息(setupTimer和startTransfer) 4.強制銷毀Speech框架數(shù)據(jù)及重置音頻配置數(shù)據(jù)(endTransfer),下文將按上述四步展開講述。
1.1 判定語音及Speech框架的使用權(quán)限
因為識別能成功的首要條件就是已經(jīng)獲取到語音和Speech框架的使用權(quán)限,因此在初始化框架功能時候需要進行權(quán)限的獲取操作,Speech框架的使用權(quán)限獲取代碼參考如下:
-(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框架權(quán)限信息。語音的使用權(quán)限獲取操作,相關代碼參考如下:
-(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可以獲知語音權(quán)限信息,實際使用時候首先應該獲取語音的權(quán)限然后在此基礎上再獲取Sepeech框架權(quán)限信息,只有用戶在擁有兩個權(quán)限的前提下才能進入到下一個“語音識別參數(shù)及相關類初始化”環(huán)節(jié)。
1.2 語音識別參數(shù)及相關類初始化
Sepeech框架初始化前需要建立一個音頻流的輸入通道,AVAudioEngine為這個輸入通道不可或缺的節(jié)點,通過AVAudioEngine可以生成和處理音頻信號,執(zhí)行音頻信號的輸入操作,AVAudioInputNode 為音頻流的拼接通道,該通道可以實時拼接一段一段的語音,以此完成動態(tài)化的音頻流數(shù)據(jù)。AVAudioEngine和AVAudioInputNode 配合使用完成音頻流數(shù)據(jù)獲取的準備工作,AVAudioEngine執(zhí)行方法- (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回調(diào)接口可以獲取到SFSpeechAudioBufferRecognitionRequest完整實時音頻數(shù)據(jù)流信息。
Sepeech框架使用時需要一個關鍵類-錄音器(AVAudioRecorder),該類用來設定語音采集的格式,音頻通道,比特率,數(shù)據(jù)緩存路徑等重要信息并完成語音的采集等功能,只有調(diào)用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];
如上代碼中語音識別初始化過程中的第一行代碼應該首先執(zhí)行endTransfer,該接口主要功能是初始化音頻參數(shù)為默認狀態(tài):強制銷毀音頻流輸入通道,清除語音錄音器,清除音頻識別任務。其中初始化音頻參數(shù)屬于較為重要的一個步驟,其目的是為了防止工程中其它功能模塊對音頻輸入?yún)?shù)等信息修改引起的語音識別異常,下文將會進行相關細節(jié)講述。
1.3 開啟語音識別并實時接受識別后的文字信息
識別轉(zhuǎn)化函數(shù)-(void)startTransfer會一直輸出語音識別轉(zhuǎn)換的文字信息,該能力主要依賴于SFSpeechRecognizer類,我們可以從回調(diào)接口返回的實時參數(shù)SFSpeechRecognitionResult _Nullable result中獲取到識別轉(zhuǎn)化后的文字信息,需要注意的是只有回調(diào)無錯時輸出的文字信息才是有效的,參考代碼如下所示:
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框架中的語音識別器,通過該類即可完成對音頻數(shù)據(jù)進行本地化文字信息轉(zhuǎn)化,其中voiceTextCache即為語音實時轉(zhuǎn)化后的信息。
1.4 強制銷毀Speech框架數(shù)據(jù)及重置音頻配置數(shù)據(jù)
當識別結(jié)束的時候,需調(diào)用-(void)endTransfer方法完成強制關閉音頻流通道,刪除語音緩沖文件,停止語音監(jiān)聽器等工作,其中還需要對音頻模式及參數(shù)進行默認重置處理,相關代碼參考如下:
-(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語音識別波浪效果實現(xiàn)原理
2.1語音識別動畫效果
ios語音識別需求經(jīng)常會要求實時展示語音識別的動畫過程,常見的動畫效果是根據(jù)語音識別音量大小實時展示不同幅度的平移波浪效果,最終效果如下圖所示:
如上最終效果圖中一共有32個波浪圓點,其中前6個和后6個屬于固定靜止的圓點,只有中間20個圓點屬于隨音量大小類似波浪上下浮動的長柱圖。
2.2 語音識別動畫實現(xiàn)原理
對于上節(jié)圖中的動態(tài)效果實際實現(xiàn)方式存在兩種:一、ios系統(tǒng)傳統(tǒng)CoreAnimation框架, 二、動態(tài)更新圓點frame。如果采用傳統(tǒng)CoreAnimation框架那么對于每一個圓點都要做一個浮動效果的動畫,在實現(xiàn)流程和系統(tǒng)開銷兩方面顯得得不償失,而如果采用簡單的動態(tài)更新圓點frame的方式則事半功倍,本文內(nèi)容也將基于動態(tài)更新圓點frame的方式進行講述。
對于上圖中的動畫效果很容易就會聯(lián)想到數(shù)學中的正弦函數(shù)圖,因此可以把波浪圖的橫向定義為正弦X軸(實際上ios的坐標也是基于此概念),縱向即為正弦Y軸(高度為音量對坐標的映射值)。首先對于32個浮動圓點本地初始化對應出32個X軸映射坐標x,坐標間隙等值設定,當實時語音音量voluem數(shù)據(jù)傳輸過來后通過正弦函數(shù)y=F*sin(pi*x-pi*t)可以算出映射的振幅y,其中語音音量限定了最大的音量數(shù)據(jù),該數(shù)據(jù)和正弦函數(shù)振幅數(shù)據(jù)F強相關,相關實現(xiàn)原理圖參考如下:
那么實時語音音量voluem數(shù)據(jù)是如何獲取的?實際上前文中的監(jiān)聽器(AVAudioRecorder)就提供了語音音量數(shù)據(jù)的封裝接口- (float)peakPowerForChannel:(NSUInteger)channelNumber,channelNumber為音頻輸入/輸出的通道號,該函數(shù)返回的數(shù)據(jù)即為分貝數(shù)值,取值的范圍是 -160~0 ,聲音峰值越大,越接近0。當獲取到實時語音音量voluem數(shù)據(jù)后進行相應的量化處理就能得到振幅y的映射值。相關代碼可參考如下:
AVAudioRecorder *monitor
[monitor updateMeters];
float power = [monitor peakPowerForChannel:0];
2.3 動態(tài)更新圓點的frame代碼層面的實現(xiàn)
首先生成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的數(shù)組,該數(shù)組是為了方便重置圓點的frame而設定,offsetX為圓點在圖層中的具體坐標,需要根據(jù)需求進行詳細計算獲得。
將32個圓點添加到對應圖層后核心需要解決的問題就是如何獲取圓點振幅了,因為需求實現(xiàn)的最終效不僅是圓點正弦波浪效果,還需要同時對整個波浪進行右邊平移,因此正弦函數(shù)y=F*sin(pi*x-pi*t)輸入數(shù)據(jù)應該包含圓點坐標x和格式化的時間戳數(shù)據(jù)t,具體實現(xiàn)代碼參考如下:
-(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是根據(jù)視覺映射出的圓點y軸振幅,sinX是根據(jù)入?yún)riginX及圓點x軸坐標間隔值計算得出,timeStamp為音量實時采集時間戳格式化數(shù)據(jù)。
經(jīng)過以上步驟實現(xiàn)就只剩更新圓點frame這一步了,這一步相對簡單,代碼實現(xiàn)參考如下:
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數(shù)據(jù),然后通過計算得到當前時刻圓點的振幅viewHeight,最后用viewHeight去更新當前時刻圓點的frame,此時即完成了某個時刻20個圓點的正弦振動動態(tài)效果圖。
以上為本人工程中的技術實現(xiàn)經(jīng)驗,現(xiàn)做一個簡單的總結(jié)歸納,如有錯誤之處請不吝賜教,謝謝閱讀!
參考資料
[1] Speech框架資料:https://developer.apple.com/documentation/speech
[2] ios錄音功能資料:https://developer.apple.com/documentation/avfaudio