一、前言html
微信爲了解決小商戶老闆們在頻繁交易中不方便覈對、確認到帳的功能痛點,產品MM提出了新版本須要支持收款到帳語音提醒功能。本文藉此總結了iOS平臺上的APP後臺喚醒和語音合成、播放等一系列技術開發過程當中遇到的坑和小技巧,但願與您分享。微信
二、技術方案網絡
2.1 後臺喚醒Appapp
收款到帳語音提醒須要收款方在收到款後,播放一段TTS合成語音播報金額,微信在前臺時能夠經過模板消息將須要播報的金額帶下來,再請求TTS數據並播放,可是app在掛起或者被kill掉的狀況下要如何請求語音數據並播放呢?框架
iOS提供了兩種方式喚醒處於掛起或已經被kill掉的app。分別是Silent Notification和VoIP Push Notification,客戶端在被喚醒以後將得到30s的後臺運行時間,這段運行時間足以請求合成語音數據並播放。ide
具體技術細節以下:oop
1)Silent Notification:Silent Notification在iOS7以上即可以支持,可是每小時能推送的Silent Notification次數有限制;微信支付
2)VoIP Push Notification:VoIP Push Notification則是在iOS8以上才支持的新Push類型,相比於Silent Notification,VoIP Push具備高優先級、低延遲的優點,而且沒有次數限制。ui
對比這兩種技術方案,VoIP Push Notification明顯更適合用於收款到帳語音提醒的喚醒方案。spa
2.2 TTS合成語音
TTS語音合成方案分爲離線合成方案和在線合成方案,離線合成方案省去網絡請求,合成速度更快,節省網絡流量,可是合成音的聽起來比較機械,語速和停頓的處理較差一些。若是對合成音的效果要求不是特別高,能夠考慮採用iOS自帶的AVSpeechSynthesis框架,免去語音庫的合入,減小安裝包大小。
在線合成方案的效果則相對更像人聲,富有感情。考慮到產品體驗,咱們採用了搜索產品部提供的在線語音合成方案,接入方式能夠看這篇文章,合成音格式支持wav、mp三、silk,amr、speex。對比後發現,在合成相同文本的狀況下,amr的壓縮率最高,可是能聽到音質降低明顯。silk格式壓縮率次高,且能保持相對清晰的音質,單條合成語音大小在2KB左右。
2.3 喚醒後播放音頻文件
在請求到合成語音後,要在後臺或者鎖屏狀態下播放音頻文件,AVAudio Session的Category值須要使用AVAudioSessionCategoryPlayback或是AVAudioSessionCategoryPlayAndRecord,CategoryOptions根據實際須要可選擇MixWithOthers(與其餘聲音混音)或是DuckOthers(調低其餘聲音的音量)。
須要注意的是:只有iOS10以上才支持app被喚醒後在後臺/鎖屏狀態下播放音頻。因此iOS10如下的設備,在收到VoIP Push後只能在local push上設定一段固定鈴聲,這也是爲何iOS10如下只有「微信支付收款到帳」,而沒有後面具體的金額數值。
三、靜音開關檢測
不幸的是,在產品發佈後沒多久就受到了某互聯網大佬的吐槽。
從產品體驗上來講,收款到帳的金額播報是隨着local push的彈出一塊兒播放的,更像是一種特殊的push鈴聲,而蘋果對push鈴聲的處理是受到靜音開關控制的,因此講道理,這個吐槽是合理的。然而前面提到App在被VoIP Push喚醒以後,須要將AudioSessionCategory設置爲AVAudioSessionCategoryPlayback或AVAudioSessionCategoryPlayAndRecord才能夠在後臺播放音頻文件,這兩種模式是不受靜音開關控制的。要實現這個需求,就必須獲取當前靜音開關的狀態。而蘋果在iOS5以後並無明確地提供一種方式讓開發獲取靜音開關的狀態,這就陷入了一個尷尬的局面。
蘋果在iOS5以前可使用如下方式監聽靜音鍵開關:
1
2
3
4
5
6
7
8
9
10
11
12
|
- (BOOL)isMuted
{
CFStringRef route;
UInt32 routeSize = sizeof(CFStringRef);
OSStatus status = AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &routeSize, &route);
if
(status == kAudioSessionNoError)
{
if
(route == NULL|| !CFStringGetLength(route))
returnYES;
}
returnNO;
}
|
蘋果在iOS5以後便禁止了使用這種方式監聽靜音按鍵,背後的緣由應該是蘋果但願開發者使用AVAudioSession來提供統一的音頻播放效果。
最後我在Reddit上找到了一種曲線救國的方式,實現起來也不復雜:使用AudioServicesPlaySystemSound播放一段0.2s的空白音頻,並監聽音頻播放完成事件,若是從開始播放到回調完成方法的間隔時間小於0.1s,則意味當前靜音開關爲開啓狀態。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
void
SoundMuteNotificationCompletionProc(SystemSoundID ssID,
void
* clientData){
MMSoundSwitchDetector* detecotr = (__bridge MMSoundSwitchDetector*)clientData;
[detecotr complete];
}
- (instancetype)init {
self= [superinit];
if
(self) {
NSURL*pathURL = [[NSBundlemainBundle] URLForResource:@
"mute"
withExtension:@
"caf"
];
if
(AudioServicesCreateSystemSoundID((__bridge CFURLRef)pathURL, &_soundId) == kAudioServicesNoError){
AudioServicesAddSystemSoundCompletion(self.soundId, CFRunLoopGetMain(), kCFRunLoopDefaultMode, SoundMuteNotificationCompletionProc,(__bridge
void
*)(self));
UInt32 yes =
1
;
AudioServicesSetProperty(kAudioServicesPropertyIsUISound, sizeof(_soundId),&_soundId,sizeof(yes), &yes);
}
else
{
MMErrorWithModule(LOGMODULE, @
"Create Sound Error."
);
_soundId =
0
;
}
} returnself;
}
- (
void
)checkSoundSwitchStatus:(CheckSwitchStatusCompleteBlk)completHandler {
if
(self.soundId ==
0
) {
completHandler(YES);
return
;
}
self.completeHandler = completHandler;
self.beginTime = CACurrentMediaTime();
AudioServicesPlaySystemSound(self.soundId);
}
- (
void
)complete {
CFTimeInterval elapsed = CACurrentMediaTime() - self.beginTime;
BOOLisSwitchOn = elapsed >
0.1
;
if
(self.completeHandler) {
self.completeHandler(isSwitchOn);
}
}
|
四、設置聲音閾值
另一個用戶反饋較多的問題是聽不到播報聲音,經過查看日誌發現是觸發語音播報時,用戶設置的系統音量太小所致使。首先想到的解決方案是直接設置AVAudioPlayer的volume(或者是AudioQueue中的kAudioQueueParam_Volume),然而實驗事後發現這樣行不通,volume屬性受制於系統音量(好比系統volume是0.5,AVAudioPlayer的音量是0.6,則最終的音量爲0.5*0.6 =0.3)。
要解決音量太小的問題,仍是須要經過調節系統音量。最終的解決方案借鑑了進入收付款展現二維碼時自動調節屏幕亮度的方案:若是屏幕亮度未達到閾值,則調高屏幕亮度到閾值,離開頁面時,將亮度設回原亮度。同理,播放提示音時,若用戶設置的系統音量小於閾值,則調節到閾值。提示音播放完畢後,將提示音調回原音量。
控制系統音量有如下兩種方式。
4.1 方式一:經過MPMusicPlayerController設置音量
1
2
|
//This property is deprecated -- use MPVolumeView for volume control instead.mpc.volume = 0; //0.0~1.0
MPMusicPlayerController *mpc = [MPMusicPlayerController applicationMusicPlayer];
|
第一種方式簡單粗暴,在設置的時候會彈出系統音量提示框,若是用戶在使用app的過程忽然彈出音量框,會對用戶形成困擾,不建議使用這種方式,而且蘋果在iOS7.0之後已將該屬性標爲deprecated。
4.2 方式二:經過MPVolumeView設置音量
第二種方式則是將一個看不見的MPVolumeView添加到當前視圖上,系統音量提示框就不會顯示了。
須要注意的是:在調節完系統音量須要將MPVolumeView移除,不然後續用戶手動調節音量會出現系統音量提示框不顯示的狀況。
調節音量的方式,則是先取到MPVolumeView中名爲MPVolumeSlider的子View,並對其發送模擬用戶操做的事件。
1
2
3
4
5
6
7
8
9
10
11
12
|
- (
void
)setSystemVolume:(float)volume {
UISlider* volumeViewSlider = nil;
for
(UIView *view
in
[self.m_privateVoulmeView subviews]){
if
([view.
class
.description isEqualToString:@
"MPVolumeSlider"
]){
volumeViewSlider = (UISlider*)view;
break
;
}
}
if
(volumeViewSlider != nil) {
[volumeViewSlider setValue:volume animated:NO];
//經過send
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
|