import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'dart:io' as io; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/io.dart'; import 'package:uuid/uuid.dart'; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import '../../core/utils/logger.dart'; /// 火山语音合成双向流式API服务 /// /// 提供与火山语音TTS服务的WebSocket交互功能 /// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession /// /// 基本使用流程: /// 1. 调用 startSession 开始会话 /// 2. 调用 synthesize 合成文本 /// 3. 调用 endSession 结束会话 /// /// 或者使用 speakSingle 一次性完成上述流程 class VolcanoTtsApiService extends GetxController { // 单例实例 static VolcanoTtsApiService? _instance; // 单例工厂方法 factory VolcanoTtsApiService() { _instance ??= VolcanoTtsApiService._internal(); return _instance!; } // 私有构造函数 VolcanoTtsApiService._internal() : _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', _token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '', _resourceId = dotenv.env['VOLCANO_TTS_RESOURCE_ID'] ?? '' { if (_appId.isEmpty || _token.isEmpty) { throw Exception('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID 和 VOLCANO_TTS_APP_TOKEN'); } } // 协议常量 static const int _protocolVersion = 0x0001; static const int _defaultHeaderSize = 0x0001; // 消息类型 static const int _fullClientRequest = 0x0001; static const int _audioOnlyResponse = 0x000B; static const int _fullServerResponse = 0x0009; static const int _errorInformation = 0x000F; // 消息类型特定标志 static const int _msgTypeFlagNoSeq = 0x0000; static const int _msgTypeFlagPositiveSeq = 0x0001; static const int _msgTypeFlagLastNoSeq = 0x0002; static const int _msgTypeFlagNegativeSeq = 0x0003; static const int _msgTypeFlagWithEvent = 0x0004; // 消息序列化 static const int _noSerialization = 0x0000; static const int _jsonSerialization = 0x0001; // 消息压缩 static const int _compressionNo = 0x0000; static const int _compressionGzip = 0x0001; // 事件类型 static const int eventNone = 0; static const int eventStartConnection = 1; static const int eventFinishConnection = 2; static const int eventConnectionStarted = 50; static const int eventConnectionFailed = 51; static const int eventConnectionFinished = 52; static const int eventStartSession = 100; static const int eventFinishSession = 102; static const int eventSessionStarted = 150; static const int eventSessionFinished = 152; static const int eventSessionFailed = 153; static const int eventTaskRequest = 200; static const int eventTtsSentenceStart = 350; static const int eventTtsSentenceEnd = 351; static const int eventTtsResponse = 352; // 服务URL static const String _serviceUrl = 'wss://openspeech.bytedance.com/api/v3/tts/bidirection'; // 配置参数 final String _appId; final String _token; final String _resourceId; // WebSocket连接 WebSocketChannel? _channel; // 会话ID String? _sessionId; String? _connectionId; // 状态控制 bool _isConnected = false; // 音频数据控制器 final _audioDataController = StreamController.broadcast(); Stream get audioStream => _audioDataController.stream; // 事件控制器 final _eventController = StreamController>.broadcast(); Stream> get eventStream => _eventController.stream; // 控制器状态 bool _isDisposed = false; // 当前状态 final isConnecting = false.obs; final isConnected = false.obs; final isSynthesizing = false.obs; // 音频播放器 final AudioPlayer _audioPlayer = AudioPlayer(); // 播放列表 - 用于连续播放 final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); bool _playlistInitialized = false; // 音频数据控制器 - 用于流式播放 final StreamController> _audioStreamController = StreamController>(); // 是否正在播放 final RxBool _isPlaying = false.obs; bool get isPlaying => _isPlaying.value; // 默认发音人 - 使用预定义的值 final String defaultSpeaker = 'zh_female_yuanqinvyou_moon_bigtts'; // 音频缓冲区 final List _audioBuffer = []; // 检查是否需要准备下一个音频项目 DateTime? _lastCheckTime; int? _lastCheckedIndex; @override void onInit() { super.onInit(); // 初始化时不自动连接,等待需要时再连接 // 设置音频播放器状态监听 _setupAudioPlayerListeners(); // 初始化播放列表 _initializePlaylist(); } /// 初始化播放列表 Future _initializePlaylist() async { try { if (!_playlistInitialized) { // 设置音频属性,启用无缝播放 await _audioPlayer.setLoopMode(LoopMode.off); await _audioPlayer.setShuffleModeEnabled(false); // 设置自动连续播放 await _audioPlayer.setAutomaticallyWaitsToMinimizeStalling(false); // 设置音频源 await _audioPlayer.setAudioSource(_playlist, initialIndex: 0, initialPosition: Duration.zero); _playlistInitialized = true; if (kDebugMode) { print('播放列表初始化成功'); } } } catch (e) { if (kDebugMode) { print('初始化播放列表失败: $e'); } // 不抛出异常,避免中断整个流程 } } /// 设置音频播放器状态监听 void _setupAudioPlayerListeners() { _audioPlayer.playerStateStream.listen((state) { final processingState = state.processingState; final playing = state.playing; if (processingState == ProcessingState.ready && playing) { _isPlaying.value = true; } else { _isPlaying.value = false; } }); _audioPlayer.processingStateStream.listen((state) { if (state == ProcessingState.completed) { // 播放完成后检查播放列表是否为空 if (_playlist.length > 0) { final currentIndex = _audioPlayer.currentIndex; // 自动播放下一个项目 _advanceToNextItem(); } else { // 播放列表为空,标记为未播放状态 _isPlaying.value = false; } } }); // 监听序列状态变化 _audioPlayer.sequenceStateStream.listen((sequenceState) { if (sequenceState == null) return; // 如果当前是最后一个项目且播放已完成,标记播放完成 if (sequenceState.currentIndex == sequenceState.sequence.length - 1 && _audioPlayer.processingState == ProcessingState.completed) { _isPlaying.value = false; } }); // 监听位置变化,但减少检查频率 DateTime _lastPositionCheck = DateTime.now(); _audioPlayer.positionStream.listen((position) { // 限制检查频率,每500毫秒检查一次 final now = DateTime.now(); if (now.difference(_lastPositionCheck).inMilliseconds >= 500) { _lastPositionCheck = now; _checkForNextItem(position); } }); } /// 检查是否需要准备下一个音频项目 Future _checkForNextItem(Duration position) async { // 只有在播放中且有下一个项目时才检查 if (!_isPlaying.value || _audioPlayer.currentIndex == null) return; final currentIndex = _audioPlayer.currentIndex!; final sequenceState = _audioPlayer.sequenceState; // 如果没有序列状态,则不需要准备 if (sequenceState == null) return; // 防抖动:如果在短时间内(200毫秒)对同一个索引进行了检查,则跳过 final now = DateTime.now(); if (_lastCheckTime != null && _lastCheckedIndex == currentIndex && now.difference(_lastCheckTime!) < const Duration(milliseconds: 200)) { return; } // 更新最后检查时间和索引 _lastCheckTime = now; _lastCheckedIndex = currentIndex; // 获取当前项目的总时长 final duration = _audioPlayer.duration; if (duration == null) return; // 如果是最后一个项目,不需要预加载 final isLastItem = currentIndex >= sequenceState.sequence.length - 1; // 如果接近结束(剩余时间小于200毫秒),准备下一个项目或完成播放 if (duration - position <= const Duration(milliseconds: 200) && !isLastItem) { // 让播放器自然过渡到下一个项目 } } /// 前进到播放列表中的下一个项目 Future _advanceToNextItem() async { try { if (_playlist.length == 0) return; // 获取当前索引和序列状态 final currentIndex = _audioPlayer.currentIndex; final sequenceState = _audioPlayer.sequenceState; if (sequenceState == null) { // 如果没有序列状态,直接开始播放 await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); return; } // 如果当前索引无效,从头开始播放 if (currentIndex == null || currentIndex < 0) { await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); return; } // 计算下一个索引 final nextIndex = currentIndex + 1; // 检查是否还有下一个项目 if (nextIndex < sequenceState.sequence.length) { // 跳转到下一个项目并开始播放 await _audioPlayer.seek(Duration.zero, index: nextIndex); await _audioPlayer.play(); } else { // 已经是最后一个项目 // 确保最后一个项目播放完成 if (_audioPlayer.position < _audioPlayer.duration!) { await _audioPlayer.seek(_audioPlayer.duration!); } } } catch (e) { // 简单记录错误,不做复杂处理 } } /// 连接到火山语音服务 Future connect() async { if (_isConnected) { Logger.info('TTS已连接,跳过连接'); return true; // 已经连接 } isConnecting.value = true; try { Logger.info('开始TTS连接'); // 生成连接ID _connectionId = const Uuid().v4(); // 创建WebSocket连接 final uri = Uri.parse(_serviceUrl); if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) { // 移动平台和桌面平台 - 使用IOWebSocketChannel _channel = IOWebSocketChannel.connect( uri, headers: { 'X-Api-App-Key': _appId, 'X-Api-Access-Key': _token, 'X-Api-Resource-Id': _resourceId, 'X-Api-Connect-Id': _connectionId!, }, ); } else { // Web平台 - 使用WebSocketChannel // 注意:在Web平台上,我们无法直接设置WebSocket头部 // 这可能会导致认证失败,需要与服务提供商确认Web平台的认证方式 _channel = WebSocketChannel.connect(uri); } // 监听消息 _channel!.stream.listen( _handleMessage, onError: (error) { // 直接处理错误,不尝试重连 Logger.error('WebSocket错误: $error'); if (!_isDisposed && !_eventController.isClosed) { _eventController.add({ 'eventType': 'error', 'error': error.toString() }); } }, onDone: _handleDone, cancelOnError: false, ); // 发送开始连接事件 await _startConnection(); // 等待连接成功 final completer = Completer(); // 设置超时 Timer? timeout; timeout = Timer(const Duration(seconds: 10), () { if (!completer.isCompleted) { Logger.error('火山TTS连接超时'); completer.complete(false); // 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常 try { if (!_isDisposed && !_eventController.isClosed) { _eventController.add({ 'eventType': 'connectionFailed', 'error': '连接超时' }); } } catch (e) { Logger.error('处理TTS连接超时事件失败: $e'); } } timeout = null; }); // 监听连接事件 final subscription = eventStream.listen((event) { if (event['eventType'] == 'connectionStarted') { if (!completer.isCompleted) { Logger.info('火山TTS连接成功'); completer.complete(true); timeout?.cancel(); timeout = null; } } else if (event['eventType'] == 'connectionFailed') { if (!completer.isCompleted) { Logger.error('火山TTS连接失败: ${event['error']}'); completer.complete(false); timeout?.cancel(); timeout = null; } } }); // 等待连接结果 final result = await completer.future; subscription.cancel(); if (result) { _isConnected = true; isConnected.value = true; Logger.info('火山TTS连接成功完成'); } return result; } catch (e) { // 只记录错误,不尝试重连 Logger.error('连接过程中发生异常: $e'); return false; } finally { isConnecting.value = false; } } /// 开始TTS会话 /// /// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession /// /// [speaker] 发音人ID /// /// 返回会话是否成功开始 Future startSession(String speaker) async { Logger.info('开始TTS会话'); try { if (!_isConnected) { final connected = await connect(); Logger.info('TTS未连接状态, 开始连接, 连接结果: $connected'); if (!connected) return false; } // 生成会话ID _sessionId = const Uuid().v4().replaceAll('-', ''); // 发送开始会话事件 await _startTtsSession(speaker); // 等待会话开始 final completer = Completer(); // 设置超时 Timer? timeout; timeout = Timer(const Duration(seconds: 10), () { if (!completer.isCompleted) { Logger.error('火山TTS会话开始超时'); completer.complete(false); // 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常 try { _eventController.add({ 'eventType': 'sessionFailed', 'error': '会话开始超时' }); } catch (e) { Logger.error('处理TTS会话超时事件失败: $e'); } } timeout = null; }); // 监听会话事件 final subscription = eventStream.listen((event) { if (event['eventType'] == 'sessionStarted') { if (!completer.isCompleted) { Logger.info('火山TTS会话开始成功'); completer.complete(true); timeout?.cancel(); timeout = null; } } else if (event['eventType'] == 'sessionFailed') { if (!completer.isCompleted) { Logger.error('火山TTS会话开始失败: ${event['error']}'); completer.complete(false); timeout?.cancel(); timeout = null; } } }); // 等待会话结果 final result = await completer.future; subscription.cancel(); if (result) { Logger.info('火山TTS会话已完全建立'); } return result; } catch (e) { // 只记录错误,不尝试恢复 Logger.error('开始TTS会话失败: $e'); return false; } } /// 合成文本 Future synthesize(String text, String speaker) async { if (text.isEmpty) { Logger.warning('合成文本为空,跳过合成'); return false; } try { // 确保已连接 if (!_isConnected) { Logger.error('TTS未连接,无法合成文本'); return false; } // 确保会话已开始 if (_sessionId == null) { Logger.error('TTS会话未开始,请先调用startSession'); return false; } // 设置合成状态 isSynthesizing.value = true; // 发送合成请求 await _sendTtsMessage(speaker, text); return true; } catch (e) { _handleError(e); return false; } } /// 结束会话 /// /// 结束当前TTS会话,释放相关资源 /// 用户应在使用完毕后调用此方法 /// /// 返回会话是否成功结束 Future endSession() async { if (_sessionId == null) { return true; // 没有活跃会话 } Logger.info('TTS结束会话'); try { // 发送结束会话事件 await _finishSession(); // 等待会话结束 final completer = Completer(); // 设置超时 final timeout = Timer(const Duration(seconds: 5), () { if (!completer.isCompleted) { completer.complete(true); // 超时也认为成功 } }); // 监听会话事件 final subscription = eventStream.listen((event) { if (event['eventType'] == 'sessionFinished') { if (!completer.isCompleted) { completer.complete(true); timeout.cancel(); } } }); // 等待会话结果 final result = await completer.future; subscription.cancel(); // 清除会话ID _sessionId = null; isSynthesizing.value = false; return result; } catch (e) { _handleError(e); return false; } } /// 断开连接 Future disconnect() async { if (!_isConnected) { return true; // 已经断开 } print('断开TTS连接'); try { // 如果有活跃会话,先结束会话 if (_sessionId != null) { await endSession(); } // 发送结束连接事件 await _finishConnection(); // 等待连接结束 final completer = Completer(); // 设置超时 final timeout = Timer(const Duration(seconds: 5), () { if (!completer.isCompleted) { completer.complete(true); // 超时也认为成功 } }); // 监听连接事件 final subscription = eventStream.listen((event) { if (event['eventType'] == 'connectionFinished') { if (!completer.isCompleted) { completer.complete(true); timeout.cancel(); } } }); // 等待连接结果 final result = await completer.future; subscription.cancel(); // 关闭WebSocket连接 await _channel?.sink.close(); _channel = null; _isConnected = false; _sessionId = null; isConnected.value = false; isSynthesizing.value = false; return result; } catch (e) { _handleError(e); return false; } } /// 处理接收到的消息 void _handleMessage(dynamic message) { try { if (message is! List) { _handleError(Exception('收到非二进制消息: $message')); return; } // 转换为Uint8List以提高处理效率 final messageData = Uint8List.fromList(message); final response = _parseResponse(messageData); if (response == null) { _handleError(Exception('解析响应失败')); return; } // 处理事件 final event = response['event'] as int?; if (event != null && !_isDisposed && !_eventController.isClosed) { switch (event) { case eventConnectionStarted: _eventController.add({'eventType': 'connectionStarted'}); break; case eventConnectionFailed: _eventController.add({ 'eventType': 'connectionFailed', 'error': response['errorDetails'] ?? '连接失败' }); break; case eventSessionStarted: _eventController.add({'eventType': 'sessionStarted'}); break; case eventSessionFailed: _eventController.add({ 'eventType': 'sessionFailed', 'error': response['errorDetails'] ?? '会话失败' }); break; case eventTtsSentenceStart: _eventController.add({'eventType': 'ttsSentenceStart'}); _prepareAudioStream(); break; case eventTtsSentenceEnd: _eventController.add({'eventType': 'ttsSentenceEnd'}); // 结束音频流 _finishAudioStream(); break; case eventTtsResponse: // 处理音频数据 final payload = response['payload'] as Uint8List?; if (payload != null && payload.isNotEmpty) { try { _eventController.add({'eventType': 'ttsResponse'}); if (!_isDisposed && !_audioDataController.isClosed) { _audioDataController.add(payload); } // 将音频数据添加到流中 _addAudioData(payload); } catch (audioError) { // 继续处理,不中断整个流程 } } break; case eventSessionFinished: _eventController.add({'eventType': 'sessionFinished'}); break; case eventConnectionFinished: _eventController.add({'eventType': 'connectionFinished'}); break; default: _eventController.add({'eventType': 'unknown', 'eventCode': event}); break; } } } catch (e) { _handleError(e); } } /// 准备音频流 void _prepareAudioStream() { try { // 清空音频缓冲区 _audioBuffer.clear(); } catch (e) { if (kDebugMode) { print('准备音频流时发生错误: $e'); } } } /// 添加音频数据到流 void _addAudioData(Uint8List data) { try { // 将音频数据添加到缓冲区 if (data.isNotEmpty) { _audioBuffer.add(data); } } catch (e) { if (kDebugMode) { print('添加音频数据到流时发生错误: $e'); } // 不抛出异常,继续处理 } } /// 结束音频流 void _finishAudioStream() { try { if (_audioBuffer.isNotEmpty) { // 合并所有音频数据 final totalSize = _audioBuffer.fold(0, (sum, data) => sum + data.length); final mergedData = Uint8List(totalSize); int offset = 0; for (final data in _audioBuffer) { mergedData.setRange(offset, offset + data.length, data); offset += data.length; } // 清空缓冲区 _audioBuffer.clear(); // 将合并后的数据添加到播放列表 _addToPlaylist(mergedData); } else { } } catch (e) { if (kDebugMode) { print('结束音频流时发生错误: $e'); } // 不抛出异常,避免中断整个流程 } } /// 将音频数据添加到播放列表 Future _addToPlaylist(Uint8List audioData) async { try { if (audioData.isEmpty) return; // 确保播放列表已初始化 if (!_playlistInitialized) { await _initializePlaylist(); } // 创建音频源 final audioSource = BytesAudioSource(audioData); // 添加到播放列表 await _playlist.add(audioSource); // 如果当前没有播放,开始播放 if (!_isPlaying.value) { await _startPlayback(); } else if (_audioPlayer.processingState == ProcessingState.completed) { // 如果播放已完成但状态还没更新,手动前进到下一个 await _advanceToNextItem(); } } catch (e) { // 直接报告错误,不尝试恢复 if (!_isDisposed && !_eventController.isClosed) { _eventController.add({ 'eventType': 'playbackError', 'error': '添加音频到播放列表失败: $e' }); } } } /// 开始播放收集到的音频数据 Future _startPlayback() async { try { if (!_playlistInitialized || _playlist.length == 0) { return; } // 检查当前播放状态 final processingState = _audioPlayer.processingState; // 如果已经播放完成,需要重新设置位置到第一个未播放的项目 if (processingState == ProcessingState.completed) { // 找到第一个未播放的项目 int nextIndex = 0; if (_audioPlayer.currentIndex != null) { nextIndex = math.min(_audioPlayer.currentIndex! + 1, _playlist.length - 1); } // 跳转到下一个项目 await _audioPlayer.seek(Duration.zero, index: nextIndex); } // 开始播放 await _audioPlayer.play(); } catch (e) { // 直接报告错误,不尝试恢复 if (!_isDisposed && !_eventController.isClosed) { _eventController.add({ 'eventType': 'playbackError', 'error': '开始播放失败: $e' }); } } } /// 处理错误 void _handleError(dynamic error) { try { // 只记录错误,不尝试恢复 if (!_isDisposed && !_eventController.isClosed) { _eventController.add({ 'eventType': 'error', 'error': error.toString() }); } Logger.error('火山语音TTS错误: $error'); } catch (e) { // 如果在处理错误时发生异常,记录到日志但不再抛出异常 Logger.error('处理TTS错误时发生异常: $e,原始错误: $error'); } } /// 处理连接关闭 void _handleDone() { Logger.info('TTS WebSocket连接已关闭'); // 重置所有状态 _isConnected = false; _sessionId = null; isConnected.value = false; isSynthesizing.value = false; // 通知监听者连接已断开 if (!_isDisposed && !_eventController.isClosed) { _eventController.add({'eventType': 'disconnected'}); } } /// 解析响应 Map? _parseResponse(Uint8List data) { try { if (data.length < 4) { return null; } // 解析头部 final header = { 'protocol_version': (data[0] >> 4) & 0x0F, 'header_size': data[0] & 0x0F, 'message_type': (data[1] >> 4) & 0x0F, 'message_type_specific_flags': data[1] & 0x0F, 'serialization_method': (data[2] >> 4) & 0x0F, 'message_compression': data[2] & 0x0F, 'reserved': data[3], }; final result = { 'header': header, }; int offset = 4; // 解析事件 if (header['message_type_specific_flags'] == _msgTypeFlagWithEvent) { if (data.length < offset + 4) { return result; } final event = _bytesToInt(data.sublist(offset, offset + 4)); result['event'] = event; offset += 4; } // 根据消息类型和事件类型解析剩余数据 final messageType = header['message_type']; final event = result['event'] as int?; if (messageType == _fullServerResponse || messageType == _audioOnlyResponse) { if (event != null) { switch (event) { case eventConnectionStarted: if (data.length < offset + 4) break; final connectionIdSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + connectionIdSize) break; final connectionId = String.fromCharCodes(data.sublist(offset, offset + connectionIdSize)); result['connectionId'] = connectionId; offset += connectionIdSize; break; case eventConnectionFailed: if (data.length < offset + 4) break; final metaSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + metaSize) break; final metaJson = String.fromCharCodes(data.sublist(offset, offset + metaSize)); result['meta'] = metaJson; offset += metaSize; try { final meta = jsonDecode(metaJson); result['errorDetails'] = meta['error_msg'] ?? 'Unknown error'; } catch (_) {} break; case eventSessionStarted: case eventSessionFailed: case eventSessionFinished: // 读取会话ID if (data.length < offset + 4) break; final sessionIdSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + sessionIdSize) break; final sessionId = String.fromCharCodes(data.sublist(offset, offset + sessionIdSize)); result['sessionId'] = sessionId; offset += sessionIdSize; // 读取元数据 if (data.length < offset + 4) break; final metaSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + metaSize) break; final metaJson = String.fromCharCodes(data.sublist(offset, offset + metaSize)); result['meta'] = metaJson; offset += metaSize; try { final meta = jsonDecode(metaJson); if (meta['error_msg'] != null) { result['errorDetails'] = meta['error_msg']; } } catch (_) {} break; default: // 读取会话ID if (data.length < offset + 4) break; final sessionIdSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + sessionIdSize) break; final sessionId = String.fromCharCodes(data.sublist(offset, offset + sessionIdSize)); result['sessionId'] = sessionId; offset += sessionIdSize; // 读取序列号 if (header['message_type_specific_flags'] == _msgTypeFlagNegativeSeq || header['message_type_specific_flags'] == _msgTypeFlagPositiveSeq) { if (data.length < offset + 4) break; final sequence = _bytesToInt(data.sublist(offset, offset + 4)); result['sequence'] = sequence; offset += 4; } // 读取负载 if (data.length < offset + 4) break; final payloadSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + payloadSize) break; final payload = data.sublist(offset, offset + payloadSize); result['payload'] = payload; if (messageType == _fullServerResponse) { try { final payloadStr = String.fromCharCodes(payload); result['payloadJson'] = payloadStr; } catch (_) {} } break; } } } else if (messageType == _errorInformation) { if (data.length < offset + 4) { return result; } final errorCode = _bytesToInt(data.sublist(offset, offset + 4)); result['errorCode'] = errorCode; offset += 4; // 读取负载 if (data.length < offset + 4) { return result; } final payloadSize = _bytesToInt(data.sublist(offset, offset + 4)); offset += 4; if (data.length < offset + payloadSize) { return result; } final payload = data.sublist(offset, offset + payloadSize); try { final payloadStr = String.fromCharCodes(payload); result['errorDetails'] = payloadStr; } catch (_) {} } return result; } catch (e) { if (kDebugMode) { print('解析响应失败: $e'); } return null; } } /// 开始连接 Future _startConnection() async { final header = _createHeader( _fullClientRequest, _msgTypeFlagWithEvent, _jsonSerialization, _compressionNo ); final optional = _createOptional(eventStartConnection, null, -1); final payload = utf8.encode('{}'); await _sendEvent(header, optional, payload); } /// 结束连接 Future _finishConnection() async { final header = _createHeader( _fullClientRequest, _msgTypeFlagWithEvent, _jsonSerialization, _compressionNo ); final optional = _createOptional(eventFinishConnection, null, -1); final payload = utf8.encode('{}'); await _sendEvent(header, optional, payload); } /// 开始TTS会话 Future _startTtsSession(String speaker) async { final header = _createHeader( _fullClientRequest, _msgTypeFlagWithEvent, _jsonSerialization, _compressionNo ); final optional = _createOptional(eventStartSession, _sessionId, -1); // 创建会话参数 final payloadMap = { 'user': {'uid': '123456'}, 'event': eventStartSession, 'namespace': 'BidirectionalTTS', 'req_params': { 'speaker': speaker, 'audio_params': { 'format': 'mp3', 'sample_rate': 24000 } } }; final payload = utf8.encode(jsonEncode(payloadMap)); await _sendEvent(header, optional, payload); } /// 发送TTS消息 Future _sendTtsMessage(String speaker, String text) async { final header = _createHeader( _fullClientRequest, _msgTypeFlagWithEvent, _jsonSerialization, _compressionNo ); final optional = _createOptional(eventTaskRequest, _sessionId, -1); // 创建任务参数 final payloadMap = { 'user': {'uid': '123456'}, 'event': eventTaskRequest, 'namespace': 'BidirectionalTTS', 'req_params': { 'text': text, 'speaker': speaker, 'audio_params': { 'format': 'mp3', 'sample_rate': 24000 } } }; final payload = utf8.encode(jsonEncode(payloadMap)); await _sendEvent(header, optional, payload); } /// 结束会话 Future _finishSession() async { final header = _createHeader( _fullClientRequest, _msgTypeFlagWithEvent, _jsonSerialization, _compressionNo ); final optional = _createOptional(eventFinishSession, _sessionId, -1); final payload = utf8.encode('{}'); await _sendEvent(header, optional, payload); } /// 创建头部 Uint8List _createHeader( int messageType, int messageTypeSpecificFlags, int serializationMethod, int messageCompression ) { return Uint8List.fromList([ // Protocol version | Header size ((_protocolVersion << 4) | _defaultHeaderSize) & 0xFF, // Message type | Message type specific flags ((messageType << 4) | messageTypeSpecificFlags) & 0xFF, // Serialization method | Compression method ((serializationMethod << 4) | messageCompression) & 0xFF, // Reserved 0 ]); } /// 创建可选部分 Uint8List _createOptional(int event, String? sessionId, int sequence) { final buffer = BytesBuilder(); // 添加事件 if (event != eventNone) { buffer.add(_intToBytes(event)); } // 添加会话ID if (sessionId != null) { final sessionIdBytes = utf8.encode(sessionId); buffer.add(_intToBytes(sessionIdBytes.length)); buffer.add(sessionIdBytes); } // 添加序列号 if (sequence >= 0) { buffer.add(_intToBytes(sequence)); } return buffer.toBytes(); } /// 发送事件 Future _sendEvent(Uint8List header, Uint8List optional, List payload) async { if (_channel == null) { throw Exception('WebSocket未连接'); } final buffer = BytesBuilder(); // 添加头部 buffer.add(header); // 添加可选部分 if (optional.isNotEmpty) { buffer.add(optional); } // 添加负载大小 buffer.add(_intToBytes(payload.length)); // 添加负载 buffer.add(payload); // 发送数据 _channel!.sink.add(buffer.toBytes()); } /// 整数转字节数组 Uint8List _intToBytes(int value) { return Uint8List(4) ..[0] = (value >> 24) & 0xFF ..[1] = (value >> 16) & 0xFF ..[2] = (value >> 8) & 0xFF ..[3] = value & 0xFF; } /// 字节数组转整数 int _bytesToInt(Uint8List bytes) { if (bytes.length != 4) { throw ArgumentError('字节数组长度必须为4'); } return ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); } /// 释放资源 @override void onClose() async { _isDisposed = true; await disconnect(); // 关闭控制器 if (!_audioDataController.isClosed) { await _audioDataController.close(); } if (!_eventController.isClosed) { await _eventController.close(); } // 释放资源 _audioPlayer.dispose(); super.onClose(); } /// 合成并播放文本 Future speak(String text, {String? speaker}) async { if (text.isEmpty) return false; try { // 使用默认发音人 final actualSpeaker = speaker ?? defaultSpeaker; // 合成文本 final success = await synthesize(text, actualSpeaker); return success; } catch (e) { _handleError(e); return false; } } /// 停止播放 Future stop() async { try { // 停止音频播放 await _audioPlayer.stop(); // 清空播放列表 await _playlist.clear(); // 结束当前会话 await endSession(); // 清空音频缓冲区 _audioBuffer.clear(); } catch (e) { _handleError(e); } } /// 检查是否正在播放 bool isActuallyPlaying() { return _isPlaying.value; } /// 检查会话是否活跃 bool isSessionActive() { return _sessionId != null; } /// 设置音量 Future setVolume(double volume) async { try { await _audioPlayer.setVolume(volume.clamp(0.0, 1.0)); } catch (e) { _handleError(e); } } /// 设置播放速度 Future setSpeed(double speed) async { try { await _audioPlayer.setSpeed(speed.clamp(0.5, 2.0)); } catch (e) { _handleError(e); } } /// 切换启用状态 void toggleEnabled() { // 这个方法在当前实现中不需要特殊处理 } /// 单句合成辅助函数 /// /// 一次性完成连接、会话创建、合成和播放的过程 /// 适合单句或少量文本的快速合成场景 /// /// [text] 要合成的文本 /// [speaker] 发音人,如果为null则使用默认发音人 /// /// 返回合成是否成功 Future speakSingle(String text, {String? speaker}) async { if (text.isEmpty) { Logger.warning('合成文本为空,跳过合成'); return false; } try { // 使用默认发音人 final actualSpeaker = speaker ?? defaultSpeaker; // 1. 确保连接 if (!_isConnected) { Logger.info('TTS未连接,开始连接'); final connected = await connect(); if (!connected) { Logger.error('TTS连接失败,无法合成文本'); return false; } } // 2. 确保会话已开始 final bool needNewSession = _sessionId == null; if (needNewSession) { Logger.info('TTS会话未开始,开始会话'); final sessionStarted = await startSession(actualSpeaker); if (!sessionStarted) { Logger.error('TTS会话开始失败,无法合成文本'); return false; } } // 3. 合成文本 isSynthesizing.value = true; final success = await synthesize(text, actualSpeaker); // 如果是我们开始的新会话,则结束会话 if (needNewSession) { await endSession(); } return success; } catch (e) { _handleError(e); return false; } } } /// 使用 BytesAudioSource 进行音频播放 /// 这种方式更简单可靠,适合已经收集完整的音频数据 class BytesAudioSource extends StreamAudioSource { final Uint8List _bytes; final String _contentType; static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility BytesAudioSource(List bytes, {String contentType = 'audio/mpeg'}) : _bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes), _contentType = contentType; @override Future request([int? start, int? end]) async { try { start = start ?? 0; end = end ?? _bytes.length; // 确保范围有效 final int validStart = math.min(math.max(0, start), _bytes.length); final int validEnd = math.min(math.max(validStart, end), _bytes.length); // 提取请求的数据范围 final data = _bytes.sublist(validStart, validEnd); if (kDebugMode && data.length > 1024) { print('BytesAudioSource 请求数据: $validStart-$validEnd (${data.length} 字节)'); } // 使用固定大小的块进行流式传输,提高兼容性 final chunks = >[]; var offset = 0; while (offset < data.length) { final chunkSize = math.min(_bufferSize, data.length - offset); chunks.add(data.sublist(offset, offset + chunkSize)); offset += chunkSize; } return StreamAudioResponse( sourceLength: _bytes.length, contentLength: data.length, offset: validStart, stream: Stream.fromIterable(chunks), contentType: _contentType, ); } catch (e) { if (kDebugMode) { print('BytesAudioSource 请求处理错误: $e'); } // 返回一个空响应而不是抛出异常,提高容错性 return StreamAudioResponse( sourceLength: _bytes.length, contentLength: 0, offset: 0, stream: Stream.value(Uint8List(0)), contentType: _contentType, ); } } @override Future get length => Future.value(_bytes.length); }