前一段时间,一个“支付宝到账100万”的铃声在网络上火了起来,其实这在APP上,特别支付类的应用里,经常用到,今天我们谈一下其实现方法,给类似这种场景的开发人员一个参考吧。
首先,我们这次是基于推送+语音的方式来实现。
使用sound字段
我们都知道,我们可以在进行推送的时候,指定sound的文件名,来播放指定声音文件。
于是,录好一个声音文件,暂且叫“tts_default.mp3”吧,加入到主工程中。
服务端收到一笔款项的时候,往消息中心发起一个推送,推送的格式和内容如下:
{"aps":{"alert":"XXX到账一笔","badge":1,"sound":"tts_default.mp3"}}
这样,APP在接受到通知的时候,弹出一个通知框,显示“XXX到账一笔”,并伴随一个声音,播放的是语音文件tts_default.mp3。
播报金额
如果收到一笔钱,如果能播放具体金额就更好了,因为金额是变化的,你不可能在工程里添加许多“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"}}
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