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.
315 lines
8.7 KiB
315 lines
8.7 KiB
import 'dart:async';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import '../../../data/services/azure_tts_service.dart';
|
|
import '../../../core/utils/logger.dart';
|
|
|
|
// 导入 TtsState 枚举
|
|
import '../../../data/services/azure_tts_service.dart' show TtsState;
|
|
|
|
class AzureTtsTestController extends GetxController {
|
|
final AzureTtsService _ttsService = Get.find<AzureTtsService>();
|
|
|
|
// 可观察状态
|
|
final isSpeaking = false.obs;
|
|
final errorMessage = ''.obs;
|
|
final isLoading = false.obs;
|
|
final statusMessage = '未连接'.obs;
|
|
|
|
// 添加实际音频播放状态
|
|
final isAudioPlaying = false.obs;
|
|
|
|
// 播放状态检查定时器
|
|
Timer? _playbackCheckTimer;
|
|
|
|
// 当前播放的索引
|
|
final currentPlayingIndex = RxInt(-1);
|
|
|
|
// 当前使用的语音
|
|
final currentVoice = 'zh-CN-XiaoxiaoNeural'.obs;
|
|
|
|
// 可用的语音列表
|
|
final List<Map<String, String>> availableVoices = [
|
|
{'id': 'zh-CN-XiaoxiaoNeural', 'name': '晓晓(女声)'},
|
|
{'id': 'zh-CN-YunxiNeural', 'name': '云希(男声)'},
|
|
{'id': 'zh-CN-YunyangNeural', 'name': '云扬(男声)'},
|
|
{'id': 'zh-CN-XiaohanNeural', 'name': '晓涵(女声)'},
|
|
{'id': 'zh-CN-XiaomoNeural', 'name': '晓墨(女声)'},
|
|
{'id': 'zh-CN-XiaoxuanNeural', 'name': '晓萱(女声)'},
|
|
{'id': 'en-US-AriaNeural', 'name': 'Aria(英语女声)'},
|
|
];
|
|
|
|
// 固定的播报内容
|
|
final RxList<String> speechTexts = <String>[
|
|
'这是第一条播报测试,请听取Azure合成效果。',
|
|
'这是第二条播报测试,Azure语音合成支持连续多条播报。',
|
|
'这是第三条播报测试,可以测试语音合成的流畅度和连贯性。',
|
|
'This is an English test sentence. Azure Text-to-Speech supports multiple languages.',
|
|
].obs;
|
|
|
|
// 事件订阅
|
|
StreamSubscription? _eventSubscription;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
|
|
// 初始化UI状态
|
|
isSpeaking.value = false;
|
|
isAudioPlaying.value = false;
|
|
errorMessage.value = '';
|
|
isLoading.value = false;
|
|
|
|
// 监听TTS服务状态
|
|
_setupEventListener();
|
|
|
|
// 启动播放状态检查定时器
|
|
_startPlaybackCheckTimer();
|
|
}
|
|
|
|
// 设置事件监听
|
|
void _setupEventListener() {
|
|
_eventSubscription?.cancel();
|
|
_eventSubscription = _ttsService.synthesisStream?.listen(_handleEvent);
|
|
}
|
|
|
|
// 处理事件
|
|
void _handleEvent(SynthesisEvent event) {
|
|
switch (event.type) {
|
|
case SynthesisEventType.synthesisStarted:
|
|
statusMessage.value = '合成开始';
|
|
errorMessage.value = '';
|
|
break;
|
|
case SynthesisEventType.synthesisCompleted:
|
|
statusMessage.value = '合成完成';
|
|
errorMessage.value = '';
|
|
break;
|
|
case SynthesisEventType.synthesisCanceled:
|
|
statusMessage.value = '合成取消';
|
|
errorMessage.value = event.reason ?? '未知原因';
|
|
currentPlayingIndex.value = -1;
|
|
break;
|
|
case SynthesisEventType.error:
|
|
statusMessage.value = '错误';
|
|
errorMessage.value = event.error ?? '未知错误';
|
|
currentPlayingIndex.value = -1;
|
|
break;
|
|
default:
|
|
statusMessage.value = '事件: ${event.type}';
|
|
break;
|
|
}
|
|
|
|
// 强制更新UI
|
|
update();
|
|
}
|
|
|
|
/// 更新UI状态
|
|
void updateUIState() {
|
|
// 强制更新UI
|
|
update();
|
|
}
|
|
|
|
// 启动播放状态检查定时器
|
|
void _startPlaybackCheckTimer() {
|
|
_playbackCheckTimer?.cancel();
|
|
_playbackCheckTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
|
|
// 更新实际播放状态
|
|
final actuallyPlaying = _ttsService.ttsState == TtsState.playing || _ttsService.ttsState == TtsState.synthesizing;
|
|
|
|
// 只有状态变化时才更新
|
|
if (isAudioPlaying.value != actuallyPlaying) {
|
|
isAudioPlaying.value = actuallyPlaying;
|
|
// 更新UI
|
|
update();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 连续播放所有文本
|
|
Future<void> speakContinuousTexts() async {
|
|
if (isLoading.value) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isLoading.value = true;
|
|
isSpeaking.value = true;
|
|
errorMessage.value = '';
|
|
|
|
final speaker = currentVoice.value;
|
|
|
|
// 设置语音参数
|
|
_ttsService.currentVoice = speaker;
|
|
|
|
|
|
// 连续播放所有文本
|
|
for (int i = 0; i < speechTexts.length; i++) {
|
|
// 如果播放被中断,退出循环
|
|
if (!isSpeaking.value) break;
|
|
|
|
// 在主线程上更新UI
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
currentPlayingIndex.value = i;
|
|
statusMessage.value = '正在播放第 ${i + 1}/${speechTexts.length} 条文本';
|
|
update();
|
|
});
|
|
|
|
// 播放当前文本,不等待完成
|
|
_ttsService.speak(speechTexts[i]);
|
|
|
|
// 短暂延迟,确保文本被送入队列
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
}
|
|
|
|
// 在主线程上更新UI
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
statusMessage.value = '所有文本已送入播放队列';
|
|
update();
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
errorMessage.value = '播放出错';
|
|
if (kDebugMode) {
|
|
print('连续播放出错: $e');
|
|
}
|
|
} finally {
|
|
isLoading.value = false;
|
|
|
|
// 强制更新UI
|
|
update();
|
|
}
|
|
}
|
|
|
|
/// 播放单条文本
|
|
Future<void> playSingle(int index) async {
|
|
if (index < 0 || index >= speechTexts.length) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isSpeaking.value = true;
|
|
errorMessage.value = '';
|
|
currentPlayingIndex.value = index;
|
|
statusMessage.value = '准备播放...';
|
|
|
|
final text = speechTexts[index];
|
|
final speaker = currentVoice.value;
|
|
|
|
// 设置语音参数
|
|
_ttsService.currentVoice = speaker;
|
|
|
|
// 使用异步操作,避免阻塞主线程
|
|
Future(() {
|
|
// 播放文本,不等待完成
|
|
_ttsService.speak(text);
|
|
|
|
// 在主线程上更新UI
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
statusMessage.value = '文本已送入播放队列';
|
|
update();
|
|
});
|
|
});
|
|
|
|
} catch (e) {
|
|
errorMessage.value = '播放出错';
|
|
if (kDebugMode) {
|
|
print('播放出错: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 停止播放
|
|
void stopSpeaking() {
|
|
try {
|
|
isSpeaking.value = false;
|
|
statusMessage.value = '正在停止播放...';
|
|
|
|
// 使用异步操作,避免阻塞主线程
|
|
Future(() async {
|
|
// 停止播放
|
|
await _ttsService.stop();
|
|
|
|
// 在主线程上更新UI
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
// 重置UI状态
|
|
currentPlayingIndex.value = -1;
|
|
statusMessage.value = '已停止播放';
|
|
update();
|
|
});
|
|
});
|
|
|
|
} catch (e) {
|
|
if (kDebugMode) {
|
|
print('停止播放出错: $e');
|
|
}
|
|
errorMessage.value = '停止播放出错';
|
|
}
|
|
}
|
|
|
|
// 设置当前语音
|
|
void setVoice(String voiceId) {
|
|
currentVoice.value = voiceId;
|
|
}
|
|
|
|
// 获取当前语音名称
|
|
String getCurrentVoiceName() {
|
|
final voice = availableVoices.firstWhere(
|
|
(v) => v['id'] == currentVoice.value,
|
|
orElse: () => {'id': currentVoice.value, 'name': '未知语音'},
|
|
);
|
|
return voice['name'] ?? '未知语音';
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
// 取消事件订阅
|
|
_eventSubscription?.cancel();
|
|
|
|
// 取消播放状态检查定时器
|
|
_playbackCheckTimer?.cancel();
|
|
|
|
super.onClose();
|
|
}
|
|
|
|
/// 初始化Azure TTS服务
|
|
Future<void> initialize({
|
|
String? subscriptionKey,
|
|
String? region,
|
|
}) async {
|
|
try {
|
|
isLoading.value = true;
|
|
statusMessage.value = '正在初始化Azure TTS...';
|
|
|
|
if (subscriptionKey == null || region == null) {
|
|
statusMessage.value = '尝试从环境变量读取配置...';
|
|
}
|
|
|
|
await _ttsService.initialize(
|
|
subscriptionKey: subscriptionKey,
|
|
region: region,
|
|
);
|
|
|
|
// 检查是否初始化成功
|
|
final isSuccess = _ttsService.isInitialized;
|
|
|
|
if (isSuccess) {
|
|
isSpeaking.value = false;
|
|
statusMessage.value = 'Azure TTS初始化成功';
|
|
} else {
|
|
statusMessage.value = 'Azure TTS初始化失败';
|
|
}
|
|
} catch (e) {
|
|
statusMessage.value = '初始化异常: $e';
|
|
errorMessage.value = e.toString();
|
|
if (kDebugMode) {
|
|
print('初始化异常: $e');
|
|
}
|
|
} finally {
|
|
isLoading.value = false;
|
|
// 强制更新UI
|
|
update();
|
|
}
|
|
}
|
|
}
|