import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'dart:io'; import 'dart:math' as math; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:audio_session/audio_session.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; /// 火山语音服务异常 class VolcanoTtsException implements Exception { final String message; VolcanoTtsException(this.message); @override String toString() => message; } /// 自定义音频源,用于从字节数组读取音频数据 class BytesAudioSource extends StreamAudioSource { final Uint8List _bytes; static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility BytesAudioSource(this._bytes); @override Future request([int? start, int? end]) async { start = start ?? 0; end = end ?? _bytes.length; try { final subData = _bytes.sublist(start, end); // 使用固定大小的块进行流式传输 final chunks = >[]; var offset = 0; while (offset < subData.length) { final chunkSize = math.min(_bufferSize, subData.length - offset); chunks.add(subData.sublist(offset, offset + chunkSize)); offset += chunkSize; } return StreamAudioResponse( sourceLength: _bytes.length, contentLength: subData.length, offset: start, stream: Stream.fromIterable(chunks), contentType: 'audio/mpeg', ); } catch (e) { print('BytesAudioSource request error: $e'); rethrow; } } @override Future get length => Future.value(_bytes.length); } /// 火山语音合成服务 class VolcanoTtsService extends GetxService { static const String _host = 'openspeech.bytedance.com'; static const String _apiUrl = 'wss://$_host/api/v1/tts/ws_binary'; // 配置参数 final String _appId; final String _token; final String _cluster; final String _voiceType; // 音频播放器 late AudioPlayer _audioPlayer; final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final isEnabled = true.obs; // 默认启用 final isPlaying = false.obs; StreamSubscription? _playbackEventSubscription; StreamSubscription? _playerStateSubscription; AudioSession? _audioSession; // WebSocket 相关 WebSocketChannel? _channel; bool _isDisposed = false; // 句子管理 String _pendingText = ''; final List _sentenceQueue = []; static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号 bool _isFetching = false; static const int _maxSegmentLength = 100; // 最大分段长度 static const int _optimalSegmentLength = 50; // 最佳分段长度 // WebSocket连接配置 static final Map _wsHeaders = { 'User-Agent': 'DeepVoice/1.0', 'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'Upgrade', 'Upgrade': 'websocket', 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Extensions': 'permessage-deflate', 'Origin': 'https://openspeech.bytedance.com', }; // 二进制协议头部字段定义 static const int PROTOCOL_VERSION = 0x1; // 0b0001 - 版本1 static const int HEADER_SIZE = 0x1; // 0b0001 - 4字节 static const int MESSAGE_TYPE = 0x1; // 0b0001 - full client request static const int MESSAGE_FLAGS = 0x0; // 0b0000 - 无特殊标记 static const int SERIALIZATION = 0x1; // 0b0001 - JSON static const int COMPRESSION = 0x0; // 0b0000 - 无压缩 static const int RESERVED = 0x0; // 0b0000 - 保留字段 // 构建二进制协议头 static Uint8List _buildHeader() { final header = ByteData(4); // 4字节的头部 // 第一个字节: [协议版本(4位) | 报头大小(4位)] header.setUint8(0, (PROTOCOL_VERSION << 4) | HEADER_SIZE); // 第二个字节: [消息类型(4位) | 消息标记(4位)] header.setUint8(1, (MESSAGE_TYPE << 4) | MESSAGE_FLAGS); // 第三个字节: [序列化方法(4位) | 压缩方法(4位)] header.setUint8(2, (SERIALIZATION << 4) | COMPRESSION); // 第四个字节: [保留字段(8位)] header.setUint8(3, RESERVED); return header.buffer.asUint8List(); } VolcanoTtsService() : _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', _token = dotenv.env['VOLCANO_TTS_TOKEN'] ?? '', _cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '', _voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? 'zh_male_M392_conversation_wvae_bigtts' { if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) { throw Exception('火山语音配置信息不完整,请检查环境变量'); } } @override Future onInit() async { super.onInit(); try { await _initAudioSession(); await _initAudioPlayer(); } catch (e, stackTrace) { print('初始化TTS服务失败: $e'); print('Stack trace: $stackTrace'); } } Future _initAudioSession() async { _audioSession = await AudioSession.instance; await _audioSession?.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playback, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.media, flags: AndroidAudioFlags.audibilityEnforced, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: true, )); // 确保音频会话激活并设置正确的音频模式 if (Platform.isAndroid) { await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation); } else { await _audioSession?.setActive(true); } } Future _initAudioPlayer() async { if (_isDisposed) return; _audioPlayer = AudioPlayer(); // 配置Android特定的播放参数 if (Platform.isAndroid) { await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.media, flags: AndroidAudioFlags.audibilityEnforced, )); } // 基本播放器配置 await _audioPlayer.setVolume(1.0); await _audioPlayer.setLoopMode(LoopMode.off); await _audioPlayer.setSpeed(1.0); // 设置音频缓冲配置,启用预加载以减少延迟 await _audioPlayer.setAudioSource( _playlist, initialPosition: Duration.zero, preload: true, ); _setupEventListeners(); } Future speak(String text) async { if (!isEnabled.value || text.trim().isEmpty) return; try { _pendingText += text; _extractFullSentences(); await _startPreloadIfNeeded(); } catch (e, stack) { print('语音合成失败: $e'); await _reinitializePlayer(); } } void _extractFullSentences() { if (_pendingText.isEmpty) return; final matches = _sentenceBreaks.allMatches(_pendingText).toList(); if (matches.isEmpty) { // 如果没有找到句子分隔符,按长度分割 _splitByLength(_pendingText); _pendingText = ''; return; } int lastPos = 0; String currentBatch = ''; for (final match in matches) { final end = match.end; final sentence = _pendingText.substring(lastPos, end).trim(); if (sentence.isEmpty) { lastPos = end; continue; } // 如果当前批次为空,直接添加句子 if (currentBatch.isEmpty) { currentBatch = sentence; } // 如果当前批次加上新句子不超过30字符,则合并 else if ((currentBatch + sentence).length <= 30) { currentBatch += sentence; } // 如果超过30字符,将当前批次加入队列,开始新批次 else { if (currentBatch.isNotEmpty) { _sentenceQueue.add(currentBatch); } currentBatch = sentence; } lastPos = end; } // 处理最后一个批次 if (currentBatch.isNotEmpty) { _sentenceQueue.add(currentBatch); } // 处理剩余的文本 if (lastPos < _pendingText.length) { final remaining = _pendingText.substring(lastPos).trim(); if (remaining.isNotEmpty) { if (remaining.length <= 30) { // 尝试与最后一个批次合并 if (_sentenceQueue.isNotEmpty && (_sentenceQueue.last + remaining).length <= 30) { _sentenceQueue[_sentenceQueue.length - 1] += remaining; } else { _sentenceQueue.add(remaining); } } else { _splitByLength(remaining); } } } _pendingText = ''; } void _splitByLength(String text) { if (text.isEmpty) return; int start = 0; while (start < text.length) { int end = math.min(start + 30, text.length); // 尝试在标点或空格处分割 if (end < text.length) { final spacePos = text.lastIndexOf(' ', end); final breakPos = _sentenceBreaks.firstMatch( text.substring(start, end) )?.end; if (breakPos != null && breakPos > start) { end = start + breakPos; } else if (spacePos > start) { end = spacePos; } } final segment = text.substring(start, end).trim(); if (segment.isNotEmpty) { // 尝试与前一个批次合并 if (_sentenceQueue.isNotEmpty && (_sentenceQueue.last + segment).length <= 30) { _sentenceQueue[_sentenceQueue.length - 1] += segment; } else { _sentenceQueue.add(segment); } } start = end; } } Future _startPreloadIfNeeded() async { if (_isFetching || _sentenceQueue.isEmpty) return; _isFetching = true; await _fetchNextSegment(); } Future _fetchNextSegment() async { if (_sentenceQueue.isEmpty) { _isFetching = false; return; } try { // 预加载多个片段以实现流畅播放 final segments = []; while (_sentenceQueue.isNotEmpty && segments.length < 2) { // 减少预加载数量以降低内存压力 segments.add(_sentenceQueue.removeAt(0)); } for (final segment in segments) { print('获取音频片段: $segment'); final audioData = await _synthesize(segment); if (audioData != null) { print('成功获取音频数据: ${audioData.length} bytes'); try { final audioSource = BytesAudioSource(audioData); await _playlist.add(audioSource); print('音频片段已添加到播放列表'); // 如果是第一个片段且播放器未在播放,开始播放 if (!_audioPlayer.playing && _playlist.length == 1) { // 确保音频会话处于激活状态 await _audioSession?.setActive(true); // 设置播放参数 await _audioPlayer.setVolume(1.0); await _audioPlayer.seek(Duration.zero, index: 0); // 开始播放 await _audioPlayer.play(); } } catch (e) { print('添加音频源失败: $e'); } } } // 如果队列还有内容,继续获取下一批 if (_sentenceQueue.isNotEmpty) { await Future.delayed(const Duration(milliseconds: 100)); // 添加延迟以避免过快请求 await _fetchNextSegment(); } else { _isFetching = false; // 检查是否还有未处理的文本 _extractFullSentences(); if (_sentenceQueue.isNotEmpty) { await _startPreloadIfNeeded(); } } } catch (e, stackTrace) { print('获取音频失败: $e'); print('Stack trace: $stackTrace'); _isFetching = false; } } Future _synthesize(String text) async { WebSocket? socket; StreamSubscription? subscription; try { final requestJson = { 'app': { 'appid': _appId, 'token': _token, 'cluster': _cluster, }, 'user': { 'uid': '${DateTime.now().millisecondsSinceEpoch}', }, 'audio': { 'voice_type': _voiceType, 'encoding': 'mp3', 'speed_ratio': 1.0, 'volume_ratio': 1.0, 'pitch_ratio': 1.0, 'language': 'zh', }, 'request': { 'reqid': '${DateTime.now().millisecondsSinceEpoch}', 'text': text, 'text_type': 'plain', 'operation': 'query', }, }; final jsonBytes = utf8.encode(json.encode(requestJson)); final header = _buildHeader(); final fullRequest = Uint8List(header.length + 4 + jsonBytes.length); fullRequest.setAll(0, header); final lengthBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big); fullRequest.setAll(header.length, lengthBytes.buffer.asUint8List()); fullRequest.setAll(header.length + 4, jsonBytes); final wsUrl = Uri.parse(_apiUrl); final completer = Completer(); socket = await WebSocket.connect( wsUrl.toString(), headers: { ..._wsHeaders, 'Authorization': 'Bearer;$_token', }, ).timeout( const Duration(seconds: 5), onTimeout: () { throw TimeoutException('WebSocket连接超时'); }, ); subscription = socket.listen( (response) { if (!completer.isCompleted && response is Uint8List) { if (response.length < 2) return; final messageType = response[1] >> 4; if (messageType == 0x0B) { final headerSize = response[0] & 0x0F; final payloadStart = headerSize * 4; if (response.length > payloadStart) { final audioData = response.sublist(payloadStart); completer.complete(audioData); } } else if (messageType == 0x0F) { try { final headerSize = response[0] & 0x0F; final payloadStart = headerSize * 4; if (response.length < payloadStart + 4) { throw VolcanoTtsException('错误响应数据不完整'); } final jsonData = response.sublist(payloadStart + 4); final jsonString = utf8.decode(jsonData, allowMalformed: true); if (jsonString.trim().startsWith('{')) { final errorMap = json.decode(jsonString); throw VolcanoTtsException(errorMap['error'] as String? ?? '未知错误'); } } catch (e) { if (e is VolcanoTtsException) rethrow; throw VolcanoTtsException('服务返回错误: $e'); } } } }, onError: (error) { if (!completer.isCompleted) { completer.completeError(error); } }, onDone: () { if (!completer.isCompleted) { completer.complete(null); } }, cancelOnError: false, ); socket.add(fullRequest); return await completer.future.timeout( const Duration(seconds: 10), onTimeout: () => null, ); } catch (e) { print('合成请求失败: $e'); return null; } finally { await subscription?.cancel(); await socket?.close(); } } Future _playAudio(Uint8List audioData) async { if (_isDisposed) { print('TTS服务已销毁,跳过播放'); return; } try { print('准备播放音频数据: ${audioData.length} bytes'); final audioSource = BytesAudioSource(audioData); print('清除播放列表'); await _playlist.clear(); print('添加新的音频源到播放列表'); await _playlist.add(audioSource); if (!_audioPlayer.playing) { print('设置音量和播放位置'); await _audioPlayer.setVolume(1.0); await _audioPlayer.seek(Duration.zero, index: 0); // 确保音频会话处于激活状态 await _audioSession?.setActive(true); print('等待50ms确保音频准备就绪'); await Future.delayed(const Duration(milliseconds: 50)); print('开始播放'); await _audioPlayer.play(); print('播放命令已发送'); } else { print('播放器已在播放中'); } } catch (e, stack) { print('播放音频失败: $e'); print('错误堆栈: $stack'); await _reinitializePlayer(); } } Future stop() async { try { print('停止播放'); await _audioPlayer.stop(); await _playlist.clear(); _pendingText = ''; _sentenceQueue.clear(); _isFetching = false; isPlaying.value = false; // 确保清空所有待处理的音频数据 try { // 取消所有正在进行的WebSocket连接 await _channel?.sink.close(); _channel = null; // 重置播放器状态 await _audioPlayer.pause(); await _audioPlayer.seek(Duration.zero); print('已清空所有待播放的文字和音频'); } catch (e) { print('清空音频缓存时出错: $e'); } } catch (e) { print('停止播放失败: $e'); } } // 添加一个方法来检查音频播放器是否真正在播放 bool isActuallyPlaying() { try { // 检查播放器状态 final isPlayerPlaying = _audioPlayer.playing; // 检查播放列表是否为空 final hasAudioSource = _playlist.length > 0; // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 final actuallyPlaying = isPlayerPlaying && hasAudioSource; // 如果状态不一致,记录日志 if (isPlaying.value != actuallyPlaying) { print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); } return actuallyPlaying; } catch (e) { print('检查实际播放状态失败: $e'); // 如果检查失败,返回当前的 isPlaying 值 return isPlaying.value; } } Future _reinitializePlayer() async { try { await stop(); await _audioPlayer.dispose(); // 重新初始化音频会话 await _initAudioSession(); _audioPlayer = AudioPlayer(); await _audioPlayer.setVolume(1.0); await _audioPlayer.setLoopMode(LoopMode.off); // 重新配置Android特定的播放参数 await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.media, flags: AndroidAudioFlags.audibilityEnforced, )); await _audioPlayer.setAudioSource( _playlist, initialPosition: Duration.zero, preload: true, ); _setupEventListeners(); print('播放器重新初始化成功'); } catch (e) { print('播放器重新初始化失败: $e'); } } void _setupEventListeners() { _playbackEventSubscription?.cancel(); _playerStateSubscription?.cancel(); _playbackEventSubscription = _audioPlayer.playbackEventStream.listen( (event) { if (_isDisposed) return; if (event.processingState == ProcessingState.completed) { _tryPlayNext(); } }, onError: (error) { if (_isDisposed) return; _reinitializePlayer(); }, ); _playerStateSubscription = _audioPlayer.playerStateStream.listen( (state) { if (_isDisposed) return; isPlaying.value = state.playing; if (state.processingState == ProcessingState.completed) { _tryPlayNext(); } }, onError: (error) { if (_isDisposed) return; _reinitializePlayer(); }, ); } Future _tryPlayNext() async { if (_isDisposed || _playlist.length <= 0) return; try { if (!_audioPlayer.playing) { // 确保音频会话处于激活状态 await _audioSession?.setActive(true); // 减少延迟时间 await Future.delayed(const Duration(milliseconds: 30)); if (!_isDisposed && !_audioPlayer.playing) { await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); } } } catch (e) { if (e is! StateError) { print('播放错误: $e'); await _reinitializePlayer(); } } } void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) { stop(); } } Future _cleanupResources() async { _isDisposed = true; await _playbackEventSubscription?.cancel(); await _playerStateSubscription?.cancel(); await _audioSession?.setActive(false); await _channel?.sink.close(); await stop(); await _audioPlayer.dispose(); } @override void onClose() async { print('关闭TTS服务'); await _cleanupResources(); super.onClose(); } }