// // NYVoiceManager.m // jiaPei // // Created by Ning.ge on 2023/7/26. // Copyright © 2023 JCZ. All rights reserved. // #import "NYVoiceManager.h" #import "payRequsestHandler.h" static NYVoiceManager *vomanger = nil; static dispatch_once_t onceToken; @interface NYVoiceManager () { NSMutableArray* _textArr; } //SDK 内置播放器 @property(strong) QCloudMediaPlayer *player; // demo 层播放器,如SDK播放器不能满足需求 可以自定义播放器 如下只是一个例子 //@property(strong) MediaPlayerDemo *player; @property (nonatomic,assign)float onlineSpeed; @property (nonatomic,assign)int onlineVoiceType; @property (nonatomic,assign)int onlineLanguage; @property (nonatomic,assign)float onlineVolume; @property (nonatomic,assign)float offlineSpeed; @property (nonatomic,copy)NSString *offlineVoiceType; @property (nonatomic,assign)int offlineLanguage; @property (nonatomic,assign)float offlineVolume; @property (nonatomic,assign)int responseTimeout; @property (nonatomic,assign)int requestTimeout; @property (nonatomic,assign)int checkTimeout; @property (nonatomic, copy) NSString* resource_dir; @property (nonatomic,assign)int cout; //统计合成句子数,当作utteranceId用于标记句子用 @end @implementation NYVoiceManager #pragma mark - SystemMethod + (NYVoiceManager *)sharedManager { dispatch_once(&onceToken, ^{ vomanger = [[self alloc] init]; }); return vomanger; } - (instancetype)init { self = [super init]; if (self) { [self initBaseData]; } return self; } - (void)dealloc { } #pragma mark - PublicMethods // 配置 - (void)ny_configureTTSVoiceDK { QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; [tts cancel]; [_player StopPlay]; [self setOnlineParam:tts];//在线 } -(void)setOnlineParam:(QCloudTTSEngine*)ttsEngine { //设置合成声音速度 [ttsEngine setOnlineVoiceSpeed:_onlineSpeed]; //设置合成声音类型 [ttsEngine setOnlineVoiceType:_onlineVoiceType]; //设置合成语种 [ttsEngine setOnlineVoiceLanguage:_onlineLanguage]; //设置合成声音大小默认值为0 [ttsEngine setOnlineVoiceVolume:_onlineVolume]; //设置响应超时时间 [ttsEngine setTimeoutIntervalForResource:_responseTimeout]; //设置请求超时时间 [ttsEngine setTimeoutIntervalForRequest:_requestTimeout]; //设置检测网络间隔时间 [ttsEngine setCheckNetworkIntervalTime:_checkTimeout]; } - (void)setOfflineParam:(QCloudTTSEngine*)ttsEngine{ [ttsEngine setOfflineVoiceType:_offlineVoiceType]; [ttsEngine setOfflineVoiceSpeed:_offlineSpeed]; [ttsEngine setOfflineVoiceVolume:_offlineVolume]; } //开播合成播放 - (void)startPayVoiceActionText:(NSString *)text completedBlock:(nonnull NYVoicePayCompletedBlock)block { _paytext = text; self.payCompletedBlock = block; QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; [self setOnlineParam:tts]; //设置ProjectId 不设置默认使用0,说明:项目功能用于按项目管理云资源,可以对云资源进行分项目管理,详情见 https://console.cloud.tencent.com/project //[tts setOnlineProjectId:0]; [self stopTTSBtnAction:nil]; _textArr = [self breakIntoSentencesFromString:text]; for (int i = 0; i < 10 - [_player getAudioQueueSize]; i++) { if (_textArr.count > 0) { [tts synthesize:_textArr[0] UtteranceId:[NSString stringWithFormat:@"%d",_cout++]]; [_textArr removeObjectAtIndex:0]; } } } //暂停合成播放 - (void)pausePayVoiceAction { [self pauseTTSBtnAction:nil]; } //恢复合成播放 - (void)resumePayVoiceAction { [self resumeTTSBtnAction:nil]; } //停止合成播放 - (void)stopPayVoiceAction { [self stopTTSBtnAction:nil]; } - (IBAction)stopTTSBtnAction:(id)sender { QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; [tts cancel]; [_player StopPlay]; [_textArr removeAllObjects]; } - (IBAction)pauseTTSBtnAction:(id)sender { [_player PausePlay]; } - (IBAction)resumeTTSBtnAction:(id)sender { [_player ResumePlay]; } #pragma mark - PrivateMethods - (void)initBaseData { _cout = 0; _textArr = [NSMutableArray array]; QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; //如果使用STS临时证书鉴权时需要设置Token [tts setOnlineAuthParam:QDApp_Id SecretId:QDSecret_ID SecretKey:QDSecret_Key Token:nil]; [tts engineInit:0 Delegate:self]; [tts setEnableDebugLog:YES]; //SDK 内置播放器 _player = [[QCloudMediaPlayer alloc]init]; // demo 层播放器,如SDK播放器不能满足需求 可以自定义播放器 如下只是一个例子 源码在MediaPlayerDemo.m文件 可自行修改 //_player = [[MediaPlayerDemo alloc]init]; _player.playerDelegate = self; //离线参数 //在线参数 _onlineSpeed = 0; _onlineVoiceType = 1001; //主语言类型:1-中文(默认)2-英文 _onlineLanguage = 1; _onlineVolume = 0; //连接超时默认15000ms(15s) 范围[500,30000] 单位ms , Mix模式下建议调小此值,以获得更好的体验 _responseTimeout = 15*1000; //读取超时超时默认30000ms(30s) 范围[2200,60000] 单位ms, Mix模式下建议调小此值,以获得更好的体验 _requestTimeout = 30 * 1000; //离线参数,如果您下载的是在线版SDK,以下参数不需要设置,请忽略 _offlineSpeed = 1.0;//离线语速[0.5,2.0] //voiceType 离线音色名称,名称配置位于音色资源目录tts_resource\voices\config.json 中,可自行增删音色 _offlineVoiceType = @"pb"; _offlineVolume = 1.0;//离线音量 > 0 /*_checkTimeout 大于等于0 单位s, 等于0时持续检测,直到成功 Mix模式下,已经连接网络,但出现网络错误后的检测间隔时间,用于从离线模式自动切换回在线模式,默认值5分钟 注意:每次检测时将使用所入参的一句文本请求服务器,如果后端合成成功将会额外消耗该句字数的合成额度 */ _checkTimeout = 300; @try { NSDictionary *dict = @{ @"onlineSpeed":@"0",//设置合成声音速度0默认 1正常 1.2倍 @"onlineVoiceType":@"1001",//设置合成声音类型 @"onlineLanguage":@"1",//设置合成语种-1中文 2英文 @"onlineVolume":@"0",//设置合成声音大小默认值为0 }; if(dict){ //读取服务器配置 _onlineSpeed = [dict[@"onlineSpeed"] floatValue]; _onlineVoiceType = [dict[@"onlineVoiceType"] intValue]; _onlineLanguage = [dict[@"onlineLanguage"] intValue]; _onlineVolume = [dict[@"onlineVolume"] floatValue]; } } @catch (NSException *exception) { NSLog(@"%@",exception.description); } } #pragma mark -- QCloudTTSEngineDelegate //合成结果返回 //---------QCloudTTSEngineDelegate--------- /// 合成回调 /// @param data 语音数据 /// @param utteranceId 语句id /// @param text 文本 /// @param type 引擎类型 0:在线 1:离线 /// -(void) onSynthesizeData:(NSData *_Nullable)data UtteranceId:(NSString *_Nullable)utteranceId Text:(NSString *_Nullable)text EngineType:(NSInteger)type RequestId:(NSString*)requestId{ NSLog(@"text====%@,utteranceId ==%@,type==%ld,requestId=%@",text,utteranceId,(long)type,requestId); [_player enqueueWithData:data Text:text UtteranceId:utteranceId]; // //通过保存音频的URL播放 // //NSString *str = [self filePathWithName:@"tmp.mp3"]; // //NSURL * url = [NSURL URLWithString:str]; // //[_player enqueueWithFile:url Text:text UtteranceId:utteranceId]; // // // _messageTextView.text = [NSString stringWithFormat:@"%@\n text=%@\n utteranceId=%@ requestId=%@",_messageTextView.text,text,utteranceId,requestId]; // [self scrollToBottom]; // // NSLog(@"onSynthesizeData %@",@(data.length)); } /// 错误回调 /// @param error 错误信息 /// @param utteranceId utteranceId /// @param text text -(void) onError:(TtsError *_Nullable)error UtteranceId:(NSString *_Nullable)utteranceId Text:(NSString *_Nullable)text{ // if (error.serviceError != nil && _ttsMode == TTS_MODE_MIX) { // //实际业务上判断一下,如果是混合模式下返回在线合成的后端错误码,应当忽略可不做处理 // //SDK内会继续调用离线合成继续工作,如果没有日志需求,这里直接return忽略即可 // //return; // }else{ // QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; // [tts cancel]; // } // // NSString* msg = [NSString stringWithFormat:@"%@\n error=%@\n error.code=%@\n error.message=%@\n response = %@\n utteranceId=%@",_messageTextView.text,error.error,@(error.err_code),error.msg,error.serviceError.respose, utteranceId]; // _messageTextView.text = msg; // [self scrollToBottom]; // NSLog(@"onError %@",msg); } //离线或者混合模式下,调用初始化后必须收到此回调,并且OfflineAuthInfo.err_code为0时,才可以调用合成接口,如果err_code不为0,请检查参数或者网络,重新init - (void)onOfflineAuthInfo:(QCloudOfflineAuthInfo * _Nonnull)OfflineAuthInfo { // NSString *msg = [NSString stringWithFormat:@"OfflineAuthInfo:err_code=%@,err_msg=%@\n,deviceId=%@\n,expireTime=%@\n,respose=%@\n,authVoiceList=%@",@(OfflineAuthInfo.err_code),OfflineAuthInfo.err_msg,OfflineAuthInfo.deviceId,OfflineAuthInfo.expireTime,OfflineAuthInfo.respose,OfflineAuthInfo.voiceAuthList]; // // NSLog(@"%@",msg); // _messageTextView.text = msg; // [self scrollToBottom]; } //---------QCloudPlayerDelegate--------- //播放开始 -(void) onTTSPlayStart{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n onTTSPlayStart",_messageTextView.text]; // [self scrollToBottom]; NSLog(@"onTTSPlayStart"); self.state = NYVoiceType_Start; if(self.payCompletedBlock){ self.payCompletedBlock(NYVoiceType_Start); } } //队列所有音频播放完成,音频等待中 -(void) onTTSPlayWait{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n onTTSPlayWait",_messageTextView.text]; // [self scrollToBottom]; self.state = NYVoiceType_Wait; NSLog(@"onTTSPlayWait"); if(self.payCompletedBlock){ self.payCompletedBlock(NYVoiceType_Wait); } } //恢复播放 -(void) onTTSPlayResume{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n onTTSPlayResume",_messageTextView.text]; // [self scrollToBottom]; NSLog(@"onTTSPlayResume"); self.state = NYVoiceType_Resume; if(self.payCompletedBlock){ self.payCompletedBlock(NYVoiceType_Resume); } } //暂停播放 -(void) onTTSPlayPause{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n onTTSPlayPause",_messageTextView.text]; // [self scrollToBottom]; NSLog(@"onTTSPlayPause"); self.state = NYVoiceType_Pause; if(self.payCompletedBlock){ self.payCompletedBlock(NYVoiceType_Pause); } } //播放中止 -(void)onTTSPlayStop{ _cout = 0; // _messageTextView.text = [NSString stringWithFormat:@"%@\n onTTSPlayStop",_messageTextView.text]; // [self scrollToBottom]; NSLog(@"onTTSPlayStop"); self.state = NYVoiceType_Stop; if(self.payCompletedBlock){ self.payCompletedBlock(NYVoiceType_Stop); } } //播放器异常 -(void)onTTSPlayError:(QCPlayerError* _Nullable)playError{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n playError.code==%@\n playError.massage==%@",_messageTextView.text,@(playError.mCode),playError.message]; // [self scrollToBottom]; NSLog(@"playError.code==%@,playError.massage==%@",@(playError.mCode),playError.message); } //即将播放播放下一句,即将播放音频对应的句子,以及这句话utteranceId /// 即将播放播放下一句,即将播放音频对应的句子,以及这句话utteranceId /// @param text 当前播放句子的文本 /// @param utteranceId 当前播放音频对应的ID -(void) onTTSPlayNextWithText:(NSString* _Nullable)text UtteranceId:(NSString* _Nullable)utteranceId{ NSLog(@"text==%@,utteranceId==%@",text,utteranceId); QCloudTTSEngine *tts = [QCloudTTSEngine getShareInstance]; if ([_player getAudioQueueSize] < 10) { if (_textArr.count > 0) { NSLog(@"AudioQueueSize=%@",@([_player getAudioQueueSize])); [tts synthesize:_textArr[0] UtteranceId:[NSString stringWithFormat:@"%d",_cout++]]; [_textArr removeObjectAtIndex:0]; } } } /// 当前播放的字符,当前播放的字符在所在的句子中的下标. /// @param currentWord 当前读到的单个字,是一个估算值不一定准确 /// @param currentIdex 当前播放句子中读到文字的下标 -(void)onTTSPlayProgressWithCurrentWord:(NSString*_Nullable)currentWord CurrentIndex:(NSInteger)currentIdex{ // _messageTextView.text = [NSString stringWithFormat:@"%@\n CurrentWord==%@\n currentIdex==%@",_messageTextView.text,currentWord,@(currentIdex)]; // NSLog(@"CurrentWord==%@,currentIdex==%@",currentWord,@(currentIdex)); // [self scrollToBottom]; } /*===============================使用自定义分割逻辑,以下为句子分割示例=====================*/ #define Sentence_Count_Max 50 //单次最大请求字数,建议首句不要设置太长 /* API最大请求字数详见官网文档https://cloud.tencent.com/document/product/1073/37995 */ - (NSMutableArray *)breakIntoSentencesFromString:(NSString *)string { NSMutableArray *stringsArr = [NSMutableArray array]; NSString *splitString = @",;,;"; //先按句子分割 [string enumerateSubstringsInRange:NSMakeRange(0, string.length) options:NSStringEnumerationBySentences usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) { //后台接收字符不能太长 const char *commitChar = [substring UTF8String]; if (strlen(commitChar) > Sentence_Count_Max * 3) { //UTF-8中文的字符长度一般是3个字节,英文的长度为1 //再按分号逗号分割 NSArray *textArr = [self separatedString:substring split:splitString]; for (NSString *object in textArr) { //再按字数分割 const char *objectChar = [object UTF8String]; if (strlen(objectChar) > Sentence_Count_Max * 3) { NSString *remainStr = object; while (remainStr.length > Sentence_Count_Max) { NSString *sentence = [remainStr substringToIndex:Sentence_Count_Max]; if (![self isAllSplitString:sentence split:splitString] &&![self isPunct:sentence]) { [stringsArr addObject:sentence]; } remainStr = [remainStr substringFromIndex:Sentence_Count_Max]; } if (![self isAllSplitString:remainStr split:splitString] &&![self isPunct:remainStr]) { [stringsArr addObject:remainStr]; } } else { if (![self isAllSplitString:object split:splitString] &&![self isPunct:object]) { [stringsArr addObject:object]; } } } }else{ if (![self isAllSplitString:substring split:splitString] &&![self isPunct:substring]) { [stringsArr addObject:substring]; } } }]; return stringsArr; } - (NSArray *)separatedString:(NSString *)text split:(NSString *)split { NSMutableArray *mArray = [NSMutableArray array]; NSInteger loc = 0; for (NSInteger i = 0; i < [text length]; i++) { if ([split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location != NSNotFound) { NSString *subString = [text substringWithRange:NSMakeRange(loc, i-loc+1)]; [mArray addObject:subString]; loc = i + 1; } if (i + 1 == [text length] && [split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location == NSNotFound) { //判断最后一个字符是否包含分隔符,如果不包含把最后一句添加到数组 NSString *subString = [text substringWithRange:NSMakeRange(loc, i-loc+1)]; [mArray addObject:subString]; } } return [mArray copy]; } - (BOOL)isAllSplitString:(NSString *)text split:(NSString *)split { split = @",;,;。"; if ([text length] && [split length]) { BOOL all = YES; for (NSInteger i = 0; i < [text length]; i++) { if ([split rangeOfString:[text substringWithRange:NSMakeRange(i, 1)]].location == NSNotFound) { all = NO; break; } } return all; } else { return NO; } } -(BOOL)isPunct:(NSString *)string //校验是否全为符号 { NSError *error = nil; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[\\p{P}~^<>]" options:NSRegularExpressionCaseInsensitive error:&error]; NSString *modifiedString = [regex stringByReplacingMatchesInString:string options:0 range:NSMakeRange(0, [string length]) withTemplate:@""]; NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; NSString *trimmedStr = [modifiedString stringByTrimmingCharactersInSet:set]; if (!trimmedStr.length) { return YES; } return NO; } @end