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

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,
});
}
}
}