iOS 推送播放语音的需求调研,即收到推送后,播放推送的文案,文案的内容不固定。类似于支付宝和微信的收款到账语音。
项目添加了Notification Service Extension之后的逻辑,和没添加之前有所不同。如下图:添加了之后,接受到推送时,会触发Notification Service Extension中的方法,在这个方法中,可以修改推送的标题、内容、声音。然后把修改后的推送展示出来。
通知栏的生命周期:
要注意的是,Notification Service Extension和主项目不是同一个Target,所以主项目的文件和这个Target文件是不共享的。
创建新文件的时候要注意勾选要添加到的Target
比如添加推送播放语音的类,需要勾选到Notification Service Extension Target下;
拷贝播放语音的第三方SDK,需要勾选到Notification Service Extension Target下;
在第三方平台创建新应用时,要填写的bundleID也应该是Notification Service Extension Target对应的bundleID。,这点尤其要注意,因为百度的测试账号离线SDK的添加只能添加一次,错了的话,就要用新的账号再去注册,血泪的教训,。
bundle目录的访问也不是同一个,可以通过App Group共享数据。
打开后台播放时,其实也应该是Notification Service Extension Target下的后台播放,这个后面详细说明。
创建步骤如下:
点击完成,点击Activate
打开NotificationService.m中的文件,这个类就是Notificaiton Service Extension添加后自动创建的类,添加了之后,接受到推送的处理都可以在这个位置修改
其中修改推送铃声时要注意:
多条推送处理的问题,在didReceiveNotificationRequest:withContentHandler:方法中调用self.contentHandler(self.bestAttemptContent);,即会展示对应的通知,如果不调用此方法,最多30s系统会自动调用此方法,假设一次性来了10条通知,会发现,通知并没有弹出10次,也没有按顺序一次次展示,所以多条推送如果没有处理,播放语音时就会出现问题。
语音的文件类型:自定义铃声支持的声音格式包括,aiff、wav以及wav格式,铃声的长度必须小于30s,否则系统会播放默认的铃声。
音频文件存储的目录和读取的优先级,主应用中的Library/Sounds文件夹中、AppGroups共享目录中的Library/Sounds文件夹中、main bundle
在系统播放类AVSpeechSynthesizer的代理方法中,有播放完成的回掉speechSynthesizer:didFinishSpeechUtterance:,把呼出通知栏的代码self.contentHandler(self.bestAttemptContent)从didReceiveNotificationRequest:withContentHandler:方法中,移到播放完成的回掉方法中调用,即可保证语音按顺序一条条展示。(或者添加到数组或着OperationQueue中,播放完成继续下一条)
didReceiveNotificationRequest:withContentHandler:方法,其中的bestAttemptContent中的userInfo即包含了推送的详细信息。如果想要修改展示的标题和内容或者推送的语音,都在这个方法最后回掉前操作,
@interface NotificationService ()
@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@end
@implementation NotificationService
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];
// Modify the notification content here...
// 修改推送的标题
// self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];
// 修改推送的声音,自定义铃声支持的声音格式包括,aiff、wav以及wav格式,铃声的长度必须小于30s,否则系统会播放默认的铃声。
// self.bestAttemptContent.sound = [UNNotificationSound soundNamed:@"a.wav"];
// 播放处理
[self playVoiceWithInfo:self.bestAttemptContent.userInfo];
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);
}
- (void)playVoiceWithInfo:(NSDictionary *)userInfo {
NSLog(@"NotificationExtension content : %@",userInfo);
NSString *title = userInfo[@"aps"][@"alert"][@"title"];
NSString *subTitle = userInfo[@"aps"][@"alert"][@"subtitle"];
NSString *subMessage = userInfo[@"aps"][@"alert"][@"body"];
NSString *isRead = userInfo[@"isRead"];
NSString *isUseBaiDu = userInfo[@"isBaiDu"];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionDuckOthers error:nil];
[[AVAudioSession sharedInstance] setActive:YES
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
error:nil];
// Ps: 下面代码示例并没有多条播放的处理,还请注意
if ([isRead isEqual:@"1"]) {
// 播放语音
if ([isUseBaiDu isEqual:@"1"]) {
// 使用百度离线语音播放
[[BaiDuTtsUtils shared] playBaiDuTTSVoiceWithContent:title];
}
else {
// 使用系统语音播放
[[AppleTtsUtils shared] playAppleTTSVoiceWithContent:title];
}
}
else {
// 无需播放语音
}
}
@end
其中AppleTtsUtils中实现如下,大致就是使用AVSpeechSynthesizer直接播放,设置音量和语速,需要注意的是,
音量的设置
静音时是不会播放的
实际播放的音量大小=设置的音量大小*系统音量的大小。所以即使设置了大音量,但是系统音量很小,播放的声音也很小。(比如系统volume是0.5,AVAudioPlayer的音量是0.6,则最终的音量为0.5*0.6 =0.3)。解决方案是:最终的解决方案借鉴了进入收付款展示二维码时自动调节屏幕亮度的方案:如果屏幕亮度未达到阈值,则调高屏幕亮度到阈值,离开页面时,将亮度设回原亮度。同理,播放提示音时,若用户设置的系统音量小于阈值,则调节到阈值。提示音播放完毕后,将提示音调回原音量,大致意思是:
数字的处理
数字转语音,采用zh-CN的voice后,数字的播放方式是几万几千几百几十几这种,可采用数字后面拼接空格的方式来处理;遍历内容的每一个字符串,如果是数字,则拼接一个空格到后面,最后播放时数字就会一个个读出来。
#import "AppleTtsUtils.h"
#import <AVFoundation/AVFoundation.h>
#import <AVKit/AVKit.h>
@interface AppleTtsUtils ()<AVSpeechSynthesizerDelegate>
@property (nonatomic, strong) AVSpeechSynthesizer *speechSynthesizer;
@property (nonatomic, strong) AVSpeechSynthesisVoice *speechSynthesisVoice;
@end
@implementation AppleTtsUtils
+ (instancetype)shared {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self class] new];
});
return instance;
}
- (BOOL)isNumber:(NSString *)str
{
if (str.length == 0) {
return NO;
}
NSString *regex = @"[0-9]*";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",regex];
if ([pred evaluateWithObject:str]) {
return YES;
}
return NO;
}
- (void)playAppleTtsVoiceWithContent:(NSString *)content {
if ((content == nil) || (content.length <= 0)) {
return;
}
// 数字转语音,采用zh-CN的voice后,数字的播放方式是几万几千几百几十几这种,故而采用数字后面拼接空格的方式来处理;遍历内容的每一个字符串,如果是数字,则拼接一个空格到后面,最后播放时数字就会一个个读出来。
NSString *newResult = @"";
for (int i = 0; i < content.length; i++) {
NSString *tempStr = [content substringWithRange:NSMakeRange(i, 1)];
newResult = [newResult stringByAppendingString:tempStr];
if ([self deptNumInputShouldNumber:tempStr] ) {
newResult = [newResult stringByAppendingString:@" "];
}
}
// Todo: 英文转语音
AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:newResult];
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
utterance.voice = self.speechSynthesisVoice;
utterance.volume = 1.0;
utterance.rate = AVSpeechUtteranceDefaultSpeechRate;
[self.speechSynthesizer speakUtterance:utterance];
}
- (AVSpeechSynthesizer *)speechSynthesizer {
if (!_speechSynthesizer) {
_speechSynthesizer = [[AVSpeechSynthesizer alloc] init];
_speechSynthesizer.delegate = self;
}
return _speechSynthesizer;
}
- (AVSpeechSynthesisVoice *)speechSynthesisVoice {
if (!_speechSynthesisVoice) {
_speechSynthesisVoice = [AVSpeechSynthesisVoice voiceWithLanguage:@"zh-CN"];
}
return _speechSynthesisVoice;
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didStartSpeechUtterance:(AVSpeechUtterance *)utterance {
NSLog(@"didStartSpeechUtterance");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didCancelSpeechUtterance:(AVSpeechUtterance *)utterance {
NSLog(@"didCancelSpeechUtterance");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didPauseSpeechUtterance:(AVSpeechUtterance *)utterance {
NSLog(@"didPauseSpeechUtterance");
}
- (void)speechSynthesizer:(AVSpeechSynthesizer *)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance *)utterance {
NSLog(@"didFinishSpeechUtterance");
[self.speechSynthesizer stopSpeakingAtBoundary:AVSpeechBoundaryWord];
// // 每一条语音播放完成后,我们调用此代码,用来呼出通知栏
// 可通过Block回掉暴露给上层
// self.contentHandler(self.bestAttemptContent);
}
2 . 左侧选中离线SDK管理,点击添加,然后选中刚刚创建的应用,点击完成后,点击下载序列号列表,然后把AppId、AppKey、SecretKey、以及序列号存储,用于初始化离线SDK。如下图
3 . 左侧选中离线SDK管理时,点击右边的下载SDK,以及开发文档,按照SDK的说法
❝集成指南: 强烈建议用户首先运行SDK包中的Demo工程,Demo工程中详细说明了语音合成的使用方法,并提供了完整的示例。一般情况下,您只需参照demo工程即可完成所有的集成和配置工作。
4 . 所以,把SDK下载好了之后,打开BDSClientSample项目,然后把TTSViewController.mm文件中的APP_ID、API_KEY、SECRET_KEY和SN改为刚刚申请的,然后运行测试,看能否正常播放语音,播放成功说明申请的没有问题,就可以继续往项目中集成,要不然,集成到项目中发现不播放,会怀疑是SDK的问题。,以为集成后调试确实很容易让人怀疑人生。
5 . 把SDK解压后的BDSClientHeaders、BDSClientLib、BDSClientResource文件夹拖拽到Notification Service Extension的target下,注意勾选copy选项,然后把BDSClientLib文件夹下的.gitignore删除,要不然编译会失败,真的,不骗人,,踩坑指南
6 . 添加依赖的系统库,参考BDSClientSample项目中的依赖,注意添加到Notification Service Extension的target下,如下图:
7 . done,编译Notification Service Extension的target,注意选对target,噢噢,这个地方还有个问题,新创建的target是根据Xcode的版本来的,所以还需要修改一下这个target兼容的最低target,要不然默认可能是14.4,然后运行调试不报错,能正常运行,但是断点不走,惊不惊喜,。
8 . 添加百度语音处理代码到Notification Service Extension的target下,如上面写的,BaiDuTtsUtils代码如下
#import "BaiDuTtsUtils.h"
#import "BDSSpeechSynthesizer.h"
// 百度TTS
NSString* BaiDuTTSAPP_ID = @"Your_APP_ID";
NSString* BaiDuTTSAPI_KEY = @"Your_APP_KEY";
NSString* BaiDuTTSSECRET_KEY = @"Your_SECRET_KEY";
NSString* BaiDuTTSSN = @"Your_SN";
@interface BaiDuTtsUtils ()<BDSSpeechSynthesizerDelegate>
@end
@implementation BaiDuTtsUtils
+ (instancetype)shared {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self class] new];
});
return instance;
}
#pragma mark - baidu tts
-(void)configureOfflineTTS{
NSError *err = nil;
NSString* offlineSpeechData = [[NSBundle mainBundle] pathForResource:@"bd_etts_common_speech_m15_mand_eng_high_am-mgc_v3.6.0_20190117" ofType:@"dat"];
NSString* offlineTextData = [[NSBundle mainBundle] pathForResource:@"bd_etts_common_text_txt_all_mand_eng_middle_big_v3.4.2_20210319" ofType:@"dat"];
// #error "set offline engine license"
if (offlineSpeechData == nil || offlineTextData == nil) {
NSLog(@"离线合成 资源文件为空!");
return;
}
err = [[BDSSpeechSynthesizer sharedInstance] loadOfflineEngine:offlineTextData speechDataPath:offlineSpeechData licenseFilePath:nil withAppCode:BaiDuTTSAPP_ID withSn:BaiDuTTSSN];
if(err){
NSLog(@"Offline TTS init failed");
return;
}
}
- (void)playBaiDuTTSVoiceWithContent:(NSString *)voiceText {
NSLog(@"TTS version info: %@", [BDSSpeechSynthesizer version]);
[BDSSpeechSynthesizer setLogLevel:BDS_PUBLIC_LOG_VERBOSE];
// 设置委托对象
[[BDSSpeechSynthesizer sharedInstance] setSynthesizerDelegate:self];
[self configureOfflineTTS];
[[BDSSpeechSynthesizer sharedInstance] setPlayerVolume:10];
[[BDSSpeechSynthesizer sharedInstance] setSynthParam:[NSNumber numberWithInteger:5] forKey:BDS_SYNTHESIZER_PARAM_SPEED];
// 开始合成并播放
NSError* speakError = nil;
NSInteger sentenceID = [[BDSSpeechSynthesizer sharedInstance] speakSentence:voiceText withError:&speakError];
if (speakError) {
NSLog(@"错误: %ld, %@", (long)speakError.code, speakError.localizedDescription);
}
}
- (void)synthesizerStartWorkingSentence:(NSInteger)SynthesizeSentence
{
NSLog(@"Began synthesizing sentence %ld", (long)SynthesizeSentence);
}
- (void)synthesizerFinishWorkingSentence:(NSInteger)SynthesizeSentence
{
NSLog(@"Finished synthesizing sentence %ld", (long)SynthesizeSentence);
}
- (void)synthesizerSpeechStartSentence:(NSInteger)SpeakSentence
{
NSLog(@"Began playing sentence %ld", (long)SpeakSentence);
}
- (void)synthesizerSpeechEndSentence:(NSInteger)SpeakSentence
{
NSLog(@"Finished playing sentence %ld", (long)SpeakSentence);
}
@end
刺激的部分来了,上面都编译通过了没问题,使用推送调试,先运行一次主项目,然后选中Notification Service Extension Target运行,didReceiveNotificationRequest:withContentHandler:方法中添加断点,,给自己推送消息,会发现断点走到了这里,说明target的创建没有问题。
然后控制推送参数的,isRead和isBaiDu参数,决定推送过来的语音是否走百度的语音播放。噢,说到推送参数,这个地方还需要在payload推送参数中添加"mutable-content = 1"字段,eg:
{
"aps": {
"alert": {
"title":"标题",
"subtitle: "副标题",
"body": "内容"
},
"badge": 1,
"sound": "default",
"mutable-content": "1",
}
}
推送调试,会发现运行正常,但是语音没有播放,不管是系统的还是百度的,哈哈哈,崩溃不。仔细看控制台,会发现,报错如下
Ps: iOS 12.0之后,在Notification Service Extension调用系统播放AVSpeechSynthesizer时报的错误。
[AXTTSCommon] Failure starting audio queue alp!
[AXTTSCommon] _BeginSpeaking: couldn't begin playback
Ps: iOS 12.0之后,在Notification Service Extension调用百度的SDK直接播放时报的错误。
[ERROR][AudioBufPlayer.mm:1088]AudioQueue start errored error: 561015905 (!pla)
[ERROR][AudioBufPlayer.mm:1099]Can't begin playback while in background!
都是一个意思,即不能在后台播放音频。怎么解决呢,当然是添加backgroundMode字段了,打开主工程的Signing&Capabilities,添加backgrondModes,勾选Audio, Airplay, and Picture in Picture,如下图
OK,try again! 再次推送,会发现————还是不行,同样的报错,哈哈哈,绝望不,不好意思,我收敛一下,这个地方其实添加的没错,只不过要注意
❝1. 在Notification Service Extension配置了之后,发现收到通知后还是不会播放声音,在这个Extension的Target下打开plist,添加Required background modes字段,里面item0写上App plays audio or streams audio/video using AirPlay后,再次调试,发现百度的语音即可播放。
2 . 这种方式审核时不被通过,因为这个Extension的target其实是没有backgroundMode的设置的,从Signing&Capabilities中可以看出,直接添加backgroundMode是没有的。故而如果不是上线到苹果商店的,只是公司内部分发,可以用这种方式。
添加了之后,再次推送,就会发现百度的语音就可以播放了,而且数字和英文、中文播放都十分完美,除了价格有些感人,其他的没毛病。而系统的播放语音,如果先推送系统的,会发现不能播放,还是同样的报错;但是如果先推送了走百度的,百度播放了之后,再推送系统的,就会发现系统的也能播报,但是系统播报的英文和数字会有问题,记得处理,可以听一下英文字母E的发音,发音额。。。解决方案——暂无,还没找到,建议走第三方合成的语音。
由于项目不需要上线商店,所以到这里其实就结束了。但是对于上线到商店到应用来说,这种处理方法是不行的,上线到商店的应用其实只有播放固定格式的音频一种解决方法,即替换推送的声音。使用固定格式的音频、或者固定格式的合成音频替换掉推送的声音,或者采用远程推送静音,发送多个本地通知,各个本地通知的声音替换掉这种方法。这些是从末尾的参考中得到的启示。
直接上图,整理后的思维导图如下,大部分比较复杂的处理逻辑其实是iOS 12.0之后的处理。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/Gpu4sw0Zn4IBS-k5wvF__g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。