關於APP上語音播報的完整實現(iOS篇)
前一段時間,一個“支付寶到賬100萬”的鈴聲在網路上火了起來,其實這在APP上,特別支付類的應用裡,經常用到,今天我們談一下其實現方法,給類似這種場景的開發人員一個參考吧。
首先,我們這次是基於推送+語音的方式來實現。
使用sound欄位
我們都知道,我們可以在進行推送的時候,指定sound的檔名,來播放指定聲音檔案。
於是,錄好一個聲音檔案,暫且叫“tts_default.mp3”吧,加入到主工程中。
服務端收到一筆款項的時候,往訊息中心發起一個推送,推送的格式和內容如下:
{"aps":{"alert":"XXX到賬一筆","badge":1,"sound":"tts_default.mp3"}}
這樣,APP
播報金額
如果收到一筆錢,如果能播放具體金額就更好了,因為金額是變化的,你不可能在工程裡新增許多“tts_default.mp3”檔案,那我們只有合成金額,在AVFoundation裡,有合成聲音的API,在第三方,也有如百度、訊飛一樣的第三方合成聲音的介面,我們測試一下,還是比較生硬。這裡我們仿地鐵、車站的廣播,錄了一些基礎的聲音、和一些數字,我們自己來合成所需的聲音。
假如,你要實現的語音格式是這樣的:錢到啦到賬xx.xx元。
我們錄製並預置了一些語音檔案打在包裡,這些檔案包括:
tts_pre.mp3 對應文字為:錢到啦到賬
tts.yuan.mp3 對應文字為:元。
另外還有一些表表示數字的,如0、1、2、3、4、5、6、7、8、9、十、百、千、萬、點
對應的聲音檔案為:
tts_0.mp3 ~ tts_9.mp3、tts_ten.mp3、tts_hundred.mp3、tts_thousand.mp3、tts_ten_thousand.mp3、tts_dot.mp3
當我們想播放聲音“錢到啦到賬0.25元”的時候,我們就可以依次播放聲音檔案:tts_pre.mp3、tts_0.mp3、tts_dot.mp3、tts_2.mp3、tts_5.mp3、tts.yuan.mp3
就可以了。
這裡牽扯到一個金額轉語音檔案的演算法,後面的Demo有實現,可以參考一下:
-(NSString *)wordsStringFromAmount:(NSString *)numstr;
流程是這樣的:
1、後端收到錢,給商家發起一個推送,格式為:
{"aps":{"alert":"錢到啦到賬0.25元","badge":1,"amount":0.25, "sound":"tts_default.mp3"}}
2、客戶端收到推送,處理金額欄位amount,轉成對應的播放檔案陣列。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
[[BPAudioManager sharedPlayer] playPushInfo:userInfo completed:nil] ;
}
3、開始播放聲音檔案。
後臺播放
當APP在前臺的時候,上面那種處理方法是沒有問題的,在後臺的時候,只會播放一個“tts_default.mp3”這個通用型的語音檔案,也沒有問題的,但是在後臺和APP退出的情況下,playPushInfo這個方法執行一些處理,並播放語音是不可行的,所以還藉助其他的方法,好在蘋果在iOS10的時候,釋出了UNNotificationServiceExtension擴充套件,關於此擴充套件,可以網上選擇一些資料,主要的核心思想就是,在遠端推送到底裝置之前,給你一個修改的機會,我們知道,推送體是有限制的,而且推送體大小也會影響推送的效率,藉助這個,我們可以修改標題、內容,也可以從網路上請求到內容,再去合成一個新的推送。我們這裡不修改內容,主要是用來播放語音。
要使用這個擴充套件,和其他擴充套件一樣,新建一個target,找到這個模版,然後下一步,就好了。
系統會自動實現兩個方法:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler ;
- (void)serviceExtensionTimeWillExpire;
前者,你需要在這裡做一些操作,修改內容,當你完成後,通知系統,這時候,推送才會顯示出來。我們這裡主要處理推送,並播放聲音;後者會在超時的情況下呼叫。如:
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
//step1: 標記該推送已經在這裡處理過了
NSMutableDictionary *dict = [self.bestAttemptContent.userInfo mutableCopy] ;
[dict setObject:[NSNumber numberWithBool:YES] forKey:@"hasHandled"] ;
self.bestAttemptContent.userInfo = dict ;
//step2: 忽略推送中的預設語音檔案(有可能是那個recieved.mp3)
self.bestAttemptContent.sound = [UNNotificationSound defaultSound] ;
//step3: 處理推送資訊,播放語音
[[BPAudioManager sharedPlayer] playPushInfo:self.bestAttemptContent.userInfo completed:^{
// 播放完成後,通知系統
self.contentHandler(self.bestAttemptContent);
}] ;
}
- (void)serviceExtensionTimeWillExpire {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
self.contentHandler(self.bestAttemptContent);
}
要啟用UNNotificationServiceExtension擴充套件,需要在欄位中新增mutable-content欄位,所以新的推送體為:
{"aps":{"alert":"錢到啦到賬0.25元","badge":1,"mutable-content":1,"amount":0.25, "sound":"tts_default.mp3"}}
BPAudioManager
我們定義了一個聲音處理的中間類,因為擴充套件和APP本身都會使用這個類,所以新建這個檔案的時候,注意勾選Targets
- (void) playPushInfo:(NSDictionary *)userInfo completed:(BPAudioPlayCompleted)completed {
//獲取aps
NSDictionary *aps = [userInfo objectForKey:@"aps"] ;
//判斷是否需要播報語音,因為所有的推送,都會走到這裡
BOOL playaudio = [[aps objectForKey:@"playaudio"] boolValue] ;
if(!playaudio) {
if(completed != nil) {
completed() ;
}
}
// 處理
else {
self.completed = completed ;
NSString *amount = [aps objectForKey:@"amount"] ;
NSArray* arrAudioFiles = [self getAudioFilesWithAmount:amount] ;
[self playAudioFiles:arrAudioFiles] ;
}
}
先處理金額,得到語音檔案的陣列,播放語音這裡直接用迴圈播放的方式了
// 播放聲音檔案
- (void) playAudioFiles {
// 1.獲取要播放音訊檔案的URL
NSString *fileName = [audioFiles objectAtIndex:audioIndex] ;
NSString *path = [NSString stringWithFormat:@"%@/%@",[[NSBundle mainBundle] resourcePath], fileName] ;
NSURL *fileURL = [NSURL fileURLWithPath:path];
// 2.建立 AVAudioPlayer 物件
self.audioPlayer = [[AVAudioPlayer alloc]initWithContentsOfURL:fileURL error:nil];
// 4.設定迴圈播放
self.audioPlayer.numberOfLoops = 0 ;
self.audioPlayer.delegate = self;
// 5.開始播放
[self.audioPlayer prepareToPlay] ;
[self.audioPlayer play];
}
// 播放完成回撥
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
audioIndex += 1 ;
if(audioIndex < audioFiles.count) {
[self performSelectorOnMainThread:@selector(playAudioFiles) withObject:nil waitUntilDone:NO] ;
}
else {
[self setNormalVolume] ;
[self disactivePlayback] ;
[self performSelectorOnMainThread:@selector(playCompleted) withObject:nil waitUntilDone:NO] ;
}
}
到這裡,基本就完成了,在後臺、退出後臺的情況下,可以正常語音播報了。
音量調節
有時候,我們不小心把聲音關閉了,或者音量很小,或者靜音模式下,那這個時候,播放的聲音就可能聽不見了,為了防止這個情況發生,我們在播放的時候,適當處理一下,是非要有必要的。
// 設定高音量
- (void) setHighVolume {
MPVolumeView*volumeView = [[MPVolumeViewalloc] init];
UISlider*volumeViewSlider = nil;
for(UIView*view in[volumeView subviews]){
if([view.class.descriptionisEqualToString:@"MPVolumeSlider"]){
volumeViewSlider = (UISlider*)view;
break;
}
}
// 獲取系統原來的音量,用於還原
userVolume= volumeViewSlider.value;
// 留點餘地,設定0.9吧, 值在0.0~1.0之間
if(userVolume< 0.9f) {
// 改變系統音量
[volumeViewSlider setValue:0.9fanimated:NO];
// 發一個事件使之生效
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
然後播放完成的時候,會設定會正常音量
// 設定回正常音量
- (void) setNormalVolume {
MPVolumeView*volumeView = [[MPVolumeViewalloc] init];
UISlider* volumeViewSlider = nil;
for(UIView*view in[volumeView subviews]){
if([view.class.descriptionisEqualToString:@"MPVolumeSlider"]){
volumeViewSlider = (UISlider*)view;
break;
}
}
if(volumeViewSlider.value!=userVolume) {
[volumeViewSlider setValue:userVolumeanimated:NO];
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
然後靜音處理:
// 靜音模式下,依然可以播放
- (void) activePlayback {
[[AVAudioSessionsharedInstance] setCategory:AVAudioSessionCategoryPlaybackerror:NULL];
[[AVAudioSessionsharedInstance] setActive:YESerror:NULL];
}
//迴歸正常
- (void)disactivePlayback {
[[AVAudioSessionsharedInstance] setActive:NOerror:NULL];
}
至此,語音播報算是完成了。
1、在iOS10以下,推送利用sound欄位,前臺可以正常播放,後臺、退出的情況下,播放通用聲音。
2、iOS以上,推送增加mutable-content欄位,可以完美播放。
3、我們增加了一些機制,在低音和靜音模式下,也可以正常工作。
附演示Demo
https://github.com/WinterXIE/PushAudio