|
|
@ -9,82 +9,84 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; |
|
|
class VolcanoTtsService extends GetxService implements TtsService { |
|
|
class VolcanoTtsService extends GetxService implements TtsService { |
|
|
/// 方法通道 |
|
|
/// 方法通道 |
|
|
static const MethodChannel _channel = MethodChannel('volcano_speech/tts'); |
|
|
static const MethodChannel _channel = MethodChannel('volcano_speech/tts'); |
|
|
|
|
|
|
|
|
/// 事件通道 |
|
|
/// 事件通道 |
|
|
static const EventChannel _eventChannel = EventChannel('volcano_speech/tts_events'); |
|
|
static const EventChannel _eventChannel = |
|
|
|
|
|
EventChannel('volcano_speech/tts_events'); |
|
|
|
|
|
|
|
|
/// 音频数据通道 |
|
|
/// 音频数据通道 |
|
|
static const EventChannel _audioDataChannel = EventChannel('volcano_speech/tts_audio_data'); |
|
|
static const EventChannel _audioDataChannel = |
|
|
|
|
|
EventChannel('volcano_speech/tts_audio_data'); |
|
|
|
|
|
|
|
|
/// 事件流控制器 |
|
|
/// 事件流控制器 |
|
|
final StreamController<TtsEvent> _eventStreamController = |
|
|
final StreamController<TtsEvent> _eventStreamController = |
|
|
StreamController<TtsEvent>.broadcast(); |
|
|
StreamController<TtsEvent>.broadcast(); |
|
|
|
|
|
|
|
|
/// 音频数据流控制器 |
|
|
/// 音频数据流控制器 |
|
|
final StreamController<Uint8List> _audioDataStreamController = |
|
|
final StreamController<Uint8List> _audioDataStreamController = |
|
|
StreamController<Uint8List>.broadcast(); |
|
|
StreamController<Uint8List>.broadcast(); |
|
|
|
|
|
|
|
|
/// 事件处理器 |
|
|
/// 事件处理器 |
|
|
StreamSubscription? _eventSubscription; |
|
|
StreamSubscription? _eventSubscription; |
|
|
|
|
|
|
|
|
/// 音频数据处理器 |
|
|
/// 音频数据处理器 |
|
|
StreamSubscription? _audioDataSubscription; |
|
|
StreamSubscription? _audioDataSubscription; |
|
|
|
|
|
|
|
|
/// 是否初始化完成 |
|
|
/// 是否初始化完成 |
|
|
bool _isInitialized = false; |
|
|
bool _isInitialized = false; |
|
|
|
|
|
|
|
|
/// 当前语音名称 |
|
|
/// 当前语音名称 |
|
|
String _currentVoice = "zh_female_qingxin"; |
|
|
String _currentVoice = "zh_female_qingxin"; |
|
|
|
|
|
|
|
|
/// 是否启用音频回调 |
|
|
/// 是否启用音频回调 |
|
|
bool _isAudioCallbackEnabled = false; |
|
|
bool _isAudioCallbackEnabled = false; |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
String get currentVoice => _currentVoice; |
|
|
String get currentVoice => _currentVoice; |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
bool get isSpeaking => _isSpeaking; |
|
|
bool get isSpeaking => _isSpeaking; |
|
|
|
|
|
|
|
|
/// 是否正在播放 |
|
|
/// 是否正在播放 |
|
|
bool _isSpeaking = false; |
|
|
bool _isSpeaking = false; |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Stream<TtsEvent> get onEvent => _eventStreamController.stream; |
|
|
Stream<TtsEvent> get onEvent => _eventStreamController.stream; |
|
|
|
|
|
|
|
|
/// 音频数据流 |
|
|
/// 音频数据流 |
|
|
Stream<Uint8List> get onAudioData => _audioDataStreamController.stream; |
|
|
Stream<Uint8List> get onAudioData => _audioDataStreamController.stream; |
|
|
|
|
|
|
|
|
/// 构造函数 |
|
|
/// 构造函数 |
|
|
VolcanoTtsService() { |
|
|
VolcanoTtsService() { |
|
|
_setupEventListeners(); |
|
|
_setupEventListeners(); |
|
|
_setupAudioDataListeners(); |
|
|
_setupAudioDataListeners(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 设置事件监听 |
|
|
/// 设置事件监听 |
|
|
void _setupEventListeners() { |
|
|
void _setupEventListeners() { |
|
|
_eventSubscription = _eventChannel.receiveBroadcastStream().listen( |
|
|
_eventSubscription = _eventChannel.receiveBroadcastStream().listen( |
|
|
(dynamic event) { |
|
|
(dynamic event) { |
|
|
if (event is! Map) return; |
|
|
if (event is! Map) return; |
|
|
|
|
|
|
|
|
final Map<dynamic, dynamic> eventMap = event; |
|
|
final Map<dynamic, dynamic> eventMap = event; |
|
|
final String? type = eventMap['type'] as String?; |
|
|
final String? type = eventMap['type'] as String?; |
|
|
|
|
|
|
|
|
switch (type) { |
|
|
switch (type) { |
|
|
case 'started': |
|
|
case 'started': |
|
|
_isSpeaking = true; |
|
|
_isSpeaking = true; |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.started)); |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.started)); |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case 'completed': |
|
|
case 'completed': |
|
|
_isSpeaking = false; |
|
|
_isSpeaking = false; |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.completed)); |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.completed)); |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case 'canceled': |
|
|
case 'canceled': |
|
|
_isSpeaking = false; |
|
|
_isSpeaking = false; |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.canceled)); |
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.canceled)); |
|
|
break; |
|
|
break; |
|
|
|
|
|
|
|
|
case 'error': |
|
|
case 'error': |
|
|
_isSpeaking = false; |
|
|
_isSpeaking = false; |
|
|
final errorCode = eventMap['errorCode'] as String? ?? "-1"; |
|
|
final errorCode = eventMap['errorCode'] as String? ?? "-1"; |
|
|
@ -105,7 +107,7 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
}, |
|
|
}, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 设置音频数据监听 |
|
|
/// 设置音频数据监听 |
|
|
void _setupAudioDataListeners() { |
|
|
void _setupAudioDataListeners() { |
|
|
_audioDataSubscription = _audioDataChannel.receiveBroadcastStream().listen( |
|
|
_audioDataSubscription = _audioDataChannel.receiveBroadcastStream().listen( |
|
|
@ -119,32 +121,34 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
}, |
|
|
}, |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> initialize({List<String>? supportedLanguages}) async { |
|
|
Future<bool> initialize({List<String>? supportedLanguages}) async { |
|
|
if (_isInitialized) return true; |
|
|
if (_isInitialized) return true; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 从.env文件读取配置 |
|
|
// 从.env文件读取配置 |
|
|
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|
|
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; |
|
|
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|
|
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; |
|
|
|
|
|
|
|
|
if (appId.isEmpty || appToken.isEmpty) { |
|
|
if (appId.isEmpty || appToken.isEmpty) { |
|
|
debugPrint('火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量'); |
|
|
debugPrint( |
|
|
|
|
|
'火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 初始化火山语音TTS |
|
|
// 初始化火山语音TTS |
|
|
final result = await _channel.invokeMethod<bool>('initialize', { |
|
|
final result = await _channel.invokeMethod<bool>('initialize', { |
|
|
'appId': appId, |
|
|
'appId': appId, |
|
|
'appToken': appToken, |
|
|
'appToken': appToken, |
|
|
}) ?? false; |
|
|
}) ?? |
|
|
|
|
|
false; |
|
|
|
|
|
|
|
|
if (!result) { |
|
|
if (!result) { |
|
|
debugPrint('火山语音TTS初始化失败'); |
|
|
debugPrint('火山语音TTS初始化失败'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
_isInitialized = true; |
|
|
_isInitialized = true; |
|
|
return true; |
|
|
return true; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
@ -160,16 +164,17 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
} |
|
|
} |
|
|
return true; |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> setVoice(String voiceName) async { |
|
|
Future<bool> setVoice(String voiceName) async { |
|
|
if (!await _ensureInitialized()) return false; |
|
|
if (!await _ensureInitialized()) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
final result = await _channel.invokeMethod<bool>('setVoice', { |
|
|
final result = await _channel.invokeMethod<bool>('setVoice', { |
|
|
'voice': voiceName, |
|
|
'voice': voiceName, |
|
|
}) ?? false; |
|
|
}) ?? |
|
|
|
|
|
false; |
|
|
|
|
|
|
|
|
if (result) { |
|
|
if (result) { |
|
|
_currentVoice = voiceName; |
|
|
_currentVoice = voiceName; |
|
|
} |
|
|
} |
|
|
@ -179,16 +184,17 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 设置是否启用音频数据回调 |
|
|
/// 设置是否启用音频数据回调 |
|
|
Future<bool> setEnableAudioCallback(bool enable) async { |
|
|
Future<bool> setEnableAudioCallback(bool enable) async { |
|
|
if (!await _ensureInitialized()) return false; |
|
|
if (!await _ensureInitialized()) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
final result = await _channel.invokeMethod<bool>('enableAudioCallback', { |
|
|
final result = await _channel.invokeMethod<bool>('enableAudioCallback', { |
|
|
'enable': enable, |
|
|
'enable': enable, |
|
|
}) ?? false; |
|
|
}) ?? |
|
|
|
|
|
false; |
|
|
|
|
|
|
|
|
if (result) { |
|
|
if (result) { |
|
|
_isAudioCallbackEnabled = enable; |
|
|
_isAudioCallbackEnabled = enable; |
|
|
} |
|
|
} |
|
|
@ -198,17 +204,18 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> speakOnce(String text) async { |
|
|
Future<bool> speakOnce(String text) async { |
|
|
if (!await _ensureInitialized()) return false; |
|
|
if (!await _ensureInitialized()) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 单次播放 |
|
|
// 单次播放 |
|
|
final result = await _channel.invokeMethod<bool>('speakOnce', { |
|
|
final result = await _channel.invokeMethod<bool>('speakOnce', { |
|
|
'text': text, |
|
|
'text': text, |
|
|
}) ?? false; |
|
|
}) ?? |
|
|
|
|
|
false; |
|
|
|
|
|
|
|
|
return result; |
|
|
return result; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
debugPrint('单次播放失败:$e'); |
|
|
debugPrint('单次播放失败:$e'); |
|
|
@ -216,17 +223,18 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> speakStream(String text) async { |
|
|
Future<bool> speakStream(String text) async { |
|
|
if (!await _ensureInitialized()) return false; |
|
|
if (!await _ensureInitialized()) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 流式添加文本 |
|
|
// 流式添加文本 |
|
|
final result = await _channel.invokeMethod<bool>('speakStream', { |
|
|
final result = await _channel.invokeMethod<bool>('speakStream', { |
|
|
'text': text, |
|
|
'text': text, |
|
|
}) ?? false; |
|
|
}) ?? |
|
|
|
|
|
false; |
|
|
|
|
|
|
|
|
return result; |
|
|
return result; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
debugPrint('流式播放失败:$e'); |
|
|
debugPrint('流式播放失败:$e'); |
|
|
@ -234,11 +242,11 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> flushStream() async { |
|
|
Future<bool> flushStream() async { |
|
|
if (!await _ensureInitialized()) return false; |
|
|
if (!await _ensureInitialized()) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 刷新流式文本 |
|
|
// 刷新流式文本 |
|
|
return await _channel.invokeMethod<bool>('flushStream') ?? false; |
|
|
return await _channel.invokeMethod<bool>('flushStream') ?? false; |
|
|
@ -247,11 +255,11 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<bool> stop() async { |
|
|
Future<bool> stop() async { |
|
|
if (!_isInitialized) return false; |
|
|
if (!_isInitialized) return false; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
return await _channel.invokeMethod<bool>('stop') ?? false; |
|
|
return await _channel.invokeMethod<bool>('stop') ?? false; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
@ -259,27 +267,33 @@ class VolcanoTtsService extends GetxService implements TtsService { |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
Future<void> dispose() async { |
|
|
Future<void> dispose() async { |
|
|
if (_isSpeaking) { |
|
|
if (_isSpeaking) { |
|
|
await stop(); |
|
|
await stop(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
await _eventSubscription?.cancel(); |
|
|
await _eventSubscription?.cancel(); |
|
|
await _audioDataSubscription?.cancel(); |
|
|
await _audioDataSubscription?.cancel(); |
|
|
|
|
|
|
|
|
if (!_eventStreamController.isClosed) { |
|
|
if (!_eventStreamController.isClosed) { |
|
|
await _eventStreamController.close(); |
|
|
await _eventStreamController.close(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (!_audioDataStreamController.isClosed) { |
|
|
if (!_audioDataStreamController.isClosed) { |
|
|
await _audioDataStreamController.close(); |
|
|
await _audioDataStreamController.close(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (_isInitialized) { |
|
|
if (_isInitialized) { |
|
|
await _channel.invokeMethod<bool>('release'); |
|
|
await _channel.invokeMethod<bool>('release'); |
|
|
_isInitialized = false; |
|
|
_isInitialized = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Future<bool> setAudioOutputDevice(int type) { |
|
|
|
|
|
// TODO: implement setAudioOutputDevice |
|
|
|
|
|
throw UnimplementedError(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|