提交 29fa1f76 作者: edy

feat: 语音合成相关

上级 a5965975
......@@ -16,20 +16,71 @@ class AivoicePlugin: FlutterPlugin, MethodCallHandler {
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "aivoice_plugin")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initEngine" -> {
// 现有的实现
}
"stopEngine" -> {
// 现有的实现
}
"uninitEngine" -> {
// 现有的实现
}
"startOrStopEngine" -> {
// 现有的实现
}
"prepareEnvironment" -> {
// 现有的实现
}
"ttsStartEngineBtnClick" -> {
// 空实现
result.success(null)
}
"ttsSynthesis" -> {
// 空实现
result.success(null)
}
"ttsStopEngineBtnClicked" -> {
// 空实现
result.success(null)
}
"ttsPausePlayback" -> {
// 空实现
result.success(null)
}
"ttsResumePlayback" -> {
// 空实现
result.success(null)
}
"ttsInitEngine" -> {
// 空实现
result.success(null)
}
"ttsUnInitEngine" -> {
// 空实现
result.success(null)
}
"destoryTtsNovel" -> {
// 空实现
result.success(null)
}
"destoryAsrVoice" -> {
// 空实现
result.success(null)
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
......@@ -34,6 +34,6 @@ SPEC CHECKSUMS:
SpeechEngineToB: a49185c07a099cdc052de97218bc10dc4ff60152
TTNetworkManager: 47d93100d944e2ae807e035d8636df92fd5cc390
PODFILE CHECKSUM: bde3e45995fad5550475b342803cb71575488751
PODFILE CHECKSUM: 0212500e480860ee905e9b132693e030f85d651b
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2
......@@ -66,19 +66,40 @@ class _MyAppState extends State<MyApp> {
children: [
TextButton(
onPressed: () {
_aivoicePlugin.initEngine(configMap);
_aivoicePlugin.ttsInitEngine(configMap);
},
child: const Text('init')),
TextButton(
onPressed: () {
_aivoicePlugin.startOrStopEngine(true);
_aivoicePlugin.ttsStartEngineBtnClick(
{"text": "引擎启动成功,收到该回调后,在单次合成场景下收到该回调时语音合成已经开始,同时数据字段为该次请求的请求 ID; 连续合成场景下还需要再发送合成指令,才真正的开始合成。"});
},
child: const Text('start')),
TextButton(
onPressed: () {
_aivoicePlugin.stopEngine();
_aivoicePlugin.ttsStopEngineBtnClicked();
},
child: const Text('stop')),
TextButton(
onPressed: () {
_aivoicePlugin.ttsSynthesis({});
},
child: const Text('合成'))
// TextButton(
// onPressed: () {
// _aivoicePlugin.initEngine(configMap);
// },
// child: const Text('init')),
// TextButton(
// onPressed: () {
// _aivoicePlugin.startOrStopEngine(true);
// },
// child: const Text('start')),
// TextButton(
// onPressed: () {
// _aivoicePlugin.stopEngine();
// },
// child: const Text('stop')),
],
),
),
......
#import "AivoicePlugin.h"
#import "VoiceAsr.h"
#import "TtsNovel.h"
@interface AivoicePlugin () <FlutterStreamHandler, VoiceAsrDelegate>
@interface AivoicePlugin () <FlutterStreamHandler, VoiceAsrDelegate, TtsNovelDelegate>
@property (nonatomic, strong) FlutterEventSink eventSink;
@property(nonatomic, strong)VoiceAsr * voiceAsr;
@property(nonatomic, strong)TtsNovel * ttsNovel;
@property(nonatomic, strong)NSDictionary * config;
@property(nonatomic, strong)NSDictionary * ttsNovelconfig;
@end
@implementation AivoicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"aivoice_plugin"
binaryMessenger:[registrar messenger]];
AivoicePlugin* instance = [[AivoicePlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"aivoice_plugin"
binaryMessenger:[registrar messenger]];
AivoicePlugin* instance = [[AivoicePlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
FlutterEventChannel* eventChannel = [FlutterEventChannel
eventChannelWithName:@"aivoice_plugin/events"
binaryMessenger:[registrar messenger]];
[eventChannel setStreamHandler:instance];
FlutterEventChannel* eventChannel = [FlutterEventChannel
eventChannelWithName:@"aivoice_plugin/events"
binaryMessenger:[registrar messenger]];
[eventChannel setStreamHandler:instance];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// 删除了 getPlatformVersion 方法的实现
if ([@"initEngine" isEqualToString:call.method]) {
self.config = call.arguments;
[self.voiceAsr initEngineWithConfig:call.arguments];
result(nil);
} else if ([@"stopEngine" isEqualToString:call.method]) {
[self.voiceAsr stopEngineBtnClicked];
result(nil);
} else if ([@"uninitEngine" isEqualToString:call.method]) {
[self.voiceAsr uninitEngine];
result(nil);
} else if ([@"startOrStopEngine" isEqualToString:call.method]) {
// BOOL arg = [NSNumber numberWithBool:call.arguments];
[self.voiceAsr startEngineBtnClicked];
result(nil);
} else if ([@"prepareEnvironment" isEqualToString:call.method]) {
[VoiceAsr prepareEnvironment:call.arguments];
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
// 删除了 getPlatformVersion 方法的实现
if ([@"initEngine" isEqualToString:call.method]) {
self.config = call.arguments;
[self.voiceAsr initEngineWithConfig:call.arguments];
result(nil);
} else if ([@"stopEngine" isEqualToString:call.method]) {
[self.voiceAsr stopEngineBtnClicked];
result(nil);
} else if ([@"uninitEngine" isEqualToString:call.method]) {
[self.voiceAsr uninitEngine];
result(nil);
} else if ([@"startOrStopEngine" isEqualToString:call.method]) {
[self.voiceAsr startEngineBtnClicked];
result(nil);
} else if ([@"prepareEnvironment" isEqualToString:call.method]) {
[VoiceAsr prepareEnvironment:call.arguments];
result(nil);
} else if ([@"ttsStartEngineBtnClick" isEqualToString:call.method]) {
[self.ttsNovel startEngineBtnClick:call.arguments[@"text"]];
result(nil);
} else if ([@"ttsSynthesis" isEqualToString:call.method]) {
[self.ttsNovel synthesis];
result(nil);
} else if ([@"ttsStopEngineBtnClicked" isEqualToString:call.method]) {
[self.ttsNovel stopEngineBtnClicked];
result(nil);
} else if ([@"ttsPausePlayback" isEqualToString:call.method]) {
[self.ttsNovel pausePlayback];
result(nil);
} else if ([@"ttsResumePlayback" isEqualToString:call.method]) {
[self.ttsNovel resumePlayback];
result(nil);
} else if ([@"ttsInitEngine" isEqualToString:call.method]) {
self.ttsNovelconfig = call.arguments;
[self.ttsNovel switchEngine];
result(nil);
} else if ([@"ttsUnInitEngine" isEqualToString:call.method]) {
[self.ttsNovel switchEngine];
result(nil);
} else if ([@"destoryTtsNovel" isEqualToString:call.method]) {
[self destoryTtsNovel];
result(nil);
} else if ([@"destoryAsrVoice" isEqualToString:call.method]) {
[self destoryVoiceAsr];
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)events {
self.eventSink = events;
return nil;
self.eventSink = events;
return nil;
}
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
self.eventSink = nil;
return nil;
self.eventSink = nil;
return nil;
}
// 新增的发送 Map<String, dynamic> 方法
- (void)sendMessageToFlutter:(NSDictionary*)message {
if (self.eventSink) {
self.eventSink(message);
}
if (self.eventSink) {
self.eventSink(message);
}
}
- (void) destoryVoiceAsr {
[self.voiceAsr destroyEngine];
_voiceAsr = nil;
}
- (void) destoryTtsNovel {
[self.ttsNovel destroyEngine];
_ttsNovel = nil;
}
- (VoiceAsr *)voiceAsr {
......@@ -76,11 +118,18 @@
}
return _voiceAsr;
}
- (TtsNovel *)ttsNovel {
if(!_ttsNovel) {
_ttsNovel = [[TtsNovel alloc] initWithDelegate:self config:self.ttsNovelconfig];
}
return _ttsNovel;
}
- (void)onRecieve:(nonnull NSDictionary *)message {
- (void)onRecieve:(nonnull NSDictionary *)message {
[self sendMessageToFlutter:message];
}
@end
......@@ -46,8 +46,8 @@ extern NSString* SDEF_AU_DEFAULT_URI;
extern NSString* SDEF_TTS_DEFAULT_URI;
extern NSString* SDEF_TTS_DEFAULT_CLUSTER;
extern NSString* SDEF_TTS_DEFAULT_BACKEND_CLUSTER;
//extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE;
//extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE;
extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE;
extern NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE;
extern NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE;
extern NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE_TYPE;
extern NSString* SDEF_TTS_DEFAULT_ONLINE_LANGUAGE;
......
......@@ -43,8 +43,8 @@ const NSString* SDEF_AU_DEFAULT_URI = @"/api/v1/sauc";
const NSString* SDEF_TTS_DEFAULT_URI = @"/api/v1/tts/ws_binary";
const NSString* SDEF_TTS_DEFAULT_CLUSTER = @"volcano_tts";
const NSString* SDEF_TTS_DEFAULT_BACKEND_CLUSTER = @"YOUR TTS BACKEND CLUSTER";
//const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE = @"灿灿";
//const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE = @"BV213_w5H18f6VbKnhg3Ph";
const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE = @"灿灿";
const NSString* SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE = @"BV002_streaming";
//BV002_streaming BV021_PSj8BvWAZyepfUPB BV705_streaming BV115_H74MBi790rUFu993 BV213_w5H18f6VbKnhg3Ph
const NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE = @"YOUR TTS OFFLINE VOICE";
const NSString* SDEF_TTS_DEFAULT_OFFLINE_VOICE_TYPE = @"YOUR TTS OFFLINE VOICE TYPE";
......
//
// TtsNovel.h
// aivoice_plugin
//
// Created by edy on 2024/9/11.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol TtsNovelDelegate <NSObject>
- (void)onRecieve:(NSDictionary*)message;
@end
@interface TtsNovel : NSObject
- (instancetype)initWithDelegate:(id<TtsNovelDelegate>)delegate
config:(NSDictionary *)config;
- (void)switchEngine;
- (void)startEngineBtnClick:(NSString *)text;
- (void)stopEngineBtnClicked;
- (void)synthesis;
- (void) destroyEngine;
- (void)pausePlayback;
- (void)resumePlayback;
@end
NS_ASSUME_NONNULL_END
//
// TtsNovel.m
// aivoice_plugin
//
// Created by edy on 2024/9/11.
//
#import "TtsNovel.h"
#include <CoreFoundation/CoreFoundation.h>
#include <objc/objc.h>
#import <SpeechEngineToB/SpeechEngine.h>
#import <AVFoundation/AVFoundation.h>
#import "FileUtils.h"
#import "SettingsHelper.h"
#import "SensitiveDefines.h"
#import <SpeechEngineToB/SpeechResourceManager.h>
static int TTS_MAX_RETRY_COUNT = 3;
@interface TtsNovel ()<TtsNovelDelegate>
// 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;
// Settings
@property (strong, nonatomic) Settings *settings;
// 一些在线合成的配置
@property (strong, nonatomic) NSString *ttsAppId;
@property (strong, nonatomic) NSString *ttsVoiceOnline;
@property (strong, nonatomic) NSString *ttsVoiceTypeOnline;
// 一些离线合成的配置
@property (strong, nonatomic) NSString *ttsVoiceOffline;
@property (strong, nonatomic) NSString *ttsVoiceTypeOffline;
// 小说模式相关
@property (assign, nonatomic) BOOL ttsSynthesisFromPlayer;
@property (assign, nonatomic) int ttsSynthesisIndex;
@property (assign, nonatomic) int ttsPlayingIndex;
@property (assign, nonatomic) double ttsPlayingProgress;
@property (strong, nonatomic) NSMutableArray* ttsSynthesisText;
@property (strong, nonatomic) NSMutableDictionary* ttsSynthesisMap;
@property (nonatomic, weak) id<TtsNovelDelegate> delegate;
@property (strong, nonatomic) NSDictionary *config;
@property (strong, nonatomic) NSString *textFromFlutter;
@property (assign, nonatomic) int ttsRetryCount;
@end
@implementation TtsNovel
- (instancetype)init {
return [self initWithDelegate:nil config:@{}];
}
- (instancetype)initWithDelegate:(id<TtsNovelDelegate>)delegate
config:(NSDictionary *)config {
self = [super init];
if (self) {
self.delegate = delegate;
self.config = config;
// 初始化和小说模式有关的字段
self.ttsSynthesisFromPlayer = FALSE;
self.ttsSynthesisIndex = 0;
self.ttsPlayingIndex = -1;
self.ttsPlayingProgress = 0.0;
self.ttsSynthesisText = [[NSMutableArray alloc] init];
self.ttsSynthesisMap = [[NSMutableDictionary alloc]init];
self.debugPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
self.ttsRetryCount = TTS_MAX_RETRY_COUNT;
NSLog(@"当前调试路径 %@", self.debugPath);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(audioInterruptionHandler:)
name:AVAudioSessionInterruptionNotification
object:nil];
self.settings = [SettingsHelper shareInstance].ttsSettings;
}
return self;
}
#pragma mark - Config & Init & Uninit Methods
-(void)configInitParams {
//【必需配置】Engine Name
[self.curEngine setStringParam:SE_TTS_ENGINE forKey:SE_PARAMS_KEY_ENGINE_NAME_STRING];
//【必需配置】Work Mode, 可选值如下
// SETtsWorkModeOnline, 只进行在线合成,不需要配置离线合成相关参数;
// SETtsWorkModeOffline, 只进行离线合成,不需要配置在线合成相关参数;
// SETtsWorkModeAlternate, 先发起在线合成,失败后(网络超时),启动离线合成引擎开始合成;
[self.curEngine setIntParam:[self getTtsWorkMode] forKey:SE_PARAMS_KEY_TTS_WORK_MODE_INT];
//【可选配置】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];
//【可选配置】合成出的音频的采样率,默认为 24000
[self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SAMPLE_RATE] forKey:SE_PARAMS_KEY_TTS_SAMPLE_RATE_INT];
//【可选配置】打断播放时使用多长时间淡出停止,单位:毫秒。默认值 0 表示不淡出
[self.curEngine setIntParam:[self.settings getInt:SETTING_AUDIO_FADEOUT_DURATION] forKey:SE_PARAMS_KEY_AUDIO_FADEOUT_DURATION_INT];
// ------------------------ 在线合成相关配置 -----------------------
// NSString* appid = [self.settings getString:SETTING_APPID];
self.ttsAppId = self.config[@"appId"];
//【必需配置】在线合成鉴权相关:Appid
[self.curEngine setStringParam:self.ttsAppId forKey:SE_PARAMS_KEY_APP_ID_STRING];
// NSString* token = [self.settings getString:SETTING_TOKEN];
NSString* ttsAppToken = self.config[@"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];
//【必需配置】语音合成服务Uri
NSString *uri = [self.settings getString:SETTING_URI];
NSString *ttsUri = uri.length > 0 ? uri : SDEF_TTS_DEFAULT_URI;
[self.curEngine setStringParam:ttsUri forKey:SE_PARAMS_KEY_TTS_URI_STRING];
//【必需配置】语音合成服务所用集群
NSString *cluster = SDEF_TTS_DEFAULT_CLUSTER;
[self.curEngine setStringParam:cluster forKey:SE_PARAMS_KEY_TTS_CLUSTER_STRING];
//【可选配置】在线合成下发的 opus-ogg 音频的压缩倍率
[self.curEngine setIntParam:10 forKey:SE_PARAMS_KEY_TTS_COMPRESSION_RATE_INT];
// ------------------------ 离线合成相关配置 -----------------------
// if ([self getTtsWorkMode] != SETtsWorkModeOnline && [self getTtsWorkMode] != SETtsWorkModeFile) {
// NSString* resourcePath = @"";
// if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) {
// resourcePath = [[SpeechResourceManager shareInstance] getModelPath];
// } else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) {
// NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME];
// resourcePath = [[SpeechResourceManager shareInstance] getModelPath:model_name];
// }
// NSLog(@"TTS resource root path: %@", resourcePath);
// //【必需配置】离线合成所需资源存放路径
// [self.curEngine setStringParam:resourcePath forKey:SE_PARAMS_KEY_TTS_OFF_RESOURCE_PATH_STRING];
// }
//
// //【必需配置】离线合成鉴权相关:证书文件存放路径
// [self.curEngine setStringParam:self.debugPath forKey:SE_PARAMS_KEY_LICENSE_DIRECTORY_STRING];
// NSString* authenticationType = [self getAuthenticationType];
// //【必需配置】Authenticate Type
// [self.curEngine setStringParam:authenticationType forKey:SE_PARAMS_KEY_AUTHENTICATE_TYPE_STRING];
// if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_PRE_BIND]) {
// // 按包名授权,获取到授权的 APP 可以不限次数、不限设备数的使用离线合成
// NSString *licenseName = [self.settings getString:SETTING_LICENSE_NAME];
// NSString *licenseBusiId = [self.settings getString:SETTING_LICENSE_BUSI_ID];
// // 证书名和业务 ID, 离线合成鉴权相关,使用火山提供的证书下发服务时为【必需配置】, 否则为【无需配置】
// // 证书名,用于下载按报名授权的证书文件
// [self.curEngine setStringParam:licenseName forKey:SE_PARAMS_KEY_LICENSE_NAME_STRING];
// // 业务 ID, 用于下载按报名授权的证书文件
// [self.curEngine setStringParam:licenseBusiId forKey:SE_PARAMS_KEY_LICENSE_BUSI_ID_STRING];
// } else if ([authenticationType isEqualToString:SE_AUTHENTICATE_TYPE_LATE_BIND]) {
// // 按装机量授权,不限制 APP 的包名和使用次数,但是限制使用离线合成的设备数量
// //【必需配置】离线合成鉴权相关:Authenticate Address
// [self.curEngine setStringParam:SDEF_AUTHENTICATE_ADDRESS forKey:SE_PARAMS_KEY_AUTHENTICATE_ADDRESS_STRING];
// //【必需配置】离线合成鉴权相关:Authenticate Uri
// [self.curEngine setStringParam:SDEF_AUTHENTICATE_URI forKey:SE_PARAMS_KEY_AUTHENTICATE_URI_STRING];
// NSString* curBusinessKey = [self.settings getString:SETTING_BUSINESS_KEY];
// NSString* curAuthenticateSecret = [self.settings getString:SETTING_AUTHENTICATE_SECRET];
// //【必需配置】离线合成鉴权相关:Business Key
// [self.curEngine setStringParam:curBusinessKey forKey:SE_PARAMS_KEY_BUSINESS_KEY_STRING];
// //【必需配置】离线合成鉴权相关:Authenticate Secret
// [self.curEngine setStringParam:curAuthenticateSecret forKey:SE_PARAMS_KEY_AUTHENTICATE_SECRET_STRING];
// }
}
-(void)configStartTtsParams {
//【必需配置】TTS 使用场景
[self.curEngine setStringParam:SE_TTS_SCENARIO_TYPE_NOVEL forKey:SE_PARAMS_KEY_TTS_SCENARIO_STRING];
// 准备待合成的小说文本
if(![self prepareNovelText]) {
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;
}
//【可选配置】是否使用 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 setIntParam:[self.settings getBool:SETTING_TTS_ENABLE_DATA_CALLBACK] ? SETtsDataCallbackModeAll : SETtsDataCallbackModeNone forKey:SE_PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT];
}
- (void)configSynthesisParams {
NSString* text = self.ttsSynthesisText[self.ttsSynthesisIndex];
NSLog(@"Synthesis: %d, text: %@", self.ttsSynthesisIndex, text);
//【必需配置】需合成的文本,不可超过 80 字
[self.curEngine setStringParam:text forKey:SE_PARAMS_KEY_TTS_TEXT_STRING];
//【可选配置】需合成的文本的类型,支持直接传文本(TTS_TEXT_TYPE_PLAIN)和传 SSML 形式(TTS_TEXT_TYPE_SSML)的文本
[self.curEngine setStringParam:[self getTtsTextType] forKey:SE_PARAMS_KEY_TTS_TEXT_TYPE_STRING];
//【可选配置】用于控制 TTS 音频的语速,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档
[self.curEngine setDoubleParam:[self.settings getInt:SETTING_TTS_SPEAK_SPEED] forKey:SE_PARAMS_KEY_TTS_SPEED_RATIO_DOUBLE];
//【可选配置】用于控制 TTS 音频的音量,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档
// [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_VOLUME] forKey:SE_PARAMS_KEY_TTS_VOLUME_RATIO_DOUBLE];
//【可选配置】用于控制 TTS 音频的音高,支持的配置范围参考火山官网 语音技术/语音合成/离在线语音合成SDK/参数说明 文档
// [self.curEngine setDoubleParam:[self.settings getDouble:SETTING_TTS_AUDIO_PITCH] forKey:SE_PARAMS_KEY_TTS_PITCH_RATIO_DOUBLE];
//【可选配置】是否在文本的每句结尾处添加静音段,单位:毫秒,默认为 0ms
[self.curEngine setIntParam:[self.settings getInt:SETTING_TTS_SILENCE_DURATION] forKey:SE_PARAMS_KEY_TTS_SILENCE_DURATION_INT];
// ------------------------ 在线合成相关配置 -----------------------
NSString *voiceOnline = SDEF_TTS_DEFAULT_ONLINE_VOICE;
self.ttsVoiceOnline = voiceOnline;
//【必需配置】在线合成使用的发音人代号
[self.curEngine setStringParam:self.ttsVoiceOnline forKey:SE_PARAMS_KEY_TTS_VOICE_ONLINE_STRING];
NSString *voiceTypeOnline = SDEF_TTS_DEFAULT_ONLINE_VOICE_TYPE;
self.ttsVoiceTypeOnline = voiceTypeOnline;
//【必需配置】在线合成使用的音色代号
[self.curEngine setStringParam:self.ttsVoiceTypeOnline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_ONLINE_STRING];
//【可选配置】是否打开在线合成的服务端缓存,默认关闭
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_ENABLE_CACHE] forKey:SE_PARAMS_KEY_TTS_ENABLE_CACHE_BOOL];
//【可选配置】指定在线合成的语种,默认为空,即不指定
[self.curEngine setStringParam:[self.settings getString:SETTING_TTS_ONLINE_LANGUAGE] forKey:SE_PARAMS_KEY_TTS_LANGUAGE_ONLINE_STRING];
//【可选配置】是否启用在线合成的情感预测功能
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_WITH_INTENT] forKey:SE_PARAMS_KEY_TTS_WITH_INTENT_BOOL];
//【可选配置】指定在线合成的情感,例如 happy, sad 等
[self.curEngine setStringParam:[self.settings getString:SETTING_TTS_EMOTION] forKey:SE_PARAMS_KEY_TTS_EMOTION_STRING];
//【可选配置】需要返回详细的播放进度或需要启用断点续播功能时应配置为 1, 否则配置为 0 或不配置
[self.curEngine setIntParam:1 forKey:SE_PARAMS_KEY_TTS_WITH_FRONTEND_INT];
//【可选配置】需要返回字粒度的播放进度时应配置为 simple, 同时要求 PARAMS_KEY_TTS_WITH_FRONTEND_INT 也配置为 1; 默认为空
// [self.curEngine setStringParam:[self.settings getBool:SETTING_TTS_ENABLE_WORD_LEVEL_PROGRESS_UPDATE] ? @"simple" : @"" forKey:SE_PARAMS_KEY_TTS_FRONTEND_TYPE_STRING];
//【可选配置】使用复刻音色
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_USE_VOICECLONE_VOICE] forKey:SE_PARAMS_KEY_TTS_USE_VOICECLONE_BOOL];
//【可选配置】在开启前述使用复刻音色的开关后,制定复刻音色所用的后端集群
[self.curEngine setStringParam:[self.settings getString:SETTING_TTS_BACKEND_CLUSTER] forKey:SE_PARAMS_KEY_TTS_BACKEND_CLUSTER_STRING];
// ------------------------ 离线合成相关配置 -----------------------
NSString *voiceOffline = [self.settings getString:SETTING_OFFLINE_VOICE];
if (voiceOffline.length <= 0) {
voiceOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE];
}
self.ttsVoiceOffline = voiceOffline;
//【必需配置】离线合成使用的发音人代号
[self.curEngine setStringParam:self.ttsVoiceOffline forKey:SE_PARAMS_KEY_TTS_VOICE_OFFLINE_STRING];
NSString *voiceTypeOffline = [self.settings getString:SETTING_OFFLINE_VOICE_TYPE];
if (voiceTypeOffline.length <= 0) {
voiceTypeOffline = [self.settings getOptionsValue:SETTING_OFFLINE_VOICE_TYPE];
}
self.ttsVoiceTypeOffline = voiceTypeOffline;
//【必需配置】离线合成使用的音色代号
[self.curEngine setStringParam:self.ttsVoiceTypeOffline forKey:SE_PARAMS_KEY_TTS_VOICE_TYPE_OFFLINE_STRING];
//【可选配置】是否降低离线合成的 CPU 利用率,默认关闭
// 打开该配置会使离线合成的实时率变大,仅当必要(例如为避免系统主动杀死CPU占用持续过高的进程)时才应开启
[self.curEngine setBoolParam:[self.settings getBool:SETTING_TTS_LIMIT_CPU_USAGE] forKey:SE_PARAMS_KEY_TTS_LIMIT_CPU_USAGE_BOOL];
}
- (void)initEngine {
// NSLog(@"获取设备ID,调试使用");
// AppDelegate *appDelegate = [ViewController getAppDelegate];
// if (appDelegate == nil) {
// appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// }
// [ViewController setAppDelegate:appDelegate];
// self.deviceID = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
// 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]);
if ([self getTtsWorkMode] == SETtsWorkModeOnline || [self getTtsWorkMode] == SETtsWorkModeFile) {
// 当使用纯在线模式时,不需要下载离线合成所需资源
[self initEngineInternal];
} else {
// [self.statusTextView setText:@"Waiting for loading model."];
// 下载离线合成所需资源需要区分多音色资源和单音色资源,下载这两种资源所调用的方法略有不同
if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"MultipleVoice"]) {
// 多音色资源是指一个资源文件中包含了多个离线音色,这种资源一般是旧版(V2)离线合成所用资源
NSLog(@"当前所用资源类别为多音色资源,开始准备多音色资源");
[self prepareMultipleVoiceResource];
} else if ([[self.settings getOptionsValue:SETTING_TTS_OFFLINE_RESOURCE_FORMAT] isEqual: @"SingleVoice"]) {
// 单音色资源是指一个资源文件仅包含一个离线音色,新版(V4 及以上)离线合成用的就是单音色资源
NSLog(@"当前所用资源类别为单音色资源,开始准备单音色资源");
[self prepareSingleVoiceResource];
}
}
}
- (void)prepareMultipleVoiceResource {
// 因为多音色资源的一个文件包含了多个音色,导致资源的名字和音色的名字无法一一对应
// 所以下载资源需要显式指定资源名字
NSString *model_name = [self.settings getString:SETTING_TTS_MODEL_NAME];
SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance];
NSLog(@"检查本地是否存在可用模型");
if (![speechResourceManager checkModelExist:model_name]) {
NSLog(@"本地没有模型,开始下载");
[self fetchMultipleVoiceResource:model_name];
} else {
NSLog(@"模型存在,检查是否需要更新模型");
[speechResourceManager checkModelVersion:model_name completion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) {
if (status != kSERSuccess || needUpdate == NO) {
NSLog(@"无需更新,直接使用本地已有模型。");
[self initEngineInternal];
} else {
NSLog(@"存在更新,开始下载模型");
[self fetchMultipleVoiceResource:model_name];
}
}];
}
}
- (void) fetchMultipleVoiceResource:(NSString*)model_name {
NSLog(@"需要下载的模型名为 %@", model_name);
SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance];
[speechResourceManager fetchModelByName:model_name completion:^(SEResourceStatus status, NSData* data) {
if (status == kSERSuccess) {
NSLog(@"下载成功");
[self initEngineInternal];
} else {
NSLog(@"下载失败,错误码: %d", status);
[self speechEngineInitFailed:kSERDownloadFailed];
}
}];
}
- (void) prepareSingleVoiceResource {
SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance];
NSString* offlineLanguage = [self.settings getString:SETTING_TTS_OFFLINE_LANGUAGE];
if (offlineLanguage.length <= 0) {
offlineLanguage = SDEF_TTS_DEFAULT_OFFLINE_LANGUAGE;
}
NSArray* ttsLanguageArray = @[offlineLanguage];
NSLog(@"需要下载的离线合成语种资源有: %@", ttsLanguageArray);
[speechResourceManager setTtsLanguage:ttsLanguageArray];
NSArray* needDownloadVoiceType = (NSArray *)SDEF_TTS_DEFAULT_DOWNLOAD_OFFLINE_VOICES();
NSArray* voiceTypeArray = [self.settings getOptions:SETTING_OFFLINE_VOICE_TYPE].optionsArray;
if (voiceTypeArray != nil && voiceTypeArray.count > 0) {
needDownloadVoiceType = voiceTypeArray;
}
NSLog(@"需要下载的离线合成音色资源有: %@", needDownloadVoiceType);
[speechResourceManager setTtsVoiceType:needDownloadVoiceType];
NSLog(@"检查本地是否存在可用模型");
if ([speechResourceManager checkModelExist]) {
NSLog(@"本地没有模型,开始下载");
[self fetchSingleVoiceResource];
} else {
NSLog(@"模型存在,检查是否需要更新模型");
[speechResourceManager checkModelVersion:^(SEResourceStatus status, BOOL needUpdate, NSData *data) {
if (status != kSERSuccess || needUpdate == NO) {
NSLog(@"无需更新,直接使用本地已有模型。");
[self initEngineInternal];
} else {
NSLog(@"存在更新,开始下载模型");
[self fetchSingleVoiceResource];
}
}];
}
}
- (void)fetchSingleVoiceResource {
SpeechResourceManager *speechResourceManager = [SpeechResourceManager shareInstance];
[speechResourceManager fetchModel:^(SEResourceStatus status, NSData* data) {
if (status == kSERSuccess) {
NSLog(@"下载成功");
[self initEngineInternal];
} else {
NSLog(@"下载失败,错误码: %d", status);
[self speechEngineInitFailed:kSERDownloadFailed];
}
}];
}
- (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(@"引擎析构完成");
}
}
- (void) destroyEngine {
[self.curEngine destroyEngine];
}
#pragma mark - UI Actions
- (void)switchEngine {
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 = FALSE;
[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];
}
}
- (void)synthesis {
[self triggerSynthesis];
}
- (void)startEngineBtnClick:(NSString *)text{
self.textFromFlutter = text;
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");
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveStartEngine];
if (SENoError != ret) {
NSString* message = [NSString stringWithFormat:@"发送启动引擎指令失败: %d", ret];
[self sendStartEngineDirectiveFailed:message];
}
}
}
}
- (void)stopEngineBtnClicked{
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);
}
#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 SETtsSynthesisBegin:
NSLog(@"Callback: 合成开始: %@", data);
[self speechStartSynthesis:data];
break;
case SETtsSynthesisEnd:
NSLog(@"Callback: 合成结束: %@", data);
[self speechFinishSynthesis:data];
break;
case SETtsStartPlaying:
NSLog(@"Callback: 播放开始: %@", data);
[self speechStartPlaying:data];
break;
case SETtsPlaybackProgress:
NSLog(@"Callback: 播放进度");
[self updatePlayingProgress:data];
break;
case SETtsFinishPlaying:
NSLog(@"Callback: 播放结束: %@", data);
[self speechFinishPlaying:data];
break;
case SETtsAudioData:
NSLog(@"Callback: 音频数据,长度 %lu 字节", (unsigned long)data.length);
[self speechTtsAudioData:data];
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 = FALSE;
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 synthesisNextSentence];
break;
case SEConnectTimeout:
case SEReceiveTimeout:
case SENetLibError:
// 遇到网络错误时建议重试,重试次数不超过 3 次
needStop = ![self retrySynthesis];
if (needStop) {
self.engineErrorOccurred = TRUE;
}
break;
default:
needStop = YES;
self.engineErrorOccurred = TRUE;
// [self.resultTextView
// setText:[[NSString alloc]
// initWithData:data
// encoding:NSUTF8StringEncoding]];
break;
}
} else {
needStop = YES;
}
if (needStop) {
[self.curEngine sendDirective:SEDirectiveStopEngine];
}
});
}
// 根据 SDK 返回的播放进度高亮正在播放的文本,用红色表示
// 根据 SDK 返回的合成开始和合成结束回调高亮正在合成的文本,用蓝色表示
-(void)updateTtsResultText:(NSString*) playingId {
if (self.engineErrorOccurred) {
NSLog(@"When a fatal error occurs, prevent the playback text from being displayed.");
return;
}
NSNumber* val = [self.ttsSynthesisMap objectForKey:playingId];
if (val != nil) {
self.ttsPlayingIndex = [val intValue];
}
int beginIndex = MAX(self.ttsPlayingIndex, 0);
int maxSentencesDisplayed = MIN((int)[self.ttsSynthesisText count], 16);
NSMutableAttributedString *resultStr = [[NSMutableAttributedString alloc] initWithString:@""];
for (int cnt = 0; cnt < maxSentencesDisplayed; ++cnt) {
int index = (beginIndex + cnt) % [self.ttsSynthesisText count];
NSString* current_sentence = self.ttsSynthesisText[index];
NSInteger playedPosition = 0;
if (index == self.ttsPlayingIndex) {
playedPosition = MIN(ceil((double)(self.ttsPlayingProgress) * (double)([current_sentence length])), [current_sentence length]);
NSLog(@"played position: %ld", (long)playedPosition);
NSString* playedString = [current_sentence substringToIndex:playedPosition];
NSAttributedString* playedSpan = [[NSAttributedString alloc] initWithString:playedString attributes:[NSDictionary dictionaryWithObject:[UIColor redColor] forKey:NSForegroundColorAttributeName]];
[resultStr appendAttributedString:playedSpan];
}
NSString* remainString = [current_sentence substringFromIndex:playedPosition];
NSAttributedString* span = [[NSAttributedString alloc] initWithString:remainString attributes:[NSDictionary dictionaryWithObject:[UIColor blackColor] forKey:NSForegroundColorAttributeName]];
[resultStr appendAttributedString:span];
}
// [self.resultTextView setAttributedText:resultStr];
}
- (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;
}
[self synthesisNextSentence];
}
- (void)speechStartPlaying:(NSData *)data {
NSString* playingId = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"TTS start playing: %@", playingId);
dispatch_async(dispatch_get_main_queue(), ^{
// self.pauseResumeButton.enabled = TRUE;
self.ttsPlayingProgress = 0.0;
// [self updateTtsResultText:playingId];
});
}
- (void)updatePlayingProgress :(NSData *)data {
if (data != nil) {
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
if(error) {
NSLog(@"Parse data as json error!");
return ;
}
if([object isKindOfClass:[NSDictionary class]]) {
NSDictionary *results = object;
float percentage = [[results valueForKey:@"progress"] floatValue];
NSString *reqid = [results valueForKey:@"reqid"];
NSLog(@"playing id: %@, progress in percent: %.2f", reqid, percentage);
dispatch_async(dispatch_get_main_queue(), ^{
self.ttsPlayingProgress = percentage;
[self updateTtsResultText:reqid];
});
}
}
}
- (void)speechFinishPlaying :(NSData *)data {
NSString* playingId = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"TTS finish playing: %@", playingId);
// if (![self.settings getBool:SETTING_TTS_ENABLE_DUMP_NOVEL_TTS_DETAIL]) {
// [self.ttsSynthesisMap removeObjectForKey:playingId];
// }
dispatch_async(dispatch_get_main_queue(), ^{
self.ttsPlayingProgress = 1.0;
[self updateTtsResultText:playingId];
});
if (self.ttsSynthesisFromPlayer) {
if( self.ttsSynthesisIndex == (self.ttsSynthesisText.count - 1)) {
[self stopEngineBtnClicked];
} else {
[self triggerSynthesis];
self.ttsSynthesisFromPlayer = FALSE;
}
}
}
- (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)synthesisNextSentence {
self.ttsSynthesisIndex = (self.ttsSynthesisIndex + 1) % [self.ttsSynthesisText count];
if (!self.ttsSynthesisFromPlayer) {
[self triggerSynthesis];
}
}
-(void)triggerSynthesis {
[self configSynthesisParams];
// DIRECTIVE_SYNTHESIS 是连续合成必需的一个指令,在成功调用 DIRECTIVE_START_ENGINE 之后,每次合成新的文本需要再调用 DIRECTIVE_SYNTHESIS 指令
// DIRECTIVE_SYNTHESIS 需要在当前没有正在合成的文本时才可以成功调用,否则就会报错 -901,可以在收到 MESSAGE_TYPE_TTS_SYNTHESIS_END 之后调用
// 当使用 SDK 内置的播放器时,为了避免缓存过多的音频导致内存占用过高,SDK 内部限制缓存的音频数量不超过 5 次合成的结果,
// 如果 DIRECTIVE_SYNTHESIS 后返回 -902, 就需要在下一次收到 MESSAGE_TYPE_TTS_FINISH_PLAYING 再去调用 MESSAGE_TYPE_TTS_FINISH_PLAYING
NSLog(@"触发合成");
NSLog(@"Directive: DIRECTIVE_SYNTHESIS");
SEEngineErrorCode ret = [self.curEngine sendDirective:SEDirectiveSynthesis];
if (ret != SENoError) {
NSLog(@"Synthesis faile: %d", ret);
if (ret == SESynthesisPlayerIsBusy) {
self.ttsSynthesisFromPlayer = TRUE;
} else {
NSString* message = [NSString stringWithFormat:@"发送合成指令失败: %d", ret];
[self sendSynthesisDirectiveFailed:message];
}
}
}
-(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.ttsPlayingIndex = -1;
self.ttsSynthesisFromPlayer = FALSE;
[self.ttsSynthesisText removeAllObjects];
[self.ttsSynthesisMap removeAllObjects];
}
-(BOOL)prepareNovelText {
[self resetTtsContext];
NSString* text = self.textFromFlutter;
if (self.ttsSynthesisText == nil || [self.ttsSynthesisText count] <= 0) {
// 使用下面几个标点符号来分句,会让通过 MESSAGE_TYPE_TTS_PLAYBACK_PROGRESS 返回的播放进度更加准确
NSArray* temp = [text componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@";!?。!?;…"]];
for (int j = 0; j < temp.count; ++j) {
[self addSentence:temp[j]];
}
}
NSLog(@"Synthesis text item num: %ld.", [self.ttsSynthesisText count]);
return [self.ttsSynthesisText count] > 0;
}
#pragma mark - Helper
- (NSString*)getTtsTextType {
switch ([self.settings getOptions:SETTING_TTS_TEXT_TYPE].chooseIdx) {
case 0:
return SE_TTS_TEXT_TYPE_PLAIN;
case 1:
return SE_TTS_TEXT_TYPE_SSML;
default:
break;
}
return SE_TTS_TEXT_TYPE_PLAIN;;
}
- (int)getTtsWorkMode {
switch ([self.settings getOptions:SETTING_TTS_WORK_MODE].chooseIdx) {
case 0:
return SETtsWorkModeOnline;
case 1:
return SETtsWorkModeOffline;
case 2:
return SETtsWorkModeAlternate;
default:
break;
}
return SETtsWorkModeOnline;;
}
- (NSString*)getAuthenticationType {
switch ([self.settings getOptions:SETTING_AUTHENTICATION_TYPE].chooseIdx) {
case 0:
return SE_AUTHENTICATE_TYPE_PRE_BIND;
case 1:
return SE_AUTHENTICATE_TYPE_LATE_BIND;
default:
break;
}
return SE_AUTHENTICATE_TYPE_PRE_BIND;
}
- (long)timeDelayFrom:(long)pastTimestamp {
return [[NSDate date] timeIntervalSince1970] * 1000 - pastTimestamp;
}
#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];
}
}
}
}
}
- (void)onRecieve:(nonnull NSDictionary *)message {
}
@end
......@@ -26,6 +26,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)uninitEngine;
- (void) destroyEngine;
- (void)startEngineBtnClicked;
- (void)stopEngineBtnClicked;
......
......@@ -148,6 +148,10 @@
}
}
- (void) destroyEngine {
[self.curEngine destroyEngine];
}
#pragma mark - Config & Init & Uninit Methods
-(void)configInitParams{
......
......@@ -36,4 +36,49 @@ class AivoicePlugin {
Stream<Map<String, dynamic>> get onAsrResultReceived {
return _eventChannel.receiveBroadcastStream().map((event) => Map<String, dynamic>.from(event));
}
// 更改方法名
Future<void> ttsStartEngineBtnClick(Map<String, dynamic> params) {
return AivoicePluginPlatform.instance.ttsStartEngineBtnClick(params);
}
// 新增的 ttsSynthesis 方法
Future<void> ttsSynthesis(Map<String, dynamic> params) {
return AivoicePluginPlatform.instance.ttsSynthesis(params);
}
// 修改方法名
Future<void> ttsStopEngineBtnClicked() {
return AivoicePluginPlatform.instance.ttsStopEngineBtnClicked();
}
// 新增的 ttsPausePlayback 方法
Future<void> ttsPausePlayback() {
return AivoicePluginPlatform.instance.ttsPausePlayback();
}
// 新增的 ttsResumePlayback 方法
Future<void> ttsResumePlayback() {
return AivoicePluginPlatform.instance.ttsResumePlayback();
}
// 新增的 ttsInitEngine 方法
Future<void> ttsInitEngine(Map<String, dynamic> config) {
return AivoicePluginPlatform.instance.ttsInitEngine(config);
}
// 新增的 ttsUnInitEngine 方法
Future<void> ttsUnInitEngine() {
return AivoicePluginPlatform.instance.ttsUnInitEngine();
}
// 新增的 destoryTtsNovel 方法
Future<void> destoryTtsNovel() {
return AivoicePluginPlatform.instance.destoryTtsNovel();
}
// 新增的 destoryAsrVoice 方法
Future<void> destoryAsrVoice() {
return AivoicePluginPlatform.instance.destoryAsrVoice();
}
}
......@@ -35,4 +35,49 @@ class MethodChannelAivoicePlugin extends AivoicePluginPlatform {
Future<void> prepareEnvironment(Map<String, dynamic> config) async {
await methodChannel.invokeMethod<void>('prepareEnvironment', config);
}
@override
Future<void> ttsStartEngineBtnClick(Map<String, dynamic> params) async {
await methodChannel.invokeMethod<void>('ttsStartEngineBtnClick', params);
}
@override
Future<void> ttsSynthesis(Map<String, dynamic> params) async {
await methodChannel.invokeMethod<void>('ttsSynthesis', params);
}
@override
Future<void> ttsStopEngineBtnClicked() async {
await methodChannel.invokeMethod<void>('ttsStopEngineBtnClicked');
}
@override
Future<void> ttsPausePlayback() async {
await methodChannel.invokeMethod<void>('ttsPausePlayback');
}
@override
Future<void> ttsResumePlayback() async {
await methodChannel.invokeMethod<void>('ttsResumePlayback');
}
@override
Future<void> ttsInitEngine(Map<String, dynamic> config) async {
await methodChannel.invokeMethod<void>('ttsInitEngine', config);
}
@override
Future<void> ttsUnInitEngine() async {
await methodChannel.invokeMethod<void>('ttsUnInitEngine');
}
@override
Future<void> destoryTtsNovel() async {
await methodChannel.invokeMethod<void>('destoryTtsNovel');
}
@override
Future<void> destoryAsrVoice() async {
await methodChannel.invokeMethod<void>('destoryAsrVoice');
}
}
......@@ -39,4 +39,49 @@ abstract class AivoicePluginPlatform extends PlatformInterface {
// 新增的 prepareEnvironment 方法
Future<void> prepareEnvironment(Map<String, dynamic> config);
// 新增的 ttsStartEngineBtnClick 方法
Future<void> ttsStartEngineBtnClick(Map<String, dynamic> params) {
throw UnimplementedError('ttsStartEngineBtnClick() has not been implemented.');
}
// 新增的 ttsSynthesis 方法
Future<void> ttsSynthesis(Map<String, dynamic> params) {
throw UnimplementedError('ttsSynthesis() has not been implemented.');
}
// 修改方法名
Future<void> ttsStopEngineBtnClicked() {
throw UnimplementedError('ttsStopEngineBtnClicked() has not been implemented.');
}
// 新增的 ttsPausePlayback 方法
Future<void> ttsPausePlayback() {
throw UnimplementedError('ttsPausePlayback() has not been implemented.');
}
// 新增的 ttsResumePlayback 方法
Future<void> ttsResumePlayback() {
throw UnimplementedError('ttsResumePlayback() has not been implemented.');
}
// 新增的 ttsInitEngine 方法
Future<void> ttsInitEngine(Map<String, dynamic> config) {
throw UnimplementedError('ttsInitEngine() has not been implemented.');
}
// 新增的 ttsUnInitEngine 方法
Future<void> ttsUnInitEngine() {
throw UnimplementedError('ttsUnInitEngine() has not been implemented.');
}
// 新增的 destoryTtsNovel 方法
Future<void> destoryTtsNovel() {
throw UnimplementedError('destoryTtsNovel() has not been implemented.');
}
// 新增的 destoryAsrVoice 方法
Future<void> destoryAsrVoice() {
throw UnimplementedError('destoryAsrVoice() has not been implemented.');
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论