美文网首页iOSiOS开发技术
iOS 12.1语音播报的回顾

iOS 12.1语音播报的回顾

作者: 樊开囧 | 来源:发表于2018-12-28 16:46 被阅读839次

    前言

      首先介绍下自己接手的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.png
    2.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。

    相关文章

      网友评论

        本文标题:iOS 12.1语音播报的回顾

        本文链接:https://www.haomeiwen.com/subject/cqnslqtx.html