import 'dart:async'; 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:audio_session/audio_session.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/services.dart'; /// 火山语音服务异常 class VolcanoTtsException implements Exception { final String message; final dynamic originalError; VolcanoTtsException(this.message, [this.originalError]); @override String toString() => originalError != null ? '$message (原始错误: $originalError)' : message; } /// 自定义音频源,用于从字节数组读取音频数据 class BytesAudioSource extends StreamAudioSource { final Uint8List _bytes; static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility BytesAudioSource(this._bytes); // 添加 PCM 头信息,转换为 WAV 格式 Uint8List _addWavHeader(Uint8List pcmData) { // PCM 参数 const int sampleRate = 16000; // 采样率 const int numChannels = 1; // 单声道 const int bitsPerSample = 16; // 16位采样 // 计算数据大小 final int dataSize = pcmData.length; final int fileSize = 36 + dataSize; // 创建 WAV 头 final ByteData header = ByteData(44); // RIFF 头 header.setUint8(0, 'R'.codeUnitAt(0)); header.setUint8(1, 'I'.codeUnitAt(0)); header.setUint8(2, 'F'.codeUnitAt(0)); header.setUint8(3, 'F'.codeUnitAt(0)); // 文件大小 header.setUint32(4, fileSize, Endian.little); // WAVE 标识 header.setUint8(8, 'W'.codeUnitAt(0)); header.setUint8(9, 'A'.codeUnitAt(0)); header.setUint8(10, 'V'.codeUnitAt(0)); header.setUint8(11, 'E'.codeUnitAt(0)); // fmt 子块 header.setUint8(12, 'f'.codeUnitAt(0)); header.setUint8(13, 'm'.codeUnitAt(0)); header.setUint8(14, 't'.codeUnitAt(0)); header.setUint8(15, ' '.codeUnitAt(0)); // 子块大小 header.setUint32(16, 16, Endian.little); // 音频格式 (PCM = 1) header.setUint16(20, 1, Endian.little); // 声道数 header.setUint16(22, numChannels, Endian.little); // 采样率 header.setUint32(24, sampleRate, Endian.little); // 字节率 = 采样率 * 声道数 * 采样位数 / 8 header.setUint32(28, sampleRate * numChannels * bitsPerSample ~/ 8, Endian.little); // 块对齐 = 声道数 * 采样位数 / 8 header.setUint16(32, numChannels * bitsPerSample ~/ 8, Endian.little); // 采样位数 header.setUint16(34, bitsPerSample, Endian.little); // data 子块 header.setUint8(36, 'd'.codeUnitAt(0)); header.setUint8(37, 'a'.codeUnitAt(0)); header.setUint8(38, 't'.codeUnitAt(0)); header.setUint8(39, 'a'.codeUnitAt(0)); // 数据大小 header.setUint32(40, dataSize, Endian.little); // 创建完整的 WAV 数据 final Uint8List wavData = Uint8List(44 + dataSize); wavData.setRange(0, 44, header.buffer.asUint8List()); wavData.setRange(44, 44 + dataSize, pcmData); return wavData; } @override Future request([int? start, int? end]) async { // 添加 WAV 头信息 final Uint8List wavData = _addWavHeader(_bytes); start = start ?? 0; end = end ?? wavData.length; try { final subData = wavData.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: wavData.length, contentLength: subData.length, offset: start, stream: Stream.fromIterable(chunks), contentType: 'audio/wav', ); } catch (e) { print('BytesAudioSource request error: $e'); rethrow; } } @override Future get length => Future.value(_bytes.length + 44); // PCM 数据长度 + WAV 头长度 } /// 火山语音合成服务 (SDK版本) class VolcanoTtsService extends GetxService { // 平台通道 static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_tts'); // 配置参数 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 _playingStateController = StreamController.broadcast(); Stream get playingStateStream => _playingStateController.stream; StreamSubscription? _playbackEventSubscription; StreamSubscription? _playerStateSubscription; AudioSession? _audioSession; // 句子管理 String _pendingText = ''; final List _sentenceQueue = []; // 优化的句子分割正则表达式,包含更多中英文标点 static final _sentenceBreaks = RegExp(r'[。!?.!?;;::,,、]'); // 优化的句子分割参数 static const int _maxSegmentLength = 150; // 最大分段长度 static const int _optimalSegmentLength = 80; // 最佳分段长度 static const int _minSegmentLength = 5; // 最小分段长度 bool _isFetching = false; int _consecutiveErrorCount = 0; static const int _maxConsecutiveErrors = 3; // 初始化状态 bool _isInitialized = false; bool _isDisposed = false; // 性能优化参数 final int _preloadCount = 2; // 预加载片段数量 final int _maxRetryAttempts = 1; // 不进行重试 // 音频质量参数 final double _defaultVolume = 1.0; final double _defaultSpeed = 1.0; VolcanoTtsService() : // 使用新的环境变量命名 _appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '', _token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '', _cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '', _voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? '' { if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) { throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID, VOLCANO_TTS_APP_KEY 和 VOLCANO_TTS_CLUSTER'); } } @override Future onInit() async { super.onInit(); try { await _initAudioSession(); await _initAudioPlayer(); await _initializeTtsEngine(); } catch (e, stackTrace) { print('初始化TTS服务失败: $e'); print('Stack trace: $stackTrace'); } } /// 初始化TTS引擎 Future _initializeTtsEngine() async { try { print('开始初始化火山语音TTS引擎,APP_ID: ${_appId.substring(0, math.min(3, _appId.length))}***,APP_KEY: ${_token.length > 10 ? "${_token.substring(0, 5)}..." : _token}'); final result = await _channel.invokeMethod('initialize', { 'appId': _appId, 'token': _token, 'cluster': _cluster, }); _isInitialized = result ?? false; print('火山语音TTS引擎初始化${_isInitialized ? '成功' : '失败'}'); if (!_isInitialized) { throw VolcanoTtsException('TTS引擎初始化失败'); } } catch (e) { print('初始化火山语音TTS引擎失败: $e'); _isInitialized = false; throw VolcanoTtsException('初始化失败', e); } } Future _initAudioSession() async { _audioSession = await AudioSession.instance; // 针对不同平台优化音频会话配置 if (Platform.isAndroid) { await _audioSession?.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playback, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.media, flags: AndroidAudioFlags.audibilityEnforced, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: true, )); } else if (Platform.isIOS) { await _audioSession?.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playback, avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.duckOthers, avAudioSessionMode: AVAudioSessionMode.spokenAudio, avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation, )); } // 确保音频会话激活并设置正确的音频模式 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(_defaultVolume); await _audioPlayer.setLoopMode(LoopMode.off); await _audioPlayer.setSpeed(_defaultSpeed); // 设置音频缓冲配置,启用预加载以减少延迟 await _audioPlayer.setAudioSource( _playlist, initialPosition: Duration.zero, preload: true, ); _setupEventListeners(); } /// 合成并播放文本 /// /// [text] 要合成的文本 /// [voiceType] 可选的语音类型,如果提供,将覆盖默认语音类型 Future speak(String text, {String? voiceType}) async { if (!isEnabled.value || text.trim().isEmpty) return; try { // 如果提供了语音类型,记录日志 if (voiceType != null && voiceType != _voiceType) { print('使用临时语音类型: $voiceType (默认: $_voiceType)'); } _pendingText += text; _extractSentences(); await _startPreloadIfNeeded(voiceType: voiceType); } catch (e, stack) { print('语音合成失败: $e'); print('Stack trace: $stack'); _consecutiveErrorCount++; if (_consecutiveErrorCount >= _maxConsecutiveErrors) { print('连续错误次数过多,尝试重新初始化播放器'); await _reinitializePlayer(); _consecutiveErrorCount = 0; } } } /// 优化的文本分段算法 void _extractSentences() { 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; } // 如果当前批次加上新句子不超过最佳长度,则合并 else if ((currentBatch + sentence).length <= _optimalSegmentLength) { currentBatch += sentence; } // 如果超过最佳长度,将当前批次加入队列,开始新批次 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 <= _optimalSegmentLength) { // 尝试与最后一个批次合并 if (_sentenceQueue.isNotEmpty && (_sentenceQueue.last + remaining).length <= _optimalSegmentLength) { _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 + _optimalSegmentLength, text.length); // 如果没有到达文本末尾,尝试在标点或空格处分割 if (end < text.length) { // 在理想长度附近查找标点 int searchStart = math.max(start + _minSegmentLength, end - 20); int searchEnd = math.min(text.length, end + 20); String searchText = text.substring(searchStart, searchEnd); final breakMatch = _sentenceBreaks.firstMatch(searchText); if (breakMatch != null) { end = searchStart + breakMatch.end; } else { // 如果没有找到标点,尝试在空格处分割 final spacePos = text.lastIndexOf(' ', end); if (spacePos > start && spacePos > end - 20) { end = spacePos + 1; // 包含空格 } } } final segment = text.substring(start, end).trim(); if (segment.isNotEmpty) { // 尝试与前一个批次合并 if (_sentenceQueue.isNotEmpty && (_sentenceQueue.last + segment).length <= _optimalSegmentLength) { _sentenceQueue[_sentenceQueue.length - 1] += segment; } else { _sentenceQueue.add(segment); } } start = end; } } Future _startPreloadIfNeeded({String? voiceType}) async { if (_isFetching || _sentenceQueue.isEmpty) return; _isFetching = true; await _fetchNextSegment(voiceType: voiceType); } Future _fetchNextSegment({String? voiceType}) async { if (_sentenceQueue.isEmpty) { _isFetching = false; return; } try { // 预加载多个片段以实现流畅播放 final segments = []; while (_sentenceQueue.isNotEmpty && segments.length < _preloadCount) { segments.add(_sentenceQueue.removeAt(0)); } for (final segment in segments) { print('获取音频片段: $segment'); try { // 使用提供的语音类型或默认语音类型,只尝试一次 final audioData = await _synthesize(segment, voiceType: voiceType); if (audioData != null) { print('成功获取音频数据: ${audioData.length} bytes'); try { final audioSource = BytesAudioSource(audioData); await _playlist.add(audioSource); print('音频片段已添加到播放列表'); // 如果是第一个片段且播放器未在播放,开始播放 if (!_audioPlayer.playing && _playlist.length == 1) { print('开始播放'); await _audioPlayer.play(); } } catch (e) { print('添加音频到播放列表失败: $e'); } } else { print('获取音频失败: 合成返回空数据'); } } catch (e) { print('获取音频失败: $e'); // 不重试,继续处理下一个片段 } } } catch (e) { print('预加载音频片段失败: $e'); } finally { _isFetching = false; } } /// 合成文本为音频数据 Future _synthesize(String text, {String? voiceType}) async { if (text.isEmpty) { return null; } // 使用指定的语音类型,不使用任何回退 final effectiveVoiceType = voiceType ?? _voiceType; try { // 调用原生方法合成语音 final result = await _channel.invokeMethod('synthesizeSync', { 'text': text, 'voiceType': effectiveVoiceType, }); if (result != null && result.isNotEmpty) { return result; } // 如果没有结果,返回null return null; } catch (e) { // 不处理授权错误,直接抛出所有错误 print('调用原生合成方法失败: $e'); throw VolcanoTtsException('调用原生合成方法失败', e); } } /// 停止播放并清空队列 Future stop() async { try { print('停止播放'); await _audioPlayer.stop(); await _playlist.clear(); _pendingText = ''; _sentenceQueue.clear(); _isFetching = false; // 通知播放状态变化 _notifyPlayingStateChanged(false); // 确保清空所有待处理的音频数据 try { // 重置播放器状态 await _audioPlayer.pause(); await _audioPlayer.seek(Duration.zero); print('已清空所有待播放的文字和音频'); } catch (e) { print('清空音频缓存时出错: $e'); } } catch (e) { print('停止播放失败: $e'); } } /// 检查是否实际正在播放 bool isActuallyPlaying() { try { // 检查是否有活跃的播放任务 final hasActiveTask = _sentenceQueue.isNotEmpty || _isFetching; // 检查播放器状态 final isPlayerPlaying = _audioPlayer.playing; // 检查是否有待处理的文本 final hasPendingText = _pendingText.isNotEmpty; // 检查播放列表是否为空 final hasAudioSource = _playlist.length > 0; // 检查当前播放位置 final currentPosition = _audioPlayer.position; final currentDuration = _audioPlayer.duration ?? Duration.zero; // 检查是否已经播放到末尾但播放器状态未更新 final isAtEnd = currentDuration.inMilliseconds > 0 && currentPosition.inMilliseconds >= currentDuration.inMilliseconds - 100; // 如果播放器显示正在播放,但已经到达音频末尾,则认为实际上没有播放 if (isPlayerPlaying && isAtEnd) { // 如果检测到这种情况,尝试自动修复播放器状态 _fixPlayerState(); return false; } // 如果有活跃任务、播放器正在播放且未到末尾、有待处理文本,则认为正在播放 return hasActiveTask || (isPlayerPlaying && !isAtEnd) || hasPendingText; } catch (e) { print('检查TTS实际播放状态出错: $e'); return false; } } /// 修复播放器状态 void _fixPlayerState() { try { // 如果播放器显示正在播放,但实际上已经到达音频末尾,尝试修复状态 Future.microtask(() async { try { // 如果没有更多内容要播放,停止播放器 if (_sentenceQueue.isEmpty && _pendingText.isEmpty && !_isFetching) { await _audioPlayer.stop(); // 如果播放列表中只有已播放完的项目,清空播放列表 if (_playlist.length > 0) { await _playlist.clear(); } // 通知播放状态变化 _notifyPlayingStateChanged(false); } else { // 如果还有内容要播放,尝试播放下一个 _tryPlayNext(); } } catch (e) { print('修复播放器状态失败: $e'); } }); } catch (e) { print('尝试修复播放器状态时出错: $e'); } } /// 通知播放状态变化 void _notifyPlayingStateChanged(bool isPlaying) { try { _playingStateController.add(isPlaying); } catch (e) { print('通知播放状态变化失败: $e'); } } /// 重新初始化播放器 Future _reinitializePlayer() async { try { await stop(); await _audioPlayer.dispose(); // 重新初始化音频会话 await _initAudioSession(); _audioPlayer = AudioPlayer(); await _audioPlayer.setVolume(_defaultVolume); await _audioPlayer.setLoopMode(LoopMode.off); await _audioPlayer.setSpeed(_defaultSpeed); // 重新配置Android特定的播放参数 if (Platform.isAndroid) { 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; print('播放事件流错误: $error'); _reinitializePlayer(); }, ); _playerStateSubscription = _audioPlayer.playerStateStream.listen( (state) { if (_isDisposed) return; // 通知播放状态变化 final isPlaying = state.playing && _playlist.length > 0; _notifyPlayingStateChanged(isPlaying); if (state.processingState == ProcessingState.completed) { _tryPlayNext(); } }, onError: (error) { if (_isDisposed) return; print('播放状态流错误: $error'); _reinitializePlayer(); }, ); } /// 尝试播放下一个音频片段 Future _tryPlayNext() async { if (_isDisposed || _playlist.length <= 0) return; try { if (!_audioPlayer.playing) { // 确保音频会话处于激活状态 await _audioSession?.setActive(true); // 减少延迟时间 await Future.delayed(const Duration(milliseconds: 20)); if (!_isDisposed && !_audioPlayer.playing) { await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); } } } catch (e) { if (e is! StateError) { print('播放错误: $e'); await _reinitializePlayer(); } } } /// 切换TTS启用状态 void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) { stop(); } } /// 设置播放速度 Future setSpeed(double speed) async { if (speed < 0.5 || speed > 2.0) { throw VolcanoTtsException('播放速度必须在0.5到2.0之间'); } try { await _audioPlayer.setSpeed(speed); } catch (e) { print('设置播放速度失败: $e'); } } /// 设置音量 Future setVolume(double volume) async { if (volume < 0.0 || volume > 1.0) { throw VolcanoTtsException('音量必须在0.0到1.0之间'); } try { await _audioPlayer.setVolume(volume); } catch (e) { print('设置音量失败: $e'); } } /// 清理资源 Future _cleanupResources() async { _isDisposed = true; await _playbackEventSubscription?.cancel(); await _playerStateSubscription?.cancel(); await _audioSession?.setActive(false); await stop(); await _audioPlayer.dispose(); await _playingStateController.close(); // 释放原生资源 try { await _channel.invokeMethod('dispose'); } catch (e) { print('释放原生TTS资源失败: $e'); } } @override void onClose() async { print('关闭TTS服务'); await _cleanupResources(); super.onClose(); } /// 获取当前语音类型 String getCurrentVoiceType() { return _voiceType; } }