zyc 689fa8936b Integrate Volcengine realtime voice + Live2D mouth driving
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:39:23 +08:00

779 lines
30 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// BiTTSViewController.m
// SpeechDemo
//
// Created by ByteDance on 2025/7/3.
// Copyright © 2025 fangweiwei. All rights reserved.
//
#import "BiTTSViewController.h"
#include <CoreFoundation/CoreFoundation.h>
#include <objc/objc.h>
#import <AVFoundation/AVFoundation.h>
#import "AppDelegate.h"
#import "FileUtils.h"
#import "SettingsHelper.h"
#import "ViewController.h"
#import "SensitiveDefines.h"
static int TTS_MAX_RETRY_COUNT = 3;
@interface BiTTSViewController () <SpeechEngineDelegate, UITextViewDelegate>
@property (weak, nonatomic) IBOutlet UITextView *referTextView;
@property (weak, nonatomic) IBOutlet UITextView *resultTextView;
@property (weak, nonatomic) IBOutlet UITextField *statusTextView;
@property (weak, nonatomic) IBOutlet UIButton *engineSwitchButton;
@property (weak, nonatomic) IBOutlet UIButton *startEngineButton;
@property (weak, nonatomic) IBOutlet UIButton *stopEngineButton;
@property (weak, nonatomic) IBOutlet UIButton *synthesisButton;
@property (weak, nonatomic) IBOutlet UIButton *pauseResumeButton;
// Device ID: 用于定位线上问题
@property (nonatomic, strong) NSString *deviceID;
// Debug Path: 用于存放一些 SDK 相关的文件,比如模型、日志等
@property (strong, nonatomic) NSString *debugPath;
// SpeechEngine
@property (strong, nonatomic) SpeechEngine *curEngine;
// Engine State
@property (assign, nonatomic) BOOL engineInited;
@property (assign, nonatomic) BOOL engineStarted;
@property (assign, nonatomic) BOOL engineErrorOccurred;
@property (assign, nonatomic) BOOL playerPaused;
@property (assign, nonatomic) BOOL refTextChanged;
// Settings
@property (strong, nonatomic) Settings *settings;
// 一些在线合成的配置
@property (strong, nonatomic) NSString *ttsAppId;
@property (strong, nonatomic) NSString *ttsVoiceOnline;
@property (strong, nonatomic) NSString *ttsVoiceTypeOnline;
// tts合成文本相关
@property (assign, nonatomic) int ttsSynthesisIndex;
@property (strong, nonatomic) NSMutableArray* ttsSynthesisText;
@property (strong, nonatomic) NSMutableDictionary* ttsSynthesisMap;
@property (assign, nonatomic) int ttsRetryCount;
@end
@implementation BiTTSViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.settings = [[SettingsHelper shareInstance]getSettings:VIEW_BITTS];
self.engineSwitchButton.enabled = TRUE;
[self decorateTextView:self.referTextView];
[self decorateTextView:self.resultTextView];
[self.referTextView setDelegate:self];
self.referTextView.editable = TRUE;
self.engineInited = FALSE;
self.engineStarted = FALSE;
self.engineErrorOccurred = FALSE;
self.playerPaused = FALSE;
// 初始化和tts有关的字段
self.ttsSynthesisIndex = 0;
self.ttsSynthesisText = [[NSMutableArray alloc] init];
self.ttsSynthesisMap = [[NSMutableDictionary alloc]init];
self.ttsRetryCount = TTS_MAX_RETRY_COUNT;
[self.referTextView setText:@"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。"];
[self.statusTextView setText:@"Waiting for init."];
[ViewController setAppDelegate:(AppDelegate *)[[UIApplication sharedApplication] delegate]];
self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSLog(@"当前调试路径 %@", self.debugPath);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(audioInterruptionHandler:)
name:AVAudioSessionInterruptionNotification
object:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
}
- (void)decorateTextView:(UITextView *)textView {
textView.layer.cornerRadius = 5.0f;
textView.layer.borderWidth = .25f;
textView.layer.borderColor = [UIColor grayColor].CGColor;
}
#pragma mark - Notifications
-(void)appWillTerminate:(NSNotification*)note {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVAudioSessionInterruptionNotification
object:nil];
}
- (void)audioInterruptionHandler:(NSNotification*)notification {
AVAudioSessionInterruptionType interruptionType = (AVAudioSessionInterruptionType)[[notification.userInfo objectForKey:AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
AVAudioSessionInterruptionOptions intertuptionOptions = [[notification.userInfo objectForKey:AVAudioSessionInterruptionOptionKey] unsignedIntValue];
NSLog(@"Receive audio interruption notification, type: %lu, options: %lu.", (unsigned long)interruptionType, (unsigned long)intertuptionOptions);
if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
NSLog(@"Audio session interruption began");
@synchronized (self) {
[self pausePlayback];
}
} else if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
@synchronized (self) {
NSLog(@"Audio session interruption ended");
if (intertuptionOptions == AVAudioSessionInterruptionOptionShouldResume) {
AVAudioSession *session = [AVAudioSession sharedInstance];
AVAudioSessionCategoryOptions cur_options = session.categoryOptions;
// AudioQueueStart() will return AVAudioSessionErrorCodeCannotInterruptOthers if options didn't contains AVAudioSessionCategoryOptionMixWithOthers
if (!(cur_options & AVAudioSessionCategoryOptionMixWithOthers)) {
AVAudioSessionCategoryOptions readyOptions = AVAudioSessionCategoryOptionMixWithOthers | cur_options;
[session setCategory:AVAudioSessionCategoryPlayback withOptions:readyOptions error:nil];
}
[self resumePlayback];
cur_options = session.categoryOptions;
// Remove AVAudioSessionCategoryOptionMixWithOthers, or the playback will not be interrupted any more
if (cur_options & AVAudioSessionCategoryOptionMixWithOthers) {
[session setCategory:AVAudioSessionCategoryPlayback withOptions:((~AVAudioSessionCategoryOptionMixWithOthers) & cur_options) error:nil];
}
}
}
}
}
#pragma mark - Config & Init & Uninit Methods
-(void)configInitParams {
//【必需配置】Engine Name
[self.curEngine setStringParam:SE_BITTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING];
//【可选配置】Debug & Log
[self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_DEBUG_PATH_STRING];
[self.curEngine setStringParam:SE_LOG_LEVEL_DEBUG forKey:SE_PARAMS_KEY_LOG_LEVEL_STRING];
//【可选配置】User ID用以辅助定位线上用户问题
[self.curEngine setStringParam:SDEF_UID forKey:SE_PARAMS_KEY_UID_STRING];
[self.curEngine setStringParam:self.deviceID forKey:SE_PARAMS_KEY_DEVICE_ID_STRING];
//【可选配置】是否将合成出的音频保存到设备上,为 true 时需要正确配置 PARAMS_KEY_TTS_AUDIO_PATH_STRING 才会生效
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_DUMP]
forKey:SE_PARAMS_KEY_TTS_ENABLE_DUMP_BOOL];
// TTS 音频文件保存目录,必须在合成之前创建好且 APP 具有访问权限,保存的音频文件名格式为 tts_{reqid}.wav, {reqid} 是本次合成的请求 id
// PARAMS_KEY_TTS_ENABLE_DUMP_BOOL 配置为 true 的音频时为【必需配置】,否则为【可选配置】
[self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_TTS_AUDIO_PATH_STRING];
//【可选配置】自定义请求Header
NSString* headerStr = [self.settings getString:SETTING_REQUEST_HEADERS];
if ([headerStr isEqualToString:@""]) {
headerStr = @"{}";
}
[self.curEngine setStringParam:headerStr forKey:SE_PARAMS_KEY_REQUEST_HEADERS_STRING];
//【可选配置】是否使用 SDK 内置播放器播放合成出的音频,默认为 true
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_PLAYER]
forKey:SE_PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL];
//【可选配置】是否令 SDK 通过回调返回合成的音频数据,默认不返回。
// 开启后SDK 会流式返回音频,收到 SETtsAudioData 回调表示当次合成所有的音频已经全部返回
[self.curEngine setBoolParam:[self.settings getBool:SETTING_ENABLE_PLAYER_AUDIO_CALL_BACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_ENABLE_PLAYER_AUDIO_CALLBACK_BOOL];
// ------------------------ 在线合成相关配置 -----------------------
NSString* appid = [self.settings getString:SETTING_APPID];
self.ttsAppId = appid.length > 0 ? appid : SDEF_APPID;
//【必需配置】在线合成鉴权相关Appid
[self.curEngine setStringParam:self.ttsAppId forKey:SE_PARAMS_KEY_APP_ID_STRING];
NSString* token = [self.settings getString:SETTING_TOKEN];
NSString* ttsAppToken = token.length > 0 ? token : SDEF_TOKEN;
//【必需配置】在线合成鉴权相关Token
[self.curEngine setStringParam:ttsAppToken forKey:SE_PARAMS_KEY_APP_TOKEN_STRING];
NSString *address = [self.settings getString:SETTING_ADDRESS];
NSString *ttsAddress = address.length > 0 ? address : SDEF_DEFAULT_ADDRESS;
//【必需配置】语音合成服务域名
[self.curEngine setStringParam:ttsAddress forKey:SE_PARAMS_KEY_TTS_ADDRESS_STRING];
NSString *uri = [self.settings getString:SETTING_URI];
NSString *ttsUri = uri.length > 0 ? uri : SDEF_BITTS_DEFAULT_URI;
//【必需配置】语音合成服务Uri
[self.curEngine setStringParam:ttsUri forKey:SE_PARAMS_KEY_TTS_URI_STRING];
NSString *resourceId = [self.settings getString:SETTING_RESOURCE_ID];
//【必需配置】语音合成服务资源id
[self.curEngine setStringParam:resourceId forKey: SE_PARAMS_KEY_RESOURCE_ID_STRING];
//【可选配置】TTS连接超时时间
[self.curEngine setIntParam:10000 forKey:SE_PARAMS_KEY_TTS_CONN_TIMEOUT_INT];
}
-(void)configStartTtsParams {
// 准备待合成的文本
if(![self prepareBiTTSText]) {
char fake_error_info[] = "{err_code:3006, err_msg:\"Invalid input text.\"}";
[self speechEngineError:[NSData dataWithBytes:fake_error_info length:sizeof(fake_error_info)]];
return;
}
}
- (NSString*)getSynthesisText {
if(self.refTextChanged){ //如果文本有变更重新追加进去
self.refTextChanged = FALSE;
__block NSString* refText = nil;
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
refText = self.referTextView.text;
});
} else {
refText = self.referTextView.text; // 已经在主线程,直接赋值
}
if (refText.length > 0) {
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^;!?。!?;…]+[;!?。!?;…]?" options:0 error:&error];
if (!error) {
NSArray<NSTextCheckingResult *> *matches = [regex matchesInString:refText options:0 range:NSMakeRange(0, refText.length)];
for (NSTextCheckingResult *match in matches) {
NSString *sentence = [refText substringWithRange:match.range];
[self addSentence:sentence];
}
}
}
}
if(self.ttsSynthesisIndex == [self.ttsSynthesisText count]){
NSLog(@"no more tts conttent to synthesis");
[self updateTtsResultText:@"No More Text to Synthesis"];
return nil;
}
NSString* text = self.ttsSynthesisText[self.ttsSynthesisIndex];
NSLog(@"Synthesis: %d, text: %@", self.ttsSynthesisIndex, text);
self.ttsSynthesisIndex = (self.ttsSynthesisIndex + 1);
return text;
}
- (void)initEngine {
NSLog(@"获取设备ID调试使用");
AppDelegate *appDelegate = [ViewController getAppDelegate];
if (appDelegate == nil) {
appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
}
[ViewController setAppDelegate:appDelegate];
self.deviceID = appDelegate.deviceID;
NSLog(@"获取设备ID成功: %@", self.deviceID);
NSLog(@"创建引擎");
if (self.curEngine == nil) {
self.curEngine = [[SpeechEngine alloc] init];
if (![self.curEngine createEngineWithDelegate:self]) {
NSLog(@"引擎创建失败.");
return;
}
}
NSLog(@"SDK 版本号: %@", [self.curEngine getVersion]);
[self initEngineInternal];
}
- (void)initEngineInternal {
NSLog(@"配置初始化参数");
[self configInitParams];
NSLog(@"引擎初始化");
SEEngineErrorCode ret = [self.curEngine initEngine];
self.engineInited = (ret == SENoError);
if (self.engineInited) {
NSLog(@"初始化成功");
[self speechEngineInitSucceeded];
} else {
NSLog(@"初始化失败,返回值: %d", ret);
[self speechEngineInitFailed:ret];
}
}
- (void)uninitEngine {
if (self.curEngine != nil) {
NSLog(@"引擎析构");
[self.curEngine destroyEngine];
self.curEngine = nil;
NSLog(@"引擎析构完成");
}
}
#pragma mark - UI Actions
- (IBAction)switchEngine:(id)sender {
if (self.engineStarted) {
[self.statusTextView setText:@"Engine is busy, stop it first!"];
return;
}
[self clearResult:nil];
self.startEngineButton.enabled = FALSE;
self.synthesisButton.enabled = FALSE;
self.pauseResumeButton.enabled = FALSE;
if (self.engineInited) {
self.referTextView.editable = TRUE;
[self uninitEngine];
self.engineInited = FALSE;
[self.statusTextView setText:@"Waiting for init."];
self.engineSwitchButton.enabled = TRUE;
[self.engineSwitchButton setTitle:@"Init Engine" forState:UIControlStateNormal];
self.stopEngineButton.enabled = FALSE;
} else {
self.referTextView.editable = TRUE;
[self initEngine];
}
}
- (IBAction)Synthesis:(id)sender {
[self triggerSynthesis];
}
- (IBAction)startEngineBtnClicked:(id)sender {
NSLog(@"Start engine, current status: %d", self.engineStarted);
if (!self.engineStarted) {
[self clearResult:nil];
self.engineErrorOccurred = FALSE;
// Directive启动引擎前调用SYNC_STOP指令保证前一次请求结束。
NSLog(@"关闭引擎(同步)");
NSLog(@"Directive: SEDirectiveSyncStopEngine");
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSyncStopEngine];
if (ret != SENoError) {
NSLog(@"Send directive syncstop failed: %d", ret);
} else {
[self configStartTtsParams];
NSLog(@"启动引擎.");
NSLog(@"Directive: SEDirectiveStartEngine");
NSString* startPayload = @"{\"req_params\":{\"speaker\":\"zh_female_roumeinvyou_emo_v2_mars_bigtts\",\"audio_params\":{\"emotion\":\"excited\",\"loudness_rate\":50}}}";
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine data:startPayload];
if (SENoError != ret) {
NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret];
[self sendStartEngineDirectiveFailed:message];
}
}
}
}
- (IBAction)stopEngineBtnClicked:(id)sender {
NSLog(@"关闭引擎");
NSLog(@"Directive: SEDirectiveStopEngine");
[self.curEngine sendDirective:SEDirectiveStopEngine];
}
- (void)pausePlayback {
NSLog(@"暂停播放");
NSLog(@"Directive: SEDirectivePausePlayer");
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectivePausePlayer];
if (ret == SENoError) {
self.playerPaused = TRUE;
[self.pauseResumeButton setTitle:@"Resume" forState:UIControlStateNormal];
}
NSLog(@"Pause playback status: %d", ret);
}
- (void)resumePlayback {
NSLog(@"继续播放");
NSLog(@"Directive: SEDirectiveResumePlayer");
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveResumePlayer];
if (ret == SENoError) {
self.playerPaused = FALSE;
[self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal];
}
NSLog(@"Resume playback status: %d", ret);
}
- (IBAction)controlPlayingStatus:(id)sender {
NSLog(@"Pause or resume player, current player status: %hhd", self.playerPaused);
if (self.playerPaused) {
[self resumePlayback];
} else {
[self pausePlayback];
}
}
- (IBAction)clearResult:(id)sender {
dispatch_async(dispatch_get_main_queue(), ^{
[self.resultTextView setText:@""];
});
}
#pragma mark - Message Callback
- (void)onMessageWithType:(SEMessageType)type andData:(NSData *)data {
NSLog(@"Message Type: %d.", type);
switch (type) {
case SEEngineStart:
NSLog(@"Callback: 引擎启动成功: data: %@", data);
[self speechEngineStarted];
break;
case SEEngineStop:
NSLog(@"Callback: 引擎关闭: data: %@", data);
[self speechEngineStopped];
break;
case SEEngineError:
NSLog(@"Callback: 错误信息: %@", data);
[self speechEngineError:data];
break;
case SEEventConnectionStarted:
NSLog(@"Callback: SEEventConnectionStarted");
break;
case SEEventConnectionFailed:
NSLog(@"Callback: SEEventConnectionFailed");
break;
case SEEventConnectionFinished:
NSLog(@"Callback: SEEventConnectionFinished");
break;
case SEEventSessionStarted:
NSLog(@"Callback: SEEventSessionStarted");
break;
case SEEventSessionCanceled:
NSLog(@"Callback: SEEventSessionCanceled");
break;
case SEEventSessionFinished:
NSLog(@"Callback: SEEventSessionFinished");
break;
case SEEventSessionFailed:
NSLog(@"Callback: SEEventSessionFailed");
break;
case SEEventTTSSentenceStart:
[self speechStartSynthesis:data];
NSLog(@"Callback: 合成开始 SentenceStart: %@", data);
break;
case SEEventTTSSentenceEnd:
[self speechFinishSynthesis:data];
NSLog(@"Callback: 合成结束 SentenceEnd: %@", data);
case SEEventTTSResponse:
NSLog(@"Callback: 收到合成音频 TTSResponse: %@", data);
case SEEventTTSEnded:
NSLog(@"Callback: TTSEnd: %@", data);
case SEPlayerAudioData:
// NSLog(@"Callback: 播放的pcm音频: %@", data);
break;
case SEPlayerStartPlayAudio:
NSLog(@"Callback: 开始播放TTS音频");
[self speechStartPlaying:nil];
break;
case SEPlayerFinishPlayAudio:
NSLog(@"Callback: 结束播放TTS音频");
[self speechFinishPlaying:nil];
break;
default:
break;
}
}
- (void)speechEngineInitSucceeded {
dispatch_async(dispatch_get_main_queue(), ^{
self.engineSwitchButton.enabled = TRUE;
[self.engineSwitchButton setTitle:@"UninitEngine" forState:UIControlStateNormal];
[self.statusTextView setText:@"Ready"];
[self.resultTextView setText:[NSString stringWithFormat:@"DeviceID: %@.", self.deviceID]];
self.referTextView.editable = TRUE;
self.startEngineButton.enabled = TRUE;
});
}
- (void)speechEngineInitFailed:(int)initStatus {
dispatch_async(dispatch_get_main_queue(), ^{
[self uninitEngine];
[self.statusTextView setText:[[NSString alloc] initWithFormat:@"Failed to init engine, %d!", initStatus]];
self.engineSwitchButton.enabled = TRUE;
});
}
- (void)sendSynthesisDirectiveFailed:(NSString*)tipText {
NSLog(@"%@", tipText);
dispatch_async(dispatch_get_main_queue(), ^{
[self.resultTextView setText:tipText];
[self.curEngine sendDirective:SEDirectiveStopEngine];
});
}
- (void)sendStartEngineDirectiveFailed:(NSString*)tipText {
NSLog(@"%@", tipText);
dispatch_async(dispatch_get_main_queue(), ^{
[self.resultTextView setText:tipText];
self.engineStarted = FALSE;
});
}
- (void)speechEngineStarted {
self.ttsRetryCount = TTS_MAX_RETRY_COUNT;
dispatch_async(dispatch_get_main_queue(), ^{
self.referTextView.editable = TRUE;
self.engineStarted = true;
[self.statusTextView setText:@"Engine Started!"];
self.startEngineButton.enabled = FALSE;
self.synthesisButton.enabled = TRUE;
self.stopEngineButton.enabled = TRUE;
});
}
- (void)speechEngineStopped {
dispatch_async(dispatch_get_main_queue(), ^{
self.referTextView.editable = TRUE;
self.engineStarted = FALSE;
[self.statusTextView setText:@"Engine Stopped!"];
self.startEngineButton.enabled = TRUE;
self.synthesisButton.enabled = FALSE;
self.stopEngineButton.enabled = FALSE;
[self.pauseResumeButton setTitle:@"Pause" forState:UIControlStateNormal];
self.pauseResumeButton.enabled = FALSE;
self.playerPaused = FALSE;
});
}
- (void)speechEngineError:(NSData *)data {
dispatch_async(dispatch_get_main_queue(), ^{
BOOL needStop = NO;
id json_obj = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
if ([json_obj isKindOfClass:[NSDictionary class]]) {
NSDictionary *error_info = json_obj;
NSInteger code = [[error_info objectForKey:@"err_code"] intValue];
switch (code) {
case SETTSLimitQps:
case SETTSLimitCount:
case SETTSServerBusy:
case SETTSLongText:
case SETTSInvalidText:
case SETTSSynthesisTimeout:
case SETTSSynthesisError:
case SETTSSynthesisWaitingTimeout:
case SETTSErrorUnknown:
NSLog(@"When meeting this kind of error, continue to synthesize.");
[self triggerSynthesis];
break;
case SEConnectTimeout:
case SEReceiveTimeout:
case SENetLibError:
// 遇到网络错误时建议重试,重试次数不超过 3 次
needStop = ![self retrySynthesis];
if (needStop) {
self.engineErrorOccurred = TRUE;
}
break;
default:
needStop = YES;
self.engineErrorOccurred = TRUE;
dispatch_async(dispatch_get_main_queue(), ^{
[self.resultTextView
setText:[[NSString alloc]
initWithData:data
encoding:NSUTF8StringEncoding]];
});
break;
}
} else {
needStop = YES;
}
if (needStop) {
[self.curEngine sendDirective:SEDirectiveStopEngine];
}
});
}
- (void)scrollTextViewToBottom {
dispatch_async(dispatch_get_main_queue(), ^{
if (self.resultTextView.text.length > 0) {
NSRange bottomRange = NSMakeRange(self.resultTextView.text.length - 1, 1);
[self.resultTextView scrollRangeToVisible:bottomRange];
[self.resultTextView layoutIfNeeded];
}
});
}
-(void)updateTtsResultText:(NSString*) playingId {
if (self.engineErrorOccurred) {
NSLog(@"When a fatal error occurs, prevent the playback text from being displayed.");
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
NSString *currentText = self.resultTextView.text ?: @"";
NSString *newText = [NSString stringWithFormat:@"%@\n%@", currentText, playingId];
[self.resultTextView setText:newText];
});
[self scrollTextViewToBottom];
}
- (void)speechStartSynthesis:(NSData *)data {
if (self.ttsSynthesisIndex < [self.ttsSynthesisText count]) {
NSString* req_id = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[self.ttsSynthesisMap setValue:[NSNumber numberWithInt:self.ttsSynthesisIndex] forKey:req_id];
}
dispatch_async(dispatch_get_main_queue(), ^{
self.synthesisButton.enabled = FALSE;
});
}
- (void)speechFinishSynthesis:(NSData *)data {
if (self.ttsRetryCount < TTS_MAX_RETRY_COUNT) {
self.ttsRetryCount = TTS_MAX_RETRY_COUNT;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.synthesisButton.enabled = TRUE;
});
}
- (void)speechStartPlaying:(NSData *)data {
NSLog(@"TTS start playing");
dispatch_async(dispatch_get_main_queue(), ^{
self.pauseResumeButton.enabled = TRUE;
});
}
- (void)speechFinishPlaying :(NSData *)data {
NSLog(@"TTS finish playing");
dispatch_async(dispatch_get_main_queue(), ^{
[self updateTtsResultText:@"playing finished"];
});
[self triggerSynthesis];
}
- (void)speechTtsAudioData:(NSData *)data {
}
- (BOOL)retrySynthesis {
BOOL ret = FALSE;
if (self.engineStarted && self.ttsRetryCount > 0) {
NSLog(@"Retry synthesis for text: %@", self.ttsSynthesisText[self.ttsSynthesisIndex]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self triggerSynthesis];
});
self.ttsRetryCount -= 1;
ret = TRUE;
}
return ret;
}
-(void)triggerSynthesis {
NSString *text = [self getSynthesisText];
if(text == nil){
return;
}
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveEventStartSession data:@""];
if (ret != SENoError) {
NSLog(@"Start Session faile: %d", ret);
}
NSString* taskRequestJson = [NSString stringWithFormat: @"{\"req_params\":{\"text\":\"%@\"}}", text];
// SEDirectiveTaskRequest 是可以连续发送的服务端会缓存每一句文本然后按顺序通过sentence_start 、 task respnse... sentence_end返回当前处理句的结果
ret = [self.curEngine sendDirective:SEDirectiveEventTaskRequest data:taskRequestJson];
[self updateTtsResultText:text];
NSLog(@"触发合成");
NSLog(@"Directive: SEDirectiveTaskRequest");
if (ret != SENoError) {
NSLog(@"Synthesis faile: %d", ret);
}
ret = [self.curEngine sendDirective:SEDirectiveEventFinishSession data:@""];
if (ret != SENoError) {
NSLog(@"Finish Session faile: %d", ret);
}
}
-(void)addSentence:(NSString*) text {
NSCharacterSet* blankChar = [NSCharacterSet characterSetWithCharactersInString:@" "];
NSString* tmp = [text stringByTrimmingCharactersInSet:blankChar];
if (tmp.length > 0) {
[self.ttsSynthesisText addObject:tmp];
}
}
-(void)resetTtsContext {
self.ttsSynthesisIndex = 0;
[self.ttsSynthesisText removeAllObjects];
[self.ttsSynthesisMap removeAllObjects];
}
-(BOOL)prepareBiTTSText {
[self resetTtsContext];
__block NSString* text = nil;
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
text = self.referTextView.text;
});
} else {
text = self.referTextView.text; // 已经在主线程,直接赋值
}
if (text.length <= 0) {
text = @"愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光。就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。此后如竟没有炬火:我便是唯一的光。";
}
if (self.ttsSynthesisText == nil || [self.ttsSynthesisText count] <= 0) {
// 使用下面几个标点符号来分句,会让通过 MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS 返回的播放进度更加准确
NSArray* temp = [text componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@";!?。!?;…"]];
if (text.length > 0) {
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^;!?。!?;…]+[;!?。!?;…]?" options:0 error:&error];
if (!error) {
NSArray<NSTextCheckingResult *> *matches = [regex matchesInString:text options:0 range:NSMakeRange(0, text.length)];
for (NSTextCheckingResult *match in matches) {
NSString *sentence = [text substringWithRange:match.range];
[self addSentence:sentence];
}
}
}
}
NSLog(@"Synthesis text item num: %ld.", [self.ttsSynthesisText count]);
return [self.ttsSynthesisText count] > 0;
}
#pragma mark - Helper
- (long)timeDelayFrom:(long)pastTimestamp {
return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp;
}
#pragma mark - UITextViewDelegate
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if([text isEqualToString:@"\n"]) {
[textView resignFirstResponder];
return NO;
}
return YES;
}
- (void)textViewDidChange:(UITextView *)textView {
if(textView == _referTextView){
self.refTextChanged = TRUE;
}
}
#pragma mark - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
id nextPage = [segue destinationViewController];
[nextPage setValue:VIEW_BITTS forKey:@"viewId"];
}
@end