You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
7.5 KiB
285 lines
7.5 KiB
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:get/get.dart';
|
|
import '../tts_service.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
/// 火山语音合成服务实现
|
|
class VolcanoTtsService extends GetxService implements TtsService {
|
|
/// 方法通道
|
|
static const MethodChannel _channel = MethodChannel('volcano_speech/tts');
|
|
|
|
/// 事件通道
|
|
static const EventChannel _eventChannel = EventChannel('volcano_speech/tts_events');
|
|
|
|
/// 音频数据通道
|
|
static const EventChannel _audioDataChannel = EventChannel('volcano_speech/tts_audio_data');
|
|
|
|
/// 事件流控制器
|
|
final StreamController<TtsEvent> _eventStreamController =
|
|
StreamController<TtsEvent>.broadcast();
|
|
|
|
/// 音频数据流控制器
|
|
final StreamController<Uint8List> _audioDataStreamController =
|
|
StreamController<Uint8List>.broadcast();
|
|
|
|
/// 事件处理器
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
/// 音频数据处理器
|
|
StreamSubscription? _audioDataSubscription;
|
|
|
|
/// 是否初始化完成
|
|
bool _isInitialized = false;
|
|
|
|
/// 当前语音名称
|
|
String _currentVoice = "zh_female_qingxin";
|
|
|
|
/// 是否启用音频回调
|
|
bool _isAudioCallbackEnabled = false;
|
|
|
|
@override
|
|
String get currentVoice => _currentVoice;
|
|
|
|
@override
|
|
bool get isSpeaking => _isSpeaking;
|
|
|
|
/// 是否正在播放
|
|
bool _isSpeaking = false;
|
|
|
|
@override
|
|
Stream<TtsEvent> get onEvent => _eventStreamController.stream;
|
|
|
|
/// 音频数据流
|
|
Stream<Uint8List> get onAudioData => _audioDataStreamController.stream;
|
|
|
|
/// 构造函数
|
|
VolcanoTtsService() {
|
|
_setupEventListeners();
|
|
_setupAudioDataListeners();
|
|
}
|
|
|
|
/// 设置事件监听
|
|
void _setupEventListeners() {
|
|
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
|
|
(dynamic event) {
|
|
if (event is! Map) return;
|
|
|
|
final Map<dynamic, dynamic> eventMap = event;
|
|
final String? type = eventMap['type'] as String?;
|
|
|
|
switch (type) {
|
|
case 'started':
|
|
_isSpeaking = true;
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.started));
|
|
break;
|
|
|
|
case 'completed':
|
|
_isSpeaking = false;
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.completed));
|
|
break;
|
|
|
|
case 'canceled':
|
|
_isSpeaking = false;
|
|
_eventStreamController.add(TtsEvent(type: TtsEventType.canceled));
|
|
break;
|
|
|
|
case 'error':
|
|
_isSpeaking = false;
|
|
final errorCode = eventMap['errorCode'] as String? ?? "-1";
|
|
final errorMessage = eventMap['errorMessage'] as String? ?? '未知错误';
|
|
_eventStreamController.add(TtsEvent(
|
|
type: TtsEventType.error,
|
|
error: '$errorMessage (错误码: $errorCode)',
|
|
));
|
|
break;
|
|
}
|
|
},
|
|
onError: (Object error) {
|
|
debugPrint('TTS事件流错误: $error');
|
|
_eventStreamController.add(TtsEvent(
|
|
type: TtsEventType.error,
|
|
error: '事件流错误: $error',
|
|
));
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 设置音频数据监听
|
|
void _setupAudioDataListeners() {
|
|
_audioDataSubscription = _audioDataChannel.receiveBroadcastStream().listen(
|
|
(dynamic data) {
|
|
if (data is Uint8List) {
|
|
_audioDataStreamController.add(data);
|
|
}
|
|
},
|
|
onError: (Object error) {
|
|
debugPrint('TTS音频数据流错误: $error');
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<bool> initialize({List<String>? supportedLanguages}) async {
|
|
if (_isInitialized) return true;
|
|
|
|
try {
|
|
// 从.env文件读取配置
|
|
final appId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
|
|
final appToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
|
|
|
|
if (appId.isEmpty || appToken.isEmpty) {
|
|
debugPrint('火山语音初始化失败:缺少VOLCANO_SPEECH_APP_ID或VOLCANO_SPEECH_APP_TOKEN环境变量');
|
|
return false;
|
|
}
|
|
|
|
// 初始化火山语音TTS
|
|
final result = await _channel.invokeMethod<bool>('initialize', {
|
|
'appId': appId,
|
|
'appToken': appToken,
|
|
}) ?? false;
|
|
|
|
if (!result) {
|
|
debugPrint('火山语音TTS初始化失败');
|
|
return false;
|
|
}
|
|
|
|
_isInitialized = true;
|
|
return true;
|
|
} catch (e) {
|
|
debugPrint('火山语音TTS初始化异常:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 检查初始化状态
|
|
Future<bool> _ensureInitialized() async {
|
|
if (!_isInitialized) {
|
|
return await initialize();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<bool> setVoice(String voiceName) async {
|
|
if (!await _ensureInitialized()) return false;
|
|
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('setVoice', {
|
|
'voice': voiceName,
|
|
}) ?? false;
|
|
|
|
if (result) {
|
|
_currentVoice = voiceName;
|
|
}
|
|
return result;
|
|
} catch (e) {
|
|
debugPrint('设置语音失败:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 设置是否启用音频数据回调
|
|
Future<bool> setEnableAudioCallback(bool enable) async {
|
|
if (!await _ensureInitialized()) return false;
|
|
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('enableAudioCallback', {
|
|
'enable': enable,
|
|
}) ?? false;
|
|
|
|
if (result) {
|
|
_isAudioCallbackEnabled = enable;
|
|
}
|
|
return result;
|
|
} catch (e) {
|
|
debugPrint('设置音频回调失败:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> speakOnce(String text) async {
|
|
if (!await _ensureInitialized()) return false;
|
|
|
|
try {
|
|
// 单次播放
|
|
final result = await _channel.invokeMethod<bool>('speakOnce', {
|
|
'text': text,
|
|
}) ?? false;
|
|
|
|
return result;
|
|
} catch (e) {
|
|
debugPrint('单次播放失败:$e');
|
|
_eventStreamController.add(TtsEvent.error('单次播放失败:$e'));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> speakStream(String text) async {
|
|
if (!await _ensureInitialized()) return false;
|
|
|
|
try {
|
|
// 流式添加文本
|
|
final result = await _channel.invokeMethod<bool>('speakStream', {
|
|
'text': text,
|
|
}) ?? false;
|
|
|
|
return result;
|
|
} catch (e) {
|
|
debugPrint('流式播放失败:$e');
|
|
_eventStreamController.add(TtsEvent.error('流式播放失败:$e'));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> flushStream() async {
|
|
if (!await _ensureInitialized()) return false;
|
|
|
|
try {
|
|
// 刷新流式文本
|
|
return await _channel.invokeMethod<bool>('flushStream') ?? false;
|
|
} catch (e) {
|
|
debugPrint('刷新流式文本失败:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<bool> stop() async {
|
|
if (!_isInitialized) return false;
|
|
|
|
try {
|
|
return await _channel.invokeMethod<bool>('stop') ?? false;
|
|
} catch (e) {
|
|
debugPrint('停止播放失败:$e');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> dispose() async {
|
|
if (_isSpeaking) {
|
|
await stop();
|
|
}
|
|
|
|
await _eventSubscription?.cancel();
|
|
await _audioDataSubscription?.cancel();
|
|
|
|
if (!_eventStreamController.isClosed) {
|
|
await _eventStreamController.close();
|
|
}
|
|
|
|
if (!_audioDataStreamController.isClosed) {
|
|
await _audioDataStreamController.close();
|
|
}
|
|
|
|
if (_isInitialized) {
|
|
await _channel.invokeMethod<bool>('release');
|
|
_isInitialized = false;
|
|
}
|
|
}
|
|
}
|