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_azure_tts/flutter_azure_tts.dart'; import '../../core/utils/logger.dart'; /// Azure语音服务异常 class AzureTtsException implements Exception { final String message; AzureTtsException(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) { Logger.error('BytesAudioSource请求错误: $e'); rethrow; } } @override Future get length => Future.value(_bytes.length); } /// Azure语音合成服务 class AzureFlutterTtsService extends GetxService { // 配置参数 final String _subscriptionKey; final String _region; final String _voiceLocale; // 语音配置 Voice? _selectedVoice; final String _audioFormat = AudioOutputFormat.audio16khz32kBitrateMonoMp3; // 音频播放器 late AudioPlayer _audioPlayer; final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []); final isEnabled = true.obs; // 默认启用 final isPlaying = false.obs; StreamSubscription? _playerStateSubscription; bool _playlistInitialized = false; bool _isDisposed = false; // 句子管理 String _pendingText = ''; final List _sentenceQueue = []; static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号 bool _isFetching = false; static const int _minSegmentLength = 10; // 最小分段长度 static const int _optimalSegmentLength = 30; // 最佳分段长度 // 音频数据控制器 - 用于事件通知 final _eventController = StreamController>.broadcast(); Stream> get eventStream => _eventController.stream; AzureFlutterTtsService() : _subscriptionKey = dotenv.env['AZURE_TTS_SUBSCRIPTION_KEY'] ?? '', _region = dotenv.env['AZURE_TTS_SERVICE_REGION'] ?? '', _voiceLocale = dotenv.env['AZURE_TTS_DEFAULT_VOICE']?.split('-').take(2).join('-') ?? 'zh-CN' { if (_subscriptionKey.isEmpty || _region.isEmpty) { throw Exception('Azure语音配置信息不完整,请检查环境变量'); } } @override Future onInit() async { super.onInit(); try { // 初始化Azure TTS await _initAzureTts(); // 初始化音频播放器 await _initAudioPlayer(); // 配置音频设备 await _configureAudioDevice(); } catch (e, stackTrace) { Logger.error('初始化Azure TTS服务失败: $e'); Logger.error('Stack trace: $stackTrace'); } } Future _initAzureTts() async { try { // 初始化Azure TTS服务 FlutterAzureTts.init( subscriptionKey: _subscriptionKey, region: _region, withLogs: false, // 生产环境禁用日志 ); // 获取可用的语音列表 // final voicesResponse = await FlutterAzureTts.getAvailableVoices(); // if (voicesResponse.voices.isEmpty) { // throw AzureTtsException('未找到可用的Azure语音'); // } // // 选择指定区域的语音 // _selectedVoice = voicesResponse.voices // .where((voice) => voice.locale.startsWith(_voiceLocale)) // .toList(growable: false) // .firstOrNull; // // 如果没有找到指定区域的语音,使用第一个可用的语音 // _selectedVoice ??= voicesResponse.voices.first; Logger.info('已选择Azure语音: $_voiceLocale'); } catch (e) { Logger.error('初始化Azure TTS失败: $e'); rethrow; } } 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, ); _playlistInitialized = true; _setupEventListeners(); } Future _configureAudioDevice() async { try { // 使用AudioSession配置音频设备 final session = await AudioSession.instance; if (Platform.isAndroid) { // Android平台 - 明确指定只使用默认设备 await session.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.playback, androidAudioAttributes: const AndroidAudioAttributes( contentType: AndroidAudioContentType.speech, usage: AndroidAudioUsage.media, // 移除audibilityEnforced标志,它可能导致多路输出 flags: AndroidAudioFlags.none, ), androidAudioFocusGainType: AndroidAudioFocusGainType.gain, androidWillPauseWhenDucked: true, )); // 确保只使用默认音频路由 if (Platform.isAndroid) { final audioManager = AndroidAudioManager(); await audioManager.setSpeakerphoneOn(false); } } else if (Platform.isIOS) { // iOS平台 - 明确指定只使用默认设备 await session.configure(AudioSessionConfiguration( avAudioSessionCategory: AVAudioSessionCategory.soloAmbient, // 使用soloAmbient而不是playback avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.none, // 不使用任何额外选项 )); // 确保使用默认路由 if (Platform.isIOS) { final avAudioSession = AVAudioSession(); await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none); } } // 激活音频会话 await session.setActive(true); return true; } catch (e) { Logger.error('配置音频设备失败: $e'); return false; } } Future speak(String text) async { if (!isEnabled.value || text.trim().isEmpty) return; try { _pendingText += text; _extractFullSentences(); await _startPreloadIfNeeded(); } catch (e, stack) { Logger.error('语音合成失败: $e'); await _reinitializePlayer(); } } void _extractFullSentences() { if (_pendingText.isEmpty) return; // 查找所有句子分隔符 final matches = _sentenceBreaks.allMatches(_pendingText).toList(); // 如果没有找到句子分隔符 if (matches.isEmpty) { // 如果累积的文本长度超过最佳长度,才进行处理 if (_pendingText.length > _optimalSegmentLength) { // 将整个文本添加到队列 _sentenceQueue.add(_pendingText); _pendingText = ''; } // 否则继续累积文本,等待更多输入 return; } // 找到了句子分隔符,处理完整的句子 int lastPos = 0; for (final match in matches) { final end = match.end; // 提取完整的句子(包含分隔符) final sentence = _pendingText.substring(lastPos, end).trim(); if (sentence.isNotEmpty) { // 将完整的句子添加到队列 _sentenceQueue.add(sentence); } lastPos = end; } // 处理剩余的不完整文本(不包含分隔符的部分) if (lastPos < _pendingText.length) { final remaining = _pendingText.substring(lastPos).trim(); // 如果剩余文本长度超过最佳长度,也将其添加到队列 if (remaining.length > _optimalSegmentLength) { _sentenceQueue.add(remaining); _pendingText = ''; } else { // 否则保留在_pendingText中,等待更多输入 _pendingText = remaining; } } else { // 所有文本都已处理,清空_pendingText _pendingText = ''; } } // 按长度分割文本的方法(保留以便其他地方调用) void _splitByLength(String text) { if (text.isEmpty) return; // 如果文本长度不超过最佳长度,直接添加整个文本 if (text.length <= _optimalSegmentLength) { _sentenceQueue.add(text); return; } int start = 0; while (start < text.length) { int end = math.min(start + _optimalSegmentLength, text.length); // 如果剩余文本长度小于最小字符数,直接取到末尾 if (text.length - start <= _minSegmentLength) { end = text.length; } // 否则尝试在标点或空格处分割 else 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) { _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) { final audioData = await _synthesize(segment); if (audioData != null) { try { // 添加到播放列表 await _addToPlaylist(audioData); } catch (e) { Logger.error('添加音频源失败: $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) { Logger.error('获取音频失败: $e'); Logger.error('Stack trace: $stackTrace'); _isFetching = false; } } Future _synthesize(String text) async { try { if (_selectedVoice == null) { // 如果未选择语音,使用默认语音 Logger.info('未选择语音,使用默认语音'); await setVoice('zh-CN-XiaoxiaoNeural'); // 使用默认语音 // 如果设置后仍然为null,则无法继续 if (_selectedVoice == null) { throw AzureTtsException('无法设置默认语音'); } } // 创建TTS参数 final params = TtsParams( voice: _selectedVoice!, audioFormat: _audioFormat, text: text, rate: 1.0, // 语速,可以根据需要调整 ); // 请求语音合成 final ttsResponse = await FlutterAzureTts.getTts(params); // 获取音频数据 final audioBytes = ttsResponse.audio.buffer.asUint8List(); return audioBytes; } catch (e) { Logger.error('Azure语音合成请求失败: $e'); return null; } } /// 将音频数据添加到播放列表 Future _addToPlaylist(Uint8List audioData) async { try { if (audioData.isEmpty) return; // 确保播放列表已初始化 if (!_playlistInitialized) { await _initAudioPlayer(); } // 创建音频源 final audioSource = BytesAudioSource(audioData); // 添加到播放列表 await _playlist.add(audioSource); // 如果当前没有播放,开始播放 if (!isPlaying.value) { await _startPlayback(); } else if (_audioPlayer.processingState == ProcessingState.completed) { // 如果播放已完成但状态还没更新,手动前进到下一个 await _advanceToNextItem(); } } catch (e) { Logger.error('添加音频到播放列表失败: $e'); // 通知错误 _notifyEvent('playbackError', '添加音频到播放列表失败: $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(); // 手动更新播放状态,避免等待事件流更新可能导致的延迟 if (_playlist.length > 0 && !isPlaying.value) { isPlaying.value = true; } } catch (e) { Logger.error('开始播放失败: $e'); // 通知错误 _notifyEvent('playbackError', '开始播放失败: $e'); } } /// 前进到播放列表中的下一个项目 Future _advanceToNextItem() async { try { if (_playlist.length == 0) { // 如果播放列表为空,确保 isPlaying 为 false if (isPlaying.value) { isPlaying.value = false; } return; } // 获取当前索引和序列状态 final currentIndex = _audioPlayer.currentIndex; final sequenceState = _audioPlayer.sequenceState; if (sequenceState == null) { // 如果没有序列状态,直接开始播放 await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); // 更新播放状态 if (!isPlaying.value) { isPlaying.value = true; } return; } // 如果当前索引无效,从头开始播放 if (currentIndex == null || currentIndex < 0) { await _audioPlayer.seek(Duration.zero, index: 0); await _audioPlayer.play(); // 更新播放状态 if (!isPlaying.value) { isPlaying.value = true; } return; } // 计算下一个索引 final nextIndex = currentIndex + 1; // 检查是否还有下一个项目 if (nextIndex < sequenceState.sequence.length) { // 跳转到下一个项目并开始播放 await _audioPlayer.seek(Duration.zero, index: nextIndex); await _audioPlayer.play(); // 更新播放状态 if (!isPlaying.value) { isPlaying.value = true; } } else { // 已经是最后一个项目 // 确保最后一个项目播放完成 if (_audioPlayer.position < _audioPlayer.duration!) { await _audioPlayer.seek(_audioPlayer.duration!); } // 如果已经是最后一个项目且已播放完成,更新播放状态为 false if (isPlaying.value) { isPlaying.value = false; } } } catch (e) { Logger.error('前进到下一个音频项目失败: $e'); } } Future stop() async { try { // 立即更新播放状态,避免等待事件流更新可能导致的延迟 if (isPlaying.value) { isPlaying.value = false; } await _audioPlayer.stop(); await _playlist.clear(); _pendingText = ''; _sentenceQueue.clear(); _isFetching = false; // 确保清空所有待处理的音频数据 try { // 重置播放器状态 await _audioPlayer.pause(); await _audioPlayer.seek(Duration.zero); } catch (e) { Logger.error('清空音频缓存时出错: $e'); } } catch (e) { Logger.error('停止播放失败: $e'); } } // 添加一个方法来检查音频播放器是否真正在播放 bool isActuallyPlaying() { try { // 检查播放器状态 final isPlayerPlaying = _audioPlayer.playing; // 检查播放列表是否为空 final hasAudioSource = _playlist.length > 0; // 只有当播放器正在播放且播放列表不为空时,才认为真正在播放 final actuallyPlaying = isPlayerPlaying && hasAudioSource; // 如果状态不一致,记录日志 if (isPlaying.value != actuallyPlaying) { Logger.info('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})'); } return actuallyPlaying; } catch (e) { Logger.error('检查实际播放状态失败: $e'); // 如果检查失败,返回当前的 isPlaying 值 return isPlaying.value; } } Future _reinitializePlayer() async { try { await stop(); await _audioPlayer.dispose(); // 重新初始化播放器 _audioPlayer = AudioPlayer(); await _audioPlayer.setVolume(1.0); await _audioPlayer.setLoopMode(LoopMode.off); // 重新配置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, ); _playlistInitialized = true; _setupEventListeners(); Logger.info('播放器重新初始化成功'); } catch (e) { Logger.error('播放器重新初始化失败: $e'); } } void _setupEventListeners() { _playerStateSubscription?.cancel(); _playerStateSubscription = _audioPlayer.playerStateStream.listen( (state) { if (_isDisposed) return; // 更新播放状态 - 使用更准确的判断逻辑 final isPlayerPlaying = state.playing; final hasAudioSource = _playlist.length > 0; final actuallyPlaying = isPlayerPlaying && hasAudioSource; // 只有当实际状态与当前状态不同时才更新 if (isPlaying.value != actuallyPlaying) { isPlaying.value = actuallyPlaying; } // 如果播放完成,尝试播放下一个 if (state.processingState == ProcessingState.completed) { _advanceToNextItem(); } }, onError: (error) { if (_isDisposed) return; Logger.error('播放器错误: $error'); _reinitializePlayer(); }, ); } void toggleEnabled() { isEnabled.toggle(); if (!isEnabled.value) { stop(); } } Future _cleanupResources() async { _isDisposed = true; await _playerStateSubscription?.cancel(); await stop(); await _audioPlayer.dispose(); // 关闭事件控制器 if (!_eventController.isClosed) { await _eventController.close(); } } @override void onClose() async { Logger.info('关闭Azure TTS服务'); await _cleanupResources(); super.onClose(); } /// 使用系统默认音频设备 Future useDefaultAudioDevice() async { return await _configureAudioDevice(); } /// 更新语音 void updateVoice(Voice voice) { _selectedVoice = voice; Logger.info('已更新Azure语音: ${voice.shortName}'); } /// 根据语音名称设置语音 Future setVoice(String voiceName) async { Logger.info('正在设置Azure语音: $voiceName'); try { // 获取可用的语音列表 final voicesResponse = await FlutterAzureTts.getAvailableVoices(); if (voicesResponse.voices.isEmpty) { Logger.error('未找到可用的Azure语音'); return; } // 1. 首先尝试精确匹配 Voice? matchedVoice; for (final voice in voicesResponse.voices) { if (voice.shortName == voiceName) { matchedVoice = voice; break; } } // 2. 如果没有精确匹配,尝试部分匹配 if (matchedVoice == null) { for (final voice in voicesResponse.voices) { if (voice.shortName.contains(voiceName) || voiceName.contains(voice.shortName)) { matchedVoice = voice; break; } } } // 3. 如果仍然没有匹配,尝试匹配语言区域 if (matchedVoice == null) { final localePrefix = voiceName.split('-').take(2).join('-'); for (final voice in voicesResponse.voices) { if (voice.locale.startsWith(localePrefix)) { matchedVoice = voice; break; } } } // 4. 如果所有尝试都失败,使用第一个可用的语音 if (matchedVoice == null && voicesResponse.voices.isNotEmpty) { matchedVoice = voicesResponse.voices.first; } // 设置选中的语音 if (matchedVoice != null) { _selectedVoice = matchedVoice; Logger.info('已设置Azure语音: ${matchedVoice.shortName}'); } else { Logger.error('未能找到合适的语音'); } } catch (e) { Logger.error('设置Azure语音失败: $e'); // 如果获取语音列表失败,创建一个简化的Voice对象 _selectedVoice = Voice( shortName: voiceName, displayName: voiceName, locale: _voiceLocale, name: voiceName, localName: voiceName, gender: 'Female', voiceType: 'Neural', status: 'GA', sampleRateHertz: '24000', styles: [], roles: [], ); Logger.info('使用简化的Voice对象: $voiceName'); } } /// 通知事件 void _notifyEvent(String eventType, String message) { if (!_eventController.isClosed) { _eventController.add({ 'eventType': eventType, 'message': message, }); } } }