import 'dart:async'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; /// 微软 Text-to-Speech 服务异常 class MicrosoftTtsException implements Exception { final String message; MicrosoftTtsException(this.message); @override String toString() => message; } /// 微软 Text-to-Speech 服务 /// /// 该服务通过平台通道与 Android 上的 Microsoft Speech SDK 交互, /// 提供文本转语音功能。 class MicrosoftTtsService extends GetxService { static const MethodChannel _channel = MethodChannel('com.example.deep_voice/text_to_speech'); bool _isInitialized = false; late final String _subscriptionKey; late final String _serviceRegion; // 当前使用的语音 String _currentVoice = 'zh-CN-XiaoxiaoNeural'; String get currentVoice => _currentVoice; // 语音合成队列 final List _textQueue = []; bool _isProcessingQueue = false; bool _isSpeaking = false; // 可观察状态 final isEnabled = true.obs; final isSpeaking = false.obs; MicrosoftTtsService() { _loadConfig(); } /// 从环境变量加载配置 void _loadConfig() { _subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; _serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { throw MicrosoftTtsException('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); } } /// 初始化微软 TTS SDK /// /// 返回 true 表示初始化成功,否则抛出 PlatformException Future initialize() async { if (_isInitialized) return true; try { final bool result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey, 'serviceRegion': _serviceRegion, }); _isInitialized = result; return result; } on PlatformException catch (e) { throw MicrosoftTtsException('初始化失败: ${e.message}'); } } /// 设置语音 /// /// [voiceName] 语音名称,例如 "zh-CN-XiaoxiaoNeural" /// /// 返回 true 表示设置成功,否则抛出 PlatformException Future setVoice(String voiceName) async { if (!_isInitialized) { await initialize(); } try { final bool result = await _channel.invokeMethod('setVoice', { 'voiceName': voiceName, }); if (result) { _currentVoice = voiceName; } return result; } on PlatformException catch (e) { throw MicrosoftTtsException('设置语音失败: ${e.message}'); } } /// 将文本转换为语音并播放 /// /// [text] 要转换的文本 /// /// 返回合成结果消息,否则抛出 PlatformException Future speakText(String text) async { if (!_isInitialized) { await initialize(); } if (!isEnabled.value) { return "TTS 服务已禁用"; } try { _isSpeaking = true; isSpeaking.value = true; final String result = await _channel.invokeMethod('speakText', { 'text': text, }); _isSpeaking = false; isSpeaking.value = false; return result; } on PlatformException catch (e) { _isSpeaking = false; isSpeaking.value = false; throw MicrosoftTtsException('语音合成失败: ${e.message}'); } } /// 将 SSML 转换为语音并播放 /// /// [ssml] SSML 格式的文本 /// /// 返回合成结果消息,否则抛出 PlatformException Future speakSsml(String ssml) async { if (!_isInitialized) { await initialize(); } if (!isEnabled.value) { return "TTS 服务已禁用"; } try { _isSpeaking = true; isSpeaking.value = true; final String result = await _channel.invokeMethod('speakSsml', { 'ssml': ssml, }); _isSpeaking = false; isSpeaking.value = false; return result; } on PlatformException catch (e) { _isSpeaking = false; isSpeaking.value = false; throw MicrosoftTtsException('SSML 语音合成失败: ${e.message}'); } } /// 添加文本到队列并开始处理 /// /// [text] 要添加到队列的文本 /// [rate] 可选,语速,范围 -100 到 100,默认为 0 /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 /// /// 返回 true 表示成功添加到队列 Future speak(String text, {int rate = 0, int pitch = 0}) async { if (!isEnabled.value) { return false; } if (text.isEmpty) { return false; } // 生成 SSML final ssml = generateSsml( text: text, rate: rate, pitch: pitch, ); // 添加到队列 _textQueue.add(ssml); // 如果队列未在处理中,开始处理 if (!_isProcessingQueue) { _processQueue(); } return true; } /// 连续播放多段文本 /// /// [texts] 要连续播放的文本列表 /// [rate] 可选,语速,范围 -100 到 100,默认为 0 /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 /// /// 返回 true 表示成功添加到队列 Future speakMultiple(List texts, {int rate = 0, int pitch = 0}) async { if (!isEnabled.value) { return false; } if (texts.isEmpty) { return false; } // 将所有文本添加到队列 for (final text in texts) { if (text.isNotEmpty) { final ssml = generateSsml( text: text, rate: rate, pitch: pitch, ); _textQueue.add(ssml); } } // 如果队列未在处理中,开始处理 if (!_isProcessingQueue) { _processQueue(); } return true; } /// 处理语音合成队列 Future _processQueue() async { if (_textQueue.isEmpty || _isProcessingQueue) { return; } _isProcessingQueue = true; try { while (_textQueue.isNotEmpty) { // 如果服务被禁用,清空队列并退出 if (!isEnabled.value) { _textQueue.clear(); break; } // 获取队列中的下一个 SSML final ssml = _textQueue.removeAt(0); // 播放 SSML await speakSsml(ssml); } } catch (e) { print('处理语音队列时出错: $e'); } finally { _isProcessingQueue = false; } } /// 停止当前语音合成并清空队列 Future stop() async { // 清空队列 _textQueue.clear(); // 如果当前正在播放,尝试停止 if (_isSpeaking) { try { await _channel.invokeMethod('dispose'); await initialize(); // 重新初始化以确保资源正确释放和重建 _isSpeaking = false; isSpeaking.value = false; } catch (e) { print('停止语音合成时出错: $e'); } } } /// 生成 SSML 文本 /// /// [text] 要转换的文本 /// [voiceName] 可选,语音名称,默认使用当前设置的语音 /// [rate] 可选,语速,范围 -100 到 100,默认为 0 /// [pitch] 可选,音调,范围 -100 到 100,默认为 0 /// /// 返回 SSML 格式的文本 String generateSsml({ required String text, String? voiceName, int rate = 0, int pitch = 0, }) { final voice = voiceName ?? _currentVoice; final rateValue = rate.clamp(-100, 100); final pitchValue = pitch.clamp(-100, 100); // 将 rate 和 pitch 转换为 SSML 格式的值 final String rateStr = _convertRateToSsml(rateValue); final String pitchStr = _convertPitchToSsml(pitchValue); return ''' $text '''; } /// 将 rate 值转换为 SSML 格式 String _convertRateToSsml(int rate) { if (rate == 0) return '0%'; // 将 -100 到 100 的范围映射到 -90% 到 100% if (rate < 0) { // 负值映射到 -90% 到 0% return '${(rate * 0.9).round()}%'; } else { // 正值映射到 0% 到 100% return '${rate}%'; } } /// 将 pitch 值转换为 SSML 格式 String _convertPitchToSsml(int pitch) { if (pitch == 0) return '0%'; // 将 -100 到 100 的范围映射到 -50% 到 50% return '${(pitch * 0.5).round()}%'; } /// 释放资源 Future dispose() async { if (!_isInitialized) return; try { await _channel.invokeMethod('dispose'); _isInitialized = false; } on PlatformException catch (e) { throw MicrosoftTtsException('释放资源失败: ${e.message}'); } } }