iOS基于Speech框架的語音識別波浪動圖實現
作者 | 伍新爽,家庭運營中心
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