You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
793 lines
24 KiB
793 lines
24 KiB
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<StreamAudioResponse> request([int? start, int? end]) async {
|
|
start = start ?? 0;
|
|
end = end ?? _bytes.length;
|
|
|
|
try {
|
|
final subData = _bytes.sublist(start, end);
|
|
|
|
// 使用固定大小的块进行流式传输
|
|
final chunks = <List<int>>[];
|
|
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<int> 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<String> _sentenceQueue = [];
|
|
static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号
|
|
bool _isFetching = false;
|
|
static const int _minSegmentLength = 10; // 最小分段长度
|
|
static const int _optimalSegmentLength = 30; // 最佳分段长度
|
|
|
|
// 音频数据控制器 - 用于事件通知
|
|
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
|
|
Stream<Map<String, dynamic>> 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<void> 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<void> _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<void> _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<bool> _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<void> 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<void> _startPreloadIfNeeded() async {
|
|
if (_isFetching || _sentenceQueue.isEmpty) return;
|
|
_isFetching = true;
|
|
await _fetchNextSegment();
|
|
}
|
|
|
|
Future<void> _fetchNextSegment() async {
|
|
if (_sentenceQueue.isEmpty) {
|
|
_isFetching = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 预加载多个片段以实现流畅播放
|
|
final segments = <String>[];
|
|
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<Uint8List?> _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<void> _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<void> _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<void> _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<void> 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<void> _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<void> _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<bool> useDefaultAudioDevice() async {
|
|
return await _configureAudioDevice();
|
|
}
|
|
|
|
/// 更新语音
|
|
void updateVoice(Voice voice) {
|
|
_selectedVoice = voice;
|
|
Logger.info('已更新Azure语音: ${voice.shortName}');
|
|
}
|
|
|
|
/// 根据语音名称设置语音
|
|
Future<void> 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,
|
|
});
|
|
}
|
|
}
|
|
}
|