iOS 12.1语音播报的回顾
前言
首先介绍下自己接手的APP的语音播报的实现方法,appdelegate里用了讯飞IFlySpeechSynthesizer的语音播报,负责app在前台时的播报。然后在拓展NotificationService里用系统的AVSpeechSynthesizer播报,实现了在后台的语音播报。如果不能理解的话可以看看这篇文章和那篇文章。
等iOS12.1版本出来之后,发现语音播报在后台那一块失效了。查明详情,苹果官方的大概意思是不允许开发者在拓展NotificationService里合成语音和播报语音了,所以现在需要另找别的方法。经过半个多月的努力,终于还是寻到了几种各有优缺的解决方案。这里提早放出一个帮助我很多的大佬的文章。
方案一:本地存大量完整音频文件
顾名思义,苹果虽然不允许在拓展里合成和播报语音,但是在拓展里播报存在本地的录音,是可以实现的,所以,思路来了。提早生成好类似“支付宝到账100元”之类的音频文件,并设置好对应的名称,拖入主工程里,然后
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
self.bestAttemptContent.sound = [UNNotificationSound soundNamed:@"支付宝到账100元.mp3"];
self.contentHandler(self.bestAttemptContent);
}
如上所示,就这样直接调用音频文件就可以了,一般录音那边的金额设置成0.1-1000元共一万个音频文件就够用了,超过1000的播报到账一笔。支付宝之前有一个版本就是这么做的。这么做的好处显而易见,简单粗暴,但是问题就是app的包会变得非常大。那么该如何生成语音文件,你可以用讯飞,也可以用百度语音生成。这里送上大佬提供的用python生成语音的代码(源自于百度语音),我就是用这种方法生成的语音,一条语音大概2-3k。
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
import requests
import json
import request
import urllib,urllib2
from urllib.request import urlretrieve
for i in range(0,10001):
#改变保存路径
os.chdir("/Users/apple/Desktop/yuyin2")
text = "支付宝到账" + str(round(float(i)/float(10),2)) + "元"
url = "http://tsn.baidu.com/text2audio?lan=zh&ctp=1&cuid=abcdxxx&tok=24.0b321d28b3efe7e7ef9b5e674dc42f9a.2592000.1547950563.282335-11081936&tex={tex}&vol=15&per=0&spd=5&pit=5&aue=3&rate=1".format(tex=text)
sound = "支付宝到账" + str(round(float(i)/float(10),2))
a,b = urllib.urlretrieve(url,'./{sound}.mp3'.format(sound=sound))
print(sound)
这里还有个问题,这么多的语音文件需不需要打包成bundle来用呢?我把这一万多个语音包打包成bundle了,拖进主工程里,发现拓展的Targets里的Build Phases - Copy Bundle Resources还需要加一下这个bundle,才能在程序里用以下方式调用:
self.bestAttemptContent.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"yuyin.bundle/%@.mp3",string]];
但是后来发现,app增加的M数是两倍的语音bundle的大小。上面的操作相当于加了两次语音bundle。所以最后我选择了,直接拖原mp3文件,不用bundle。
这边再提供一条避免app过大的思路,可以让语音包在用户登录之后再下载(可以偷偷下载,也可以引导用户去下载),这样至少用户下载app时的大小不会太大。缺点也有很多,比如用户手机内存不够了,或者清除缓存了,等等。
2019/1/24更新,有人说这个python文件运行出的语音没声音,是token过期了,自己去(http://ai.baidu.com)里注册一个应用,获取它的两个key。然后
1.png2.png
把python文件里的token改一下就可以了。至于其它问题就自己解决吧,我也不会python,但是我会摆渡和求救。
方案二:本地存拆分的音频文件,利用本地通知播报
还是照例送上给出思路的大佬的文章。利用本地通知完成语音的播报,简直666。行吧,第一步,先录好“一”,“二”,... “百”,“千”等拆分的音频,也可以破解收钱吧之类的app获取他们的音频文件。然后上大佬的代码:
- (void)pushNotification{
NSString *tmp = self.bestAttemptContent.body;
NSString *tmp1 = [tmp substringFromIndex:4];
NSString *priceNum = [tmp1 substringToIndex:tmp1.length-1];
NSString *localString = [NSNumberFormatter localizedStringFromNumber:@([priceNum floatValue]) numberStyle:NSNumberFormatterSpellOutStyle];
NSMutableArray *array = [NSMutableArray arrayWithArray:[self stringToArray:localString]];
for (NSString *string in array) {
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self registerNotificationWithString:string completeHandler:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
dispatch_semaphore_signal(semaphore);
});
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
self.contentHandler(self.bestAttemptContent);
}
- (NSArray *)stringToArray:(NSString *)string {
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:50];
for (NSInteger i = 0; i < string.length; i++) {
NSRange range;
range.location = I;
range.length = 1;
NSString *currentString = [string substringWithRange:range];
[mutableArray addObject:currentString];
}
return mutableArray;
}
- (void)registerNotificationWithString:(NSString *)string completeHandler:(dispatch_block_t)complete {
[[UNUserNotificationCenter currentNotificationCenter] requestAuthorizationWithOptions:(UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];
content.title = @"";
content.subtitle = @"";
content.body = @"";
content.sound = [UNNotificationSound soundNamed:[NSString stringWithFormat:@"%@.mp3",string]];
content.categoryIdentifier = [NSString stringWithFormat:@"categoryIndentifier%@",string];
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.01 repeats:NO];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"categoryIndentifier%@",string] content:content trigger:trigger];
[[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
if (complete) {
complete();
}
}
}];
}
}];
}
另外注意用这种方法,需要将拓展的info.plist里的Localization native development region设置成China。例如 106 你设置成China了会转换成一百零六 没有的话就会转换成one hundred and six。
这个方法的好处很直观,包比上面那种方法小,缺点就是声音很难听,很奇怪,想让它声音变得好听,要花费不少时间了,在录音和播放时间上想想办法。还有就是本地通知每传一次会震动一次,比如:收银到账,震一下,九,震一下,万,震一下,八,震一下,千,震一下,七,震一下,百,震一下,六,震一下,十,震一下,五,震一下,点,震一下,四,震一下,三,震一下,元,震一下。这个暂时没有什么好方法,只能引导用户关掉设置里的震动开关。另外,本地通知的语音播报容易被打断。
用这个方法,还会有个问题,比如888.88元,分位上的八可能会因为角位上的八的通知对音频的引用没结束,分位上的八就要播, 会出现播不了的情况。解决方法思路:数字的mp3文件多复制一份,命名如:*_copy.mp3, 循环判断string数组,如果与前一个string重复,就改成 *_copy,这样,就不会连续读取一个文件了。
方案三:VOIP通道
这个方法我没尝试过,你可以看看别人的文章试试。听说现在的微信和支付宝用的是这种方法。建议没有通话功能的APP放弃这条路,因为我没看到语音播报交流群里的人用这个方法过审的。当然,对于无需审核的app或者是符合VOIP条件的app,这种方法肯定是最佳方案。
我的方案:方案一和方案二的结合
因为我这边需要播报五种类型的语音,统一格式是前缀+金额。所以我这边生成了五条前缀音频和一万条金额音频,如:“收银到账”+“666.6元”,然后用本地通知的方式播报,那样app不会太大,而且震动也只会震动1-2次。对我而言,算是一种比较恰当的方法了。
2019/1/24更新,新发现一个更减包大小的方法。把金额那部分进一步拆分,顺便顾及到分,那么就是666+.66元,小数点随你跟哪边。0-1000共一千个音频,角分那部分01-99共一百个文件吧,再加上前缀五个,也就1105个文件,比起上面的一万余音频来说,确实是省了不少大小。优点明显,缺点也就多一次震动吧,你可以试试。
其它的思路,有兴趣可以看看
待我完成后上报给CTO的时候,CTO给了一种思路:在服务器里保存这些语音,然后在拓展里http请求下载,然后播报。这个方案是否可行?
还有,有人发现UNNotificationSound soundNamed:这个方法在两个地方可以取到音频(官方原话:The sound file must be contained in the app’s bundle or in the Library/Sounds folder of the app's data container.)。而且在主程序Library/Sounds文件夹里的音频文件,拓展里还是可以用本地通知的方式播报出来。所以这边是否有新的方案?比如把音频文件生成到Library/Sounds文件夹里,然后播报。
经过测试,发现这两种方案都不行。拓展你可以把它看成一个新的app,和主程序没有数据交互。所以在拓展里进行http请求下载的音频也是进入到拓展的对应的沙盒里,没有什么办法把它移动到主程序的沙盒里。 主程序的路径.png 拓展里的路径.png如图,是两个不同的Library/Sounds文件夹。把音频文件放到主程序的路径的Library/Sounds里,UNNotificationSound soundNamed:能获取到,但是拓展里也有自己的Library/Sounds,里面即使放了音频也无法播放,而且拓展里也没办法把音频转移到主程序路径的Library/Sounds里去。所以此路不通。
结言
还有个问题就是,我这边12.0版本的手机实测也是无法后台语音播报的,12.0.1的可以正常播报。问了下别人,他们只在12.1里有问题,所以我很奇怪。我这边反正是12.0和12.1及12.1之后版本都做好了处理。拿同事的手机测试了没啥问题。如果你知道这个问题的原因请告诉我下。
这近半个月时间基本都花在这个问题上面了。存个档,纪念一下,也给未来遇见这个问题的人节省点时间。如果文中有什么错误,请告诉我。如果你还有更好的解决方法,请一定要告诉我。有什么问题可以加入这个QQ群一起探讨:839097185。