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.

354 lines
10 KiB

import 'dart:async';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:flutter_azure_tts/flutter_azure_tts.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// A custom AudioSource that reads audio data from a byte array
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _recommendedBufferSize = 8192; // 使用推荐的缓冲区大小
BytesAudioSource(this._bytes);
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? _bytes.length;
try {
// 确保缓冲区大小至少为推荐值
final bufferSize = (end - start) < _recommendedBufferSize
? _recommendedBufferSize
: end - start;
final subData = _bytes.sublist(start, end);
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: subData.length,
offset: start,
stream: Stream.value(subData),
contentType: 'audio/mpeg',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// Azure TTS Service,支持将文本转换为语音并以流式方式边下载边播放
class AzureTtsService extends GetxService {
static bool _isInitialized = false;
final _audioPlayer = AudioPlayer();
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = false.obs;
// 播放状态变化通知流
final _playingStateController = StreamController<bool>.broadcast();
Stream<bool> get playingStateStream => _playingStateController.stream;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// Azure 配置
final String _azureKey;
final String _azureRegion;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?]');
// 状态
bool _isFetching = false;
late Voice _selectedVoice;
bool _isDisposed = false;
AzureTtsService() :
_azureKey = dotenv.env['AZURE_TTS_KEY'] ?? '',
_azureRegion = dotenv.env['AZURE_TTS_REGION'] ?? '' {
if (_azureKey.isEmpty || _azureRegion.isEmpty) {
throw Exception('Azure TTS配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
// 初始化音频会话
_audioSession = await AudioSession.instance;
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
));
// 设置音频播放器
if (!_isDisposed) {
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: false,
);
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
print('播放事件: $event');
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
print('播放错误: $error');
_handlePlaybackError(error);
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen((state) {
if (_isDisposed) return;
print('播放器状态变化: playing=${state.playing},processingState=${state.processingState}');
// 通知播放状态变化
_notifyPlayingStateChanged(state.playing && _playlist.length > 0);
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
});
}
// 初始化 Azure TTS (只初始化一次)
if (!_isInitialized && !_isDisposed) {
FlutterAzureTts.init(
subscriptionKey: _azureKey,
region: _azureRegion,
withLogs: true,
);
_isInitialized = true;
}
// 获取可用语音列表并设置默认语音
if (!_isDisposed) {
final voicesResponse = await FlutterAzureTts.getAvailableVoices();
_selectedVoice = voicesResponse.voices.firstWhere(
(v) => v.shortName == 'zh-CN-YunxiNeural',
orElse: () => voicesResponse.voices.first,
);
print('已选择语音: ${_selectedVoice.shortName}');
}
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await stop();
await _audioPlayer.dispose();
await _playingStateController.close();
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
void _handlePlaybackError(dynamic error) {
print('处理播放错误: $error');
// 尝试重新初始化播放器
_reinitializePlayer();
}
Future<void> _reinitializePlayer() async {
try {
await _audioPlayer.stop();
await _playlist.clear();
await _audioPlayer.setAudioSource(_playlist);
// 通知播放状态变化
_notifyPlayingStateChanged(false);
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
void _tryPlayNext() async {
if (_playlist.length > 0 && !_audioPlayer.playing) {
try {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
} catch (e) {
print('尝试播放下一段失败: $e');
}
}
}
Future<void> speak(String text) async {
if (!isEnabled.value) {
print('TTS 未启用,跳过播放。');
return;
}
try {
print('准备播放文本: $text');
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
if (!_audioPlayer.playing && _playlist.length > 0) {
print('开始播放音频');
await _audioPlayer.play();
}
} catch (e) {
print('播放文本失败: $e');
}
}
// 检查音频播放器是否真正在播放
bool isActuallyPlaying() {
try {
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 只有当播放器正在播放且播放列表不为空时,才认为真正在播放
return isPlayerPlaying && hasAudioSource;
} catch (e) {
print('检查实际播放状态失败: $e');
return false;
}
}
// 通知播放状态变化
void _notifyPlayingStateChanged(bool isPlaying) {
try {
_playingStateController.add(isPlaying);
} catch (e) {
print('通知播放状态变化失败: $e');
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) 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 > 0) {
_pendingText = _pendingText.substring(lastPos);
}
}
Future<void> _startPreloadIfNeeded() async {
if (_isFetching) return;
if (_sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment();
}
Future<void> _fetchNextSegment() async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
final sentence = _sentenceQueue.removeAt(0);
print('获取音频片段: $sentence');
try {
final params = TtsParams(
voice: _selectedVoice,
audioFormat: AudioOutputFormat.audio16khz32kBitrateMonoMp3,
rate: 1.0,
text: sentence,
);
final ttsResponse = await FlutterAzureTts.getTts(params);
if (ttsResponse.audio != null && ttsResponse.audio.isNotEmpty) {
print('成功获取音频数据: ${ttsResponse.audio.length} bytes');
try {
final audioSource = BytesAudioSource(ttsResponse.audio);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
if (!_audioPlayer.playing && _playlist.length == 1) {
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
await Future.delayed(const Duration(milliseconds: 100));
await _audioPlayer.play();
}
} catch (e) {
print('添加音频源失败: $e');
}
} else {
print('获取音频数据失败: 返回为空或长度为0');
}
} catch (e, stackTrace) {
print('获取音频失败: $e');
print('Stack trace: $stackTrace');
}
await _fetchNextSegment();
}
Future<void> stop() async {
try {
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
// 通知播放状态变化
_notifyPlayingStateChanged(false);
} catch (e) {
print('停止播放失败: $e');
}
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
}