// // HiSpeechSynthesizer.m // HiBaiduTts // // Created by 连仁辉 on 2022/4/22. // Copyright © 2022 Facebook. All rights reserved. // #import "HiSpeechSynthesizer.h" #import "BDSSpeechSynthesizer.h" #import "TTDFileReader.h" #import "BDSSpeechSynthesizerParams.h" #import <AVFoundation/AVFoundation.h> #define READ_SYNTHESIS_TEXT_FROM_FILE (NO) static BOOL isSpeak = YES; static BOOL textFromFile = READ_SYNTHESIS_TEXT_FROM_FILE; static BOOL displayAllSentences = !READ_SYNTHESIS_TEXT_FROM_FILE; @interface HiSpeechSynthesizer () @property (nonatomic,assign)BOOL pauseOrResumeEnable; @property (nonatomic,assign)BOOL cancelEnable; @property (nonatomic,copy)NSString* apiKey; @property (nonatomic,copy)NSString* secretKey; @property (nonatomic,strong)NSMutableArray* synthesisTexts; @property (nonatomic,strong)NSMutableArray* addTextQueue; /*used with textFromFile*/ @end @implementation HiSpeechSynthesizer + (BOOL)isFileSynthesisEnabled{ return textFromFile; } + (BOOL)isSpeakEnabled{ return isSpeak; } + (void)setFileSynthesisEnabled:(BOOL)isEnabled{ textFromFile = isEnabled; displayAllSentences = !textFromFile; } + (void)setSpeakEnabled:(BOOL)isEnabled{ isSpeak = isEnabled; } - (instancetype)initApiKey:(NSString *)apiKey secretKey:(NSString *)secretKey { self = [super init]; if(self){ self.synthesisTexts = [[NSMutableArray alloc] init]; self.apiKey = apiKey; self.secretKey = secretKey; self.pauseOrResumeEnable = FALSE; self.cancelEnable = FALSE; [self configureSDK]; } return self; } - (void)setSpeaker:(NSString *)speaker { NSError *err = [[BDSSpeechSynthesizer sharedInstance] setSynthParam:speaker forKey:BDS_SYNTHESIZER_PARAM_SPEAKER]; if(err){ [self displayError:err withTitle:@"Failed set online TTS speaker"]; } } - (void)setVolume:(NSNumber *)value{ NSError* err = [[BDSSpeechSynthesizer sharedInstance] setSynthParam:value forKey:BDS_SYNTHESIZER_PARAM_VOLUME]; if(err){ [self displayError:err withTitle:@"Failed set synth volume"]; } } - (void)setSpeed:(NSNumber *)value{ NSError* err = [[BDSSpeechSynthesizer sharedInstance] setSynthParam:value forKey:BDS_SYNTHESIZER_PARAM_SPEED]; if(err){ [self displayError:err withTitle:@"Failed set synth speed"]; } } - (void)setPitch:(NSNumber *)value{ NSError* err = [[BDSSpeechSynthesizer sharedInstance] setSynthParam:value forKey:BDS_SYNTHESIZER_PARAM_PITCH]; if(err){ [self displayError:err withTitle:@"Failed set synth pitch"]; } } - (void)setAudioSessionEnable:(NSNumber *)value { NSError* err = [[BDSSpeechSynthesizer sharedInstance] setSynthParam:value forKey:BDS_SYNTHESIZER_PARAM_ENABLE_AVSESSION_MGMT]; if(err){ [self displayError:err withTitle:@"Failed set synth session enable"]; } } - (void)setAudioSessionCategory:(NSNumber *)category{ int _category = category.intValue; NSString *avCategory = nil; switch (_category) { case 0: avCategory = AVAudioSessionCategoryAmbient; break; case 1: avCategory = AVAudioSessionCategorySoloAmbient; break; case 2: avCategory = AVAudioSessionCategoryPlayback; break; case 3: avCategory = AVAudioSessionCategoryPlayAndRecord; break; default: break; } if (avCategory) { <#statements#> } } - (NSMutableArray *)addTextQueue { if(!_addTextQueue){ _addTextQueue = [[NSMutableArray alloc] init]; } return _addTextQueue; } -(void)configureSDK{ NSLog(@"TTS version info: %@", [BDSSpeechSynthesizer version]); [BDSSpeechSynthesizer setLogLevel:BDS_PUBLIC_LOG_VERBOSE]; [[BDSSpeechSynthesizer sharedInstance] setSynthesizerDelegate:self]; [self configureOnlineTTS]; } -(void)configureOnlineTTS{ [[BDSSpeechSynthesizer sharedInstance] setApiKey:self.apiKey withSecretKey:self.secretKey]; [[BDSSpeechSynthesizer sharedInstance] setSynthParam:@(BDS_SYNTHESIZER_SPEAKER_FEMALE) forKey:BDS_SYNTHESIZER_PARAM_SPEAKER]; [[BDSSpeechSynthesizer sharedInstance] setSynthParam:@(7) forKey:BDS_SYNTHESIZER_PARAM_SPEED]; } -(void)configureOfflineTTS{ } -(void)logTitle:(NSString *)title message:(NSString *)message { NSLog(@"error: %@ detail: %@",title,message); } -(void)displayError:(NSError*)error withTitle:(NSString*)title{ NSString* errMessage = error.localizedDescription; NSLog(@"error: %@ detail: %@",title,errMessage); } -(void)updateSynthProgress{ // [self.SynthesizeTextProgressView setText:nil]; NSMutableAttributedString *str = [[NSMutableAttributedString alloc] init]; if(displayAllSentences){ for(NSDictionary* contentDict in self.synthesisTexts){ [str appendAttributedString:[contentDict objectForKey:@"TEXT"]]; } } else{ if(self.synthesisTexts.count > 0){ NSDictionary* contentDict = [self.synthesisTexts objectAtIndex:0]; [str appendAttributedString:[contentDict objectForKey:@"TEXT"]]; } } // [self.SynthesizeTextProgressView setAttributedText:str]; } -(void)addFileTextLoop { if(self.addTextQueue.count > 0){ NSAttributedString* string = [[NSAttributedString alloc] initWithString:[self.addTextQueue objectAtIndex:0]]; [self.addTextQueue removeObjectAtIndex:0]; NSInteger sentenceID; NSError* err = nil; if(isSpeak) sentenceID = [[BDSSpeechSynthesizer sharedInstance] speakSentence:[string string] withError:&err]; else sentenceID = [[BDSSpeechSynthesizer sharedInstance] synthesizeSentence:[string string] withError:&err]; if(err == nil){ NSMutableDictionary *addedString = [[NSMutableDictionary alloc] initWithObjects:@[string, [NSNumber numberWithInteger:sentenceID], [NSNumber numberWithInteger:0], [NSNumber numberWithInteger:0]] forKeys:@[@"TEXT", @"ID", @"SPEAK_LEN", @"SYNTH_LEN"]]; [self.synthesisTexts addObject:addedString]; [self updateSynthProgress]; if(self.synthesisTexts.count == 1){ self.cancelEnable = YES; self.pauseOrResumeEnable = YES; } } else{ [self displayError:err withTitle:@"Add sentence Error"]; [self.addTextQueue removeAllObjects]; } } if(self.addTextQueue.count > 0){ [self performSelector:@selector(addFileTextLoop) withObject:nil afterDelay:0.2]; } } - (void)synthesizeFile:(NSString *)text_file { if(textFromFile){ if(text_file == nil){ NSString *domain = @"cn.histron"; NSString *desc = [NSString stringWithFormat:@"Couldn't find text file \"%@\" from mainBundle",text_file];//NSLocalizedString国际化 NSDictionary *userInfo = @{NSLocalizedDescriptionKey:desc}; NSError *error = [NSError errorWithDomain:domain code:500 userInfo:userInfo]; [self displayError:error withTitle:@"File not found"]; return; } int total = 0; TTDFileReader* reader = [[TTDFileReader alloc] initWithFilePath:text_file]; NSString *line = @""; NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(/*kCFStringEncodingUTF8*/kCFStringEncodingGB_18030_2000); while ( (line = [reader readLineWithEncoding:gbkEncoding]) ) { total++; } total = total*4; int added = 1; // for(int i = 0;i < 4; i++){ reader = [[TTDFileReader alloc] initWithFilePath:text_file]; // NSString *line = @""; // NSStringEncoding gbkEncoding = CFStringConvertEncodingToNSStringEncoding(/*kCFStringEncodingUTF8*/kCFStringEncodingGB_18030_2000); while ( (line = [reader readLineWithEncoding:gbkEncoding]) ) { [self.addTextQueue addObject:[NSString stringWithFormat:@"Text %d of %d: %@", added, total, line]]; added++; } // } [self performSelector:@selector(addFileTextLoop) withObject:nil afterDelay:0.2]; } } - (void)synthesizeText:(NSString *)text { if(textFromFile)return; //[self.SynthesizeButton setEnabled:NO]; NSAttributedString* string = [[NSAttributedString alloc] initWithString:text]; NSInteger sentenceID; NSError* err = nil; if(isSpeak) sentenceID = [[BDSSpeechSynthesizer sharedInstance] speakSentence:[string string] withError:&err]; else sentenceID = [[BDSSpeechSynthesizer sharedInstance] synthesizeSentence:[string string] withError:&err]; if(err == nil){ NSMutableDictionary *addedString = [[NSMutableDictionary alloc] initWithObjects:@[string, [NSNumber numberWithInteger:sentenceID], [NSNumber numberWithInteger:0], [NSNumber numberWithInteger:0]] forKeys:@[@"TEXT", @"ID", @"SPEAK_LEN", @"SYNTH_LEN"]]; [self.synthesisTexts addObject:addedString]; [self updateSynthProgress]; if(self.synthesisTexts.count == 1){ self.cancelEnable = YES; self.pauseOrResumeEnable = YES; } } else{ [self displayError:err withTitle:@"Add sentence Error"]; } } - (void)pauseOrResume { if([[BDSSpeechSynthesizer sharedInstance] synthesizerStatus] == BDS_SYNTHESIZER_STATUS_PAUSED){ [[BDSSpeechSynthesizer sharedInstance] resume]; }else if([[BDSSpeechSynthesizer sharedInstance] synthesizerStatus] == BDS_SYNTHESIZER_STATUS_WORKING){ [[BDSSpeechSynthesizer sharedInstance] pause]; }else{ [self logTitle:@"Error" message:@"Synthesis doesn't seem to be running so can't pause or resume..."]; self.cancelEnable = NO; self.pauseOrResumeEnable = NO; } } - (void)cancel{ if(self.addTextQueue){ [self.addTextQueue removeAllObjects]; } [[BDSSpeechSynthesizer sharedInstance] cancel]; [self.synthesisTexts removeAllObjects]; [self updateSynthProgress]; self.cancelEnable = NO; self.pauseOrResumeEnable = NO; } - (void)releaseInstance{ [self cancel]; [BDSSpeechSynthesizer releaseInstance]; } -(void)refreshAfterProgressUpdate:(NSMutableDictionary*)updatedSentence{ NSString* totalText = [((NSAttributedString*)[updatedSentence objectForKey:@"TEXT"]) string]; NSInteger readOffset = [[updatedSentence objectForKey:@"SPEAK_LEN"] integerValue]; NSInteger synthOffset = [[updatedSentence objectForKey:@"SYNTH_LEN"] integerValue]; NSLog(@"UPDATE PROGRESS: ReadLen: %ld, SynthLen: %ld, TotalLen: %ld", readOffset, synthOffset, totalText.length); NSRange readRange = NSMakeRange(0, readOffset); NSRange synthRange = NSMakeRange(readOffset, synthOffset-readOffset); NSRange unprocessedRange = NSMakeRange(synthOffset, totalText.length-synthOffset); NSString* readText = [totalText substringWithRange: readRange]; NSString* synthesizeSentenceText = [totalText substringWithRange: synthRange]; NSString* unProcessedText = [totalText substringWithRange: unprocessedRange]; NSLog(@"readText: %@",readText); NSLog(@"synthesizeSentenceText: %@",synthesizeSentenceText); NSLog(@"unProcessedText: %@",unProcessedText); // [updatedSentence setObject:allMessage forKey:@"TEXT"]; // [self updateSynthProgress]; } #pragma mark - implement BDSSpeechSynthesizerDelegate - (void)synthesizerStartWorkingSentence:(NSInteger)SynthesizeSentence{ NSLog(@"Did start synth %ld", SynthesizeSentence); self.cancelEnable = YES; self.pauseOrResumeEnable = YES; } - (void)synthesizerFinishWorkingSentence:(NSInteger)SynthesizeSentence{ NSLog(@"Did finish synth, %ld", SynthesizeSentence); if(!isSpeak){ if(self.synthesisTexts.count > 0 && SynthesizeSentence == [[[self.synthesisTexts objectAtIndex:0] objectForKey:@"ID"] integerValue]){ [self.synthesisTexts removeObjectAtIndex:0]; [self updateSynthProgress]; } else{ NSLog(@"Sentence ID mismatch??? received ID: %ld\nKnown sentences:", (long)SynthesizeSentence); for(NSDictionary* dict in self.synthesisTexts){ NSLog(@"ID: %ld Text:\"%@\"", [[dict objectForKey:@"ID"] integerValue], [((NSAttributedString*)[dict objectForKey:@"TEXT"]) string]); } } if(self.synthesisTexts.count == 0){ self.cancelEnable = NO; self.pauseOrResumeEnable = NO; } } } - (void)synthesizerSpeechStartSentence:(NSInteger)SpeakSentence{ NSLog(@"Did start speak %ld", SpeakSentence); } - (void)synthesizerSpeechEndSentence:(NSInteger)SpeakSentence{ NSLog(@"Did end speak %ld", SpeakSentence); if(self.synthesisTexts.count > 0 && SpeakSentence == [[[self.synthesisTexts objectAtIndex:0] objectForKey:@"ID"] integerValue]){ [self.synthesisTexts removeObjectAtIndex:0]; [self updateSynthProgress]; } else{ NSLog(@"Sentence ID mismatch??? received ID: %ld\nKnown sentences:", (long)SpeakSentence); for(NSDictionary* dict in self.synthesisTexts){ NSLog(@"ID: %ld Text:\"%@\"", [[dict objectForKey:@"ID"] integerValue], [((NSAttributedString*)[dict objectForKey:@"TEXT"]) string]); } } if(self.synthesisTexts.count == 0){ self.cancelEnable = NO; self.pauseOrResumeEnable = NO; } } - (void)synthesizerNewDataArrived:(NSData *)newData DataFormat:(BDSAudioFormat)fmt characterCount:(int)newLength sentenceNumber:(NSInteger)SynthesizeSentence{ NSMutableDictionary* sentenceDict = nil; for(NSMutableDictionary *dict in self.synthesisTexts){ if([[dict objectForKey:@"ID"] integerValue] == SynthesizeSentence){ sentenceDict = dict; break; } } if(sentenceDict == nil){ NSLog(@"Sentence ID mismatch??? received ID: %ld\nKnown sentences:", (long)SynthesizeSentence); for(NSDictionary* dict in self.synthesisTexts){ NSLog(@"ID: %ld Text:\"%@\"", [[dict objectForKey:@"ID"] integerValue], [((NSAttributedString*)[dict objectForKey:@"TEXT"]) string]); } return; } [sentenceDict setObject:[NSNumber numberWithInteger:newLength] forKey:@"SYNTH_LEN"]; [self refreshAfterProgressUpdate:sentenceDict]; } - (void)synthesizerTextSpeakLengthChanged:(int)newLength sentenceNumber:(NSInteger)SpeakSentence{ NSLog(@"SpeakLen %ld, %d", SpeakSentence, newLength); NSMutableDictionary* sentenceDict = nil; for(NSMutableDictionary *dict in self.synthesisTexts){ if([[dict objectForKey:@"ID"] integerValue] == SpeakSentence){ sentenceDict = dict; break; } } if(sentenceDict == nil){ NSLog(@"Sentence ID mismatch??? received ID: %ld\nKnown sentences:", (long)SpeakSentence); for(NSDictionary* dict in self.synthesisTexts){ NSLog(@"ID: %ld Text:\"%@\"", [[dict objectForKey:@"ID"] integerValue], [((NSAttributedString*)[dict objectForKey:@"TEXT"]) string]); } return; } [sentenceDict setObject:[NSNumber numberWithInteger:newLength] forKey:@"SPEAK_LEN"]; [self refreshAfterProgressUpdate:sentenceDict]; } - (void)synthesizerdidPause{ [self logTitle:@"synthesizer did pause" message:@""]; } - (void)synthesizerResumed{ NSLog(@"Did resume"); [self logTitle:@"synthesizer Resumed" message:@""]; } - (void)synthesizerCanceled{ NSLog(@"Did cancel"); } - (void)synthesizerErrorOccurred:(NSError *)error speaking:(NSInteger)SpeakSentence synthesizing:(NSInteger)SynthesizeSentence{ NSLog(@"Did error %ld, %ld", SpeakSentence, SynthesizeSentence); if(self.addTextQueue){ [self.addTextQueue removeAllObjects]; } self.cancelEnable = NO; self.pauseOrResumeEnable = NO; [self.synthesisTexts removeAllObjects]; [self updateSynthProgress]; [[BDSSpeechSynthesizer sharedInstance] cancel]; [self displayError:error withTitle:@"Synthesis failed"]; } @end