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.

1501 lines
43 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:io' as io;
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/io.dart';
import 'package:uuid/uuid.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import '../../core/utils/logger.dart';
/// 火山语音合成双向流式API服务
///
/// 提供与火山语音TTS服务的WebSocket交互功能
/// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession
///
/// 基本使用流程:
/// 1. 调用 startSession 开始会话
/// 2. 调用 synthesize 合成文本
/// 3. 调用 endSession 结束会话
///
/// 或者使用 speakSingle 一次性完成上述流程
class VolcanoTtsApiService extends GetxController {
// 单例实例
static VolcanoTtsApiService? _instance;
// 单例工厂方法
factory VolcanoTtsApiService() {
_instance ??= VolcanoTtsApiService._internal();
return _instance!;
}
// 私有构造函数
VolcanoTtsApiService._internal() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_resourceId = dotenv.env['VOLCANO_TTS_RESOURCE_ID'] ?? '' {
if (_appId.isEmpty || _token.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID 和 VOLCANO_TTS_APP_TOKEN');
}
}
// 协议常量
static const int _protocolVersion = 0x0001;
static const int _defaultHeaderSize = 0x0001;
// 消息类型
static const int _fullClientRequest = 0x0001;
static const int _audioOnlyResponse = 0x000B;
static const int _fullServerResponse = 0x0009;
static const int _errorInformation = 0x000F;
// 消息类型特定标志
static const int _msgTypeFlagNoSeq = 0x0000;
static const int _msgTypeFlagPositiveSeq = 0x0001;
static const int _msgTypeFlagLastNoSeq = 0x0002;
static const int _msgTypeFlagNegativeSeq = 0x0003;
static const int _msgTypeFlagWithEvent = 0x0004;
// 消息序列化
static const int _noSerialization = 0x0000;
static const int _jsonSerialization = 0x0001;
// 消息压缩
static const int _compressionNo = 0x0000;
static const int _compressionGzip = 0x0001;
// 事件类型
static const int eventNone = 0;
static const int eventStartConnection = 1;
static const int eventFinishConnection = 2;
static const int eventConnectionStarted = 50;
static const int eventConnectionFailed = 51;
static const int eventConnectionFinished = 52;
static const int eventStartSession = 100;
static const int eventFinishSession = 102;
static const int eventSessionStarted = 150;
static const int eventSessionFinished = 152;
static const int eventSessionFailed = 153;
static const int eventTaskRequest = 200;
static const int eventTtsSentenceStart = 350;
static const int eventTtsSentenceEnd = 351;
static const int eventTtsResponse = 352;
// 服务URL
static const String _serviceUrl = 'wss://openspeech.bytedance.com/api/v3/tts/bidirection';
// 配置参数
final String _appId;
final String _token;
final String _resourceId;
// WebSocket连接
WebSocketChannel? _channel;
// 会话ID
String? _sessionId;
String? _connectionId;
// 状态控制
bool _isConnected = false;
// 音频数据控制器
final _audioDataController = StreamController<Uint8List>.broadcast();
Stream<Uint8List> get audioStream => _audioDataController.stream;
// 事件控制器
final _eventController = StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get eventStream => _eventController.stream;
// 控制器状态
bool _isDisposed = false;
// 当前状态
final isConnecting = false.obs;
final isConnected = false.obs;
final isSynthesizing = false.obs;
// 音频播放器
final AudioPlayer _audioPlayer = AudioPlayer();
// 播放列表 - 用于连续播放
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
bool _playlistInitialized = false;
// 音频数据控制器 - 用于流式播放
final StreamController<List<int>> _audioStreamController = StreamController<List<int>>();
// 是否正在播放
final RxBool _isPlaying = false.obs;
bool get isPlaying => _isPlaying.value;
// 默认发音人 - 使用预定义的值
final String defaultSpeaker = 'zh_female_yuanqinvyou_moon_bigtts';
// 音频缓冲区
final List<Uint8List> _audioBuffer = [];
// 检查是否需要准备下一个音频项目
DateTime? _lastCheckTime;
int? _lastCheckedIndex;
@override
void onInit() {
super.onInit();
// 初始化时不自动连接,等待需要时再连接
// 设置音频播放器状态监听
_setupAudioPlayerListeners();
// 初始化播放列表
_initializePlaylist();
}
/// 初始化播放列表
Future<void> _initializePlaylist() async {
try {
if (!_playlistInitialized) {
// 设置音频属性,启用无缝播放
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setShuffleModeEnabled(false);
// 设置自动连续播放
await _audioPlayer.setAutomaticallyWaitsToMinimizeStalling(false);
// 设置音频源
await _audioPlayer.setAudioSource(_playlist, initialIndex: 0, initialPosition: Duration.zero);
_playlistInitialized = true;
if (kDebugMode) {
print('播放列表初始化成功');
}
}
} catch (e) {
if (kDebugMode) {
print('初始化播放列表失败: $e');
}
// 不抛出异常,避免中断整个流程
}
}
/// 设置音频播放器状态监听
void _setupAudioPlayerListeners() {
_audioPlayer.playerStateStream.listen((state) {
final processingState = state.processingState;
final playing = state.playing;
if (processingState == ProcessingState.ready && playing) {
_isPlaying.value = true;
} else {
_isPlaying.value = false;
}
});
_audioPlayer.processingStateStream.listen((state) {
if (state == ProcessingState.completed) {
// 播放完成后检查播放列表是否为空
if (_playlist.length > 0) {
final currentIndex = _audioPlayer.currentIndex;
// 自动播放下一个项目
_advanceToNextItem();
} else {
// 播放列表为空,标记为未播放状态
_isPlaying.value = false;
}
}
});
// 监听序列状态变化
_audioPlayer.sequenceStateStream.listen((sequenceState) {
if (sequenceState == null) return;
// 如果当前是最后一个项目且播放已完成,标记播放完成
if (sequenceState.currentIndex == sequenceState.sequence.length - 1 &&
_audioPlayer.processingState == ProcessingState.completed) {
_isPlaying.value = false;
}
});
// 监听位置变化,但减少检查频率
DateTime _lastPositionCheck = DateTime.now();
_audioPlayer.positionStream.listen((position) {
// 限制检查频率,每500毫秒检查一次
final now = DateTime.now();
if (now.difference(_lastPositionCheck).inMilliseconds >= 500) {
_lastPositionCheck = now;
_checkForNextItem(position);
}
});
}
/// 检查是否需要准备下一个音频项目
Future<void> _checkForNextItem(Duration position) async {
// 只有在播放中且有下一个项目时才检查
if (!_isPlaying.value || _audioPlayer.currentIndex == null) return;
final currentIndex = _audioPlayer.currentIndex!;
final sequenceState = _audioPlayer.sequenceState;
// 如果没有序列状态,则不需要准备
if (sequenceState == null) return;
// 防抖动:如果在短时间内(200毫秒)对同一个索引进行了检查,则跳过
final now = DateTime.now();
if (_lastCheckTime != null &&
_lastCheckedIndex == currentIndex &&
now.difference(_lastCheckTime!) < const Duration(milliseconds: 200)) {
return;
}
// 更新最后检查时间和索引
_lastCheckTime = now;
_lastCheckedIndex = currentIndex;
// 获取当前项目的总时长
final duration = _audioPlayer.duration;
if (duration == null) return;
// 如果是最后一个项目,不需要预加载
final isLastItem = currentIndex >= sequenceState.sequence.length - 1;
// 如果接近结束(剩余时间小于200毫秒),准备下一个项目或完成播放
if (duration - position <= const Duration(milliseconds: 200) && !isLastItem) {
// 让播放器自然过渡到下一个项目
}
}
/// 前进到播放列表中的下一个项目
Future<void> _advanceToNextItem() async {
try {
if (_playlist.length == 0) return;
// 获取当前索引和序列状态
final currentIndex = _audioPlayer.currentIndex;
final sequenceState = _audioPlayer.sequenceState;
if (sequenceState == null) {
// 如果没有序列状态,直接开始播放
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
return;
}
// 如果当前索引无效,从头开始播放
if (currentIndex == null || currentIndex < 0) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
return;
}
// 计算下一个索引
final nextIndex = currentIndex + 1;
// 检查是否还有下一个项目
if (nextIndex < sequenceState.sequence.length) {
// 跳转到下一个项目并开始播放
await _audioPlayer.seek(Duration.zero, index: nextIndex);
await _audioPlayer.play();
} else {
// 已经是最后一个项目
// 确保最后一个项目播放完成
if (_audioPlayer.position < _audioPlayer.duration!) {
await _audioPlayer.seek(_audioPlayer.duration!);
}
}
} catch (e) {
// 简单记录错误,不做复杂处理
}
}
/// 连接到火山语音服务
Future<bool> connect() async {
if (_isConnected) {
Logger.info('TTS已连接,跳过连接');
return true; // 已经连接
}
isConnecting.value = true;
try {
Logger.info('开始TTS连接');
// 生成连接ID
_connectionId = const Uuid().v4();
// 创建WebSocket连接
final uri = Uri.parse(_serviceUrl);
if (!kIsWeb && (io.Platform.isAndroid || io.Platform.isIOS || io.Platform.isMacOS || io.Platform.isLinux || io.Platform.isWindows)) {
// 移动平台和桌面平台 - 使用IOWebSocketChannel
_channel = IOWebSocketChannel.connect(
uri,
headers: {
'X-Api-App-Key': _appId,
'X-Api-Access-Key': _token,
'X-Api-Resource-Id': _resourceId,
'X-Api-Connect-Id': _connectionId!,
},
);
} else {
// Web平台 - 使用WebSocketChannel
// 注意:在Web平台上,我们无法直接设置WebSocket头部
// 这可能会导致认证失败,需要与服务提供商确认Web平台的认证方式
_channel = WebSocketChannel.connect(uri);
}
// 监听消息
_channel!.stream.listen(
_handleMessage,
onError: (error) {
// 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error');
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'error',
'error': error.toString()
});
}
},
onDone: _handleDone,
cancelOnError: false,
);
// 发送开始连接事件
await _startConnection();
// 等待连接成功
final completer = Completer<bool>();
// 设置超时
Timer? timeout;
timeout = Timer(const Duration(seconds: 10), () {
if (!completer.isCompleted) {
Logger.error('火山TTS连接超时');
completer.complete(false);
// 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常
try {
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'connectionFailed',
'error': '连接超时'
});
}
} catch (e) {
Logger.error('处理TTS连接超时事件失败: $e');
}
}
timeout = null;
});
// 监听连接事件
final subscription = eventStream.listen((event) {
if (event['eventType'] == 'connectionStarted') {
if (!completer.isCompleted) {
Logger.info('火山TTS连接成功');
completer.complete(true);
timeout?.cancel();
timeout = null;
}
} else if (event['eventType'] == 'connectionFailed') {
if (!completer.isCompleted) {
Logger.error('火山TTS连接失败: ${event['error']}');
completer.complete(false);
timeout?.cancel();
timeout = null;
}
}
});
// 等待连接结果
final result = await completer.future;
subscription.cancel();
if (result) {
_isConnected = true;
isConnected.value = true;
Logger.info('火山TTS连接成功完成');
}
return result;
} catch (e) {
// 只记录错误,不尝试重连
Logger.error('连接过程中发生异常: $e');
return false;
} finally {
isConnecting.value = false;
}
}
/// 开始TTS会话
///
/// 用户需要自行管理会话状态,确保在使用完毕后调用 endSession
///
/// [speaker] 发音人ID
///
/// 返回会话是否成功开始
Future<bool> startSession(String speaker) async {
Logger.info('开始TTS会话');
try {
if (!_isConnected) {
final connected = await connect();
Logger.info('TTS未连接状态, 开始连接, 连接结果: $connected');
if (!connected) return false;
}
// 生成会话ID
_sessionId = const Uuid().v4().replaceAll('-', '');
// 发送开始会话事件
await _startTtsSession(speaker);
// 等待会话开始
final completer = Completer<bool>();
// 设置超时
Timer? timeout;
timeout = Timer(const Duration(seconds: 10), () {
if (!completer.isCompleted) {
Logger.error('火山TTS会话开始超时');
completer.complete(false);
// 使用 try-catch 包裹错误处理,避免在计时器回调中出现未捕获的异常
try {
_eventController.add({
'eventType': 'sessionFailed',
'error': '会话开始超时'
});
} catch (e) {
Logger.error('处理TTS会话超时事件失败: $e');
}
}
timeout = null;
});
// 监听会话事件
final subscription = eventStream.listen((event) {
if (event['eventType'] == 'sessionStarted') {
if (!completer.isCompleted) {
Logger.info('火山TTS会话开始成功');
completer.complete(true);
timeout?.cancel();
timeout = null;
}
} else if (event['eventType'] == 'sessionFailed') {
if (!completer.isCompleted) {
Logger.error('火山TTS会话开始失败: ${event['error']}');
completer.complete(false);
timeout?.cancel();
timeout = null;
}
}
});
// 等待会话结果
final result = await completer.future;
subscription.cancel();
if (result) {
Logger.info('火山TTS会话已完全建立');
}
return result;
} catch (e) {
// 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e');
return false;
}
}
/// 合成文本
Future<bool> synthesize(String text, String speaker) async {
if (text.isEmpty) {
Logger.warning('合成文本为空,跳过合成');
return false;
}
try {
// 确保已连接
if (!_isConnected) {
Logger.error('TTS未连接,无法合成文本');
return false;
}
// 确保会话已开始
if (_sessionId == null) {
Logger.error('TTS会话未开始,请先调用startSession');
return false;
}
// 设置合成状态
isSynthesizing.value = true;
// 发送合成请求
await _sendTtsMessage(speaker, text);
return true;
} catch (e) {
_handleError(e);
return false;
}
}
/// 结束会话
///
/// 结束当前TTS会话,释放相关资源
/// 用户应在使用完毕后调用此方法
///
/// 返回会话是否成功结束
Future<bool> endSession() async {
if (_sessionId == null) {
return true; // 没有活跃会话
}
Logger.info('TTS结束会话');
try {
// 发送结束会话事件
await _finishSession();
// 等待会话结束
final completer = Completer<bool>();
// 设置超时
final timeout = Timer(const Duration(seconds: 5), () {
if (!completer.isCompleted) {
completer.complete(true); // 超时也认为成功
}
});
// 监听会话事件
final subscription = eventStream.listen((event) {
if (event['eventType'] == 'sessionFinished') {
if (!completer.isCompleted) {
completer.complete(true);
timeout.cancel();
}
}
});
// 等待会话结果
final result = await completer.future;
subscription.cancel();
// 清除会话ID
_sessionId = null;
isSynthesizing.value = false;
return result;
} catch (e) {
_handleError(e);
return false;
}
}
/// 断开连接
Future<bool> disconnect() async {
if (!_isConnected) {
return true; // 已经断开
}
print('断开TTS连接');
try {
// 如果有活跃会话,先结束会话
if (_sessionId != null) {
await endSession();
}
// 发送结束连接事件
await _finishConnection();
// 等待连接结束
final completer = Completer<bool>();
// 设置超时
final timeout = Timer(const Duration(seconds: 5), () {
if (!completer.isCompleted) {
completer.complete(true); // 超时也认为成功
}
});
// 监听连接事件
final subscription = eventStream.listen((event) {
if (event['eventType'] == 'connectionFinished') {
if (!completer.isCompleted) {
completer.complete(true);
timeout.cancel();
}
}
});
// 等待连接结果
final result = await completer.future;
subscription.cancel();
// 关闭WebSocket连接
await _channel?.sink.close();
_channel = null;
_isConnected = false;
_sessionId = null;
isConnected.value = false;
isSynthesizing.value = false;
return result;
} catch (e) {
_handleError(e);
return false;
}
}
/// 处理接收到的消息
void _handleMessage(dynamic message) {
try {
if (message is! List<int>) {
_handleError(Exception('收到非二进制消息: $message'));
return;
}
// 转换为Uint8List以提高处理效率
final messageData = Uint8List.fromList(message);
final response = _parseResponse(messageData);
if (response == null) {
_handleError(Exception('解析响应失败'));
return;
}
// 处理事件
final event = response['event'] as int?;
if (event != null && !_isDisposed && !_eventController.isClosed) {
switch (event) {
case eventConnectionStarted:
_eventController.add({'eventType': 'connectionStarted'});
break;
case eventConnectionFailed:
_eventController.add({
'eventType': 'connectionFailed',
'error': response['errorDetails'] ?? '连接失败'
});
break;
case eventSessionStarted:
_eventController.add({'eventType': 'sessionStarted'});
break;
case eventSessionFailed:
_eventController.add({
'eventType': 'sessionFailed',
'error': response['errorDetails'] ?? '会话失败'
});
break;
case eventTtsSentenceStart:
_eventController.add({'eventType': 'ttsSentenceStart'});
_prepareAudioStream();
break;
case eventTtsSentenceEnd:
_eventController.add({'eventType': 'ttsSentenceEnd'});
// 结束音频流
_finishAudioStream();
break;
case eventTtsResponse:
// 处理音频数据
final payload = response['payload'] as Uint8List?;
if (payload != null && payload.isNotEmpty) {
try {
_eventController.add({'eventType': 'ttsResponse'});
if (!_isDisposed && !_audioDataController.isClosed) {
_audioDataController.add(payload);
}
// 将音频数据添加到流中
_addAudioData(payload);
} catch (audioError) {
// 继续处理,不中断整个流程
}
}
break;
case eventSessionFinished:
_eventController.add({'eventType': 'sessionFinished'});
break;
case eventConnectionFinished:
_eventController.add({'eventType': 'connectionFinished'});
break;
default:
_eventController.add({'eventType': 'unknown', 'eventCode': event});
break;
}
}
} catch (e) {
_handleError(e);
}
}
/// 准备音频流
void _prepareAudioStream() {
try {
// 清空音频缓冲区
_audioBuffer.clear();
} catch (e) {
if (kDebugMode) {
print('准备音频流时发生错误: $e');
}
}
}
/// 添加音频数据到流
void _addAudioData(Uint8List data) {
try {
// 将音频数据添加到缓冲区
if (data.isNotEmpty) {
_audioBuffer.add(data);
}
} catch (e) {
if (kDebugMode) {
print('添加音频数据到流时发生错误: $e');
}
// 不抛出异常,继续处理
}
}
/// 结束音频流
void _finishAudioStream() {
try {
if (_audioBuffer.isNotEmpty) {
// 合并所有音频数据
final totalSize = _audioBuffer.fold<int>(0, (sum, data) => sum + data.length);
final mergedData = Uint8List(totalSize);
int offset = 0;
for (final data in _audioBuffer) {
mergedData.setRange(offset, offset + data.length, data);
offset += data.length;
}
// 清空缓冲区
_audioBuffer.clear();
// 将合并后的数据添加到播放列表
_addToPlaylist(mergedData);
} else {
}
} catch (e) {
if (kDebugMode) {
print('结束音频流时发生错误: $e');
}
// 不抛出异常,避免中断整个流程
}
}
/// 将音频数据添加到播放列表
Future<void> _addToPlaylist(Uint8List audioData) async {
try {
if (audioData.isEmpty) return;
// 确保播放列表已初始化
if (!_playlistInitialized) {
await _initializePlaylist();
}
// 创建音频源
final audioSource = BytesAudioSource(audioData);
// 添加到播放列表
await _playlist.add(audioSource);
// 如果当前没有播放,开始播放
if (!_isPlaying.value) {
await _startPlayback();
} else if (_audioPlayer.processingState == ProcessingState.completed) {
// 如果播放已完成但状态还没更新,手动前进到下一个
await _advanceToNextItem();
}
} catch (e) {
// 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'playbackError',
'error': '添加音频到播放列表失败: $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();
} catch (e) {
// 直接报告错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'playbackError',
'error': '开始播放失败: $e'
});
}
}
}
/// 处理错误
void _handleError(dynamic error) {
try {
// 只记录错误,不尝试恢复
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({
'eventType': 'error',
'error': error.toString()
});
}
Logger.error('火山语音TTS错误: $error');
} catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error');
}
}
/// 处理连接关闭
void _handleDone() {
Logger.info('TTS WebSocket连接已关闭');
// 重置所有状态
_isConnected = false;
_sessionId = null;
isConnected.value = false;
isSynthesizing.value = false;
// 通知监听者连接已断开
if (!_isDisposed && !_eventController.isClosed) {
_eventController.add({'eventType': 'disconnected'});
}
}
/// 解析响应
Map<String, dynamic>? _parseResponse(Uint8List data) {
try {
if (data.length < 4) {
return null;
}
// 解析头部
final header = {
'protocol_version': (data[0] >> 4) & 0x0F,
'header_size': data[0] & 0x0F,
'message_type': (data[1] >> 4) & 0x0F,
'message_type_specific_flags': data[1] & 0x0F,
'serialization_method': (data[2] >> 4) & 0x0F,
'message_compression': data[2] & 0x0F,
'reserved': data[3],
};
final result = <String, dynamic>{
'header': header,
};
int offset = 4;
// 解析事件
if (header['message_type_specific_flags'] == _msgTypeFlagWithEvent) {
if (data.length < offset + 4) {
return result;
}
final event = _bytesToInt(data.sublist(offset, offset + 4));
result['event'] = event;
offset += 4;
}
// 根据消息类型和事件类型解析剩余数据
final messageType = header['message_type'];
final event = result['event'] as int?;
if (messageType == _fullServerResponse || messageType == _audioOnlyResponse) {
if (event != null) {
switch (event) {
case eventConnectionStarted:
if (data.length < offset + 4) break;
final connectionIdSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + connectionIdSize) break;
final connectionId = String.fromCharCodes(data.sublist(offset, offset + connectionIdSize));
result['connectionId'] = connectionId;
offset += connectionIdSize;
break;
case eventConnectionFailed:
if (data.length < offset + 4) break;
final metaSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + metaSize) break;
final metaJson = String.fromCharCodes(data.sublist(offset, offset + metaSize));
result['meta'] = metaJson;
offset += metaSize;
try {
final meta = jsonDecode(metaJson);
result['errorDetails'] = meta['error_msg'] ?? 'Unknown error';
} catch (_) {}
break;
case eventSessionStarted:
case eventSessionFailed:
case eventSessionFinished:
// 读取会话ID
if (data.length < offset + 4) break;
final sessionIdSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + sessionIdSize) break;
final sessionId = String.fromCharCodes(data.sublist(offset, offset + sessionIdSize));
result['sessionId'] = sessionId;
offset += sessionIdSize;
// 读取元数据
if (data.length < offset + 4) break;
final metaSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + metaSize) break;
final metaJson = String.fromCharCodes(data.sublist(offset, offset + metaSize));
result['meta'] = metaJson;
offset += metaSize;
try {
final meta = jsonDecode(metaJson);
if (meta['error_msg'] != null) {
result['errorDetails'] = meta['error_msg'];
}
} catch (_) {}
break;
default:
// 读取会话ID
if (data.length < offset + 4) break;
final sessionIdSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + sessionIdSize) break;
final sessionId = String.fromCharCodes(data.sublist(offset, offset + sessionIdSize));
result['sessionId'] = sessionId;
offset += sessionIdSize;
// 读取序列号
if (header['message_type_specific_flags'] == _msgTypeFlagNegativeSeq ||
header['message_type_specific_flags'] == _msgTypeFlagPositiveSeq) {
if (data.length < offset + 4) break;
final sequence = _bytesToInt(data.sublist(offset, offset + 4));
result['sequence'] = sequence;
offset += 4;
}
// 读取负载
if (data.length < offset + 4) break;
final payloadSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + payloadSize) break;
final payload = data.sublist(offset, offset + payloadSize);
result['payload'] = payload;
if (messageType == _fullServerResponse) {
try {
final payloadStr = String.fromCharCodes(payload);
result['payloadJson'] = payloadStr;
} catch (_) {}
}
break;
}
}
} else if (messageType == _errorInformation) {
if (data.length < offset + 4) {
return result;
}
final errorCode = _bytesToInt(data.sublist(offset, offset + 4));
result['errorCode'] = errorCode;
offset += 4;
// 读取负载
if (data.length < offset + 4) {
return result;
}
final payloadSize = _bytesToInt(data.sublist(offset, offset + 4));
offset += 4;
if (data.length < offset + payloadSize) {
return result;
}
final payload = data.sublist(offset, offset + payloadSize);
try {
final payloadStr = String.fromCharCodes(payload);
result['errorDetails'] = payloadStr;
} catch (_) {}
}
return result;
} catch (e) {
if (kDebugMode) {
print('解析响应失败: $e');
}
return null;
}
}
/// 开始连接
Future<void> _startConnection() async {
final header = _createHeader(
_fullClientRequest,
_msgTypeFlagWithEvent,
_jsonSerialization,
_compressionNo
);
final optional = _createOptional(eventStartConnection, null, -1);
final payload = utf8.encode('{}');
await _sendEvent(header, optional, payload);
}
/// 结束连接
Future<void> _finishConnection() async {
final header = _createHeader(
_fullClientRequest,
_msgTypeFlagWithEvent,
_jsonSerialization,
_compressionNo
);
final optional = _createOptional(eventFinishConnection, null, -1);
final payload = utf8.encode('{}');
await _sendEvent(header, optional, payload);
}
/// 开始TTS会话
Future<void> _startTtsSession(String speaker) async {
final header = _createHeader(
_fullClientRequest,
_msgTypeFlagWithEvent,
_jsonSerialization,
_compressionNo
);
final optional = _createOptional(eventStartSession, _sessionId, -1);
// 创建会话参数
final payloadMap = {
'user': {'uid': '123456'},
'event': eventStartSession,
'namespace': 'BidirectionalTTS',
'req_params': {
'speaker': speaker,
'audio_params': {
'format': 'mp3',
'sample_rate': 24000
}
}
};
final payload = utf8.encode(jsonEncode(payloadMap));
await _sendEvent(header, optional, payload);
}
/// 发送TTS消息
Future<void> _sendTtsMessage(String speaker, String text) async {
final header = _createHeader(
_fullClientRequest,
_msgTypeFlagWithEvent,
_jsonSerialization,
_compressionNo
);
final optional = _createOptional(eventTaskRequest, _sessionId, -1);
// 创建任务参数
final payloadMap = {
'user': {'uid': '123456'},
'event': eventTaskRequest,
'namespace': 'BidirectionalTTS',
'req_params': {
'text': text,
'speaker': speaker,
'audio_params': {
'format': 'mp3',
'sample_rate': 24000
}
}
};
final payload = utf8.encode(jsonEncode(payloadMap));
await _sendEvent(header, optional, payload);
}
/// 结束会话
Future<void> _finishSession() async {
final header = _createHeader(
_fullClientRequest,
_msgTypeFlagWithEvent,
_jsonSerialization,
_compressionNo
);
final optional = _createOptional(eventFinishSession, _sessionId, -1);
final payload = utf8.encode('{}');
await _sendEvent(header, optional, payload);
}
/// 创建头部
Uint8List _createHeader(
int messageType,
int messageTypeSpecificFlags,
int serializationMethod,
int messageCompression
) {
return Uint8List.fromList([
// Protocol version | Header size
((_protocolVersion << 4) | _defaultHeaderSize) & 0xFF,
// Message type | Message type specific flags
((messageType << 4) | messageTypeSpecificFlags) & 0xFF,
// Serialization method | Compression method
((serializationMethod << 4) | messageCompression) & 0xFF,
// Reserved
0
]);
}
/// 创建可选部分
Uint8List _createOptional(int event, String? sessionId, int sequence) {
final buffer = BytesBuilder();
// 添加事件
if (event != eventNone) {
buffer.add(_intToBytes(event));
}
// 添加会话ID
if (sessionId != null) {
final sessionIdBytes = utf8.encode(sessionId);
buffer.add(_intToBytes(sessionIdBytes.length));
buffer.add(sessionIdBytes);
}
// 添加序列号
if (sequence >= 0) {
buffer.add(_intToBytes(sequence));
}
return buffer.toBytes();
}
/// 发送事件
Future<void> _sendEvent(Uint8List header, Uint8List optional, List<int> payload) async {
if (_channel == null) {
throw Exception('WebSocket未连接');
}
final buffer = BytesBuilder();
// 添加头部
buffer.add(header);
// 添加可选部分
if (optional.isNotEmpty) {
buffer.add(optional);
}
// 添加负载大小
buffer.add(_intToBytes(payload.length));
// 添加负载
buffer.add(payload);
// 发送数据
_channel!.sink.add(buffer.toBytes());
}
/// 整数转字节数组
Uint8List _intToBytes(int value) {
return Uint8List(4)
..[0] = (value >> 24) & 0xFF
..[1] = (value >> 16) & 0xFF
..[2] = (value >> 8) & 0xFF
..[3] = value & 0xFF;
}
/// 字节数组转整数
int _bytesToInt(Uint8List bytes) {
if (bytes.length != 4) {
throw ArgumentError('字节数组长度必须为4');
}
return ((bytes[0] & 0xFF) << 24) |
((bytes[1] & 0xFF) << 16) |
((bytes[2] & 0xFF) << 8) |
(bytes[3] & 0xFF);
}
/// 释放资源
@override
void onClose() async {
_isDisposed = true;
await disconnect();
// 关闭控制器
if (!_audioDataController.isClosed) {
await _audioDataController.close();
}
if (!_eventController.isClosed) {
await _eventController.close();
}
// 释放资源
_audioPlayer.dispose();
super.onClose();
}
/// 合成并播放文本
Future<bool> speak(String text, {String? speaker}) async {
if (text.isEmpty) return false;
try {
// 使用默认发音人
final actualSpeaker = speaker ?? defaultSpeaker;
// 合成文本
final success = await synthesize(text, actualSpeaker);
return success;
} catch (e) {
_handleError(e);
return false;
}
}
/// 停止播放
Future<void> stop() async {
try {
// 停止音频播放
await _audioPlayer.stop();
// 清空播放列表
await _playlist.clear();
// 结束当前会话
await endSession();
// 清空音频缓冲区
_audioBuffer.clear();
} catch (e) {
_handleError(e);
}
}
/// 检查是否正在播放
bool isActuallyPlaying() {
return _isPlaying.value;
}
/// 检查会话是否活跃
bool isSessionActive() {
return _sessionId != null;
}
/// 设置音量
Future<void> setVolume(double volume) async {
try {
await _audioPlayer.setVolume(volume.clamp(0.0, 1.0));
} catch (e) {
_handleError(e);
}
}
/// 设置播放速度
Future<void> setSpeed(double speed) async {
try {
await _audioPlayer.setSpeed(speed.clamp(0.5, 2.0));
} catch (e) {
_handleError(e);
}
}
/// 切换启用状态
void toggleEnabled() {
// 这个方法在当前实现中不需要特殊处理
}
/// 单句合成辅助函数
///
/// 一次性完成连接、会话创建、合成和播放的过程
/// 适合单句或少量文本的快速合成场景
///
/// [text] 要合成的文本
/// [speaker] 发音人,如果为null则使用默认发音人
///
/// 返回合成是否成功
Future<bool> speakSingle(String text, {String? speaker}) async {
if (text.isEmpty) {
Logger.warning('合成文本为空,跳过合成');
return false;
}
try {
// 使用默认发音人
final actualSpeaker = speaker ?? defaultSpeaker;
// 1. 确保连接
if (!_isConnected) {
Logger.info('TTS未连接,开始连接');
final connected = await connect();
if (!connected) {
Logger.error('TTS连接失败,无法合成文本');
return false;
}
}
// 2. 确保会话已开始
final bool needNewSession = _sessionId == null;
if (needNewSession) {
Logger.info('TTS会话未开始,开始会话');
final sessionStarted = await startSession(actualSpeaker);
if (!sessionStarted) {
Logger.error('TTS会话开始失败,无法合成文本');
return false;
}
}
// 3. 合成文本
isSynthesizing.value = true;
final success = await synthesize(text, actualSpeaker);
// 如果是我们开始的新会话,则结束会话
if (needNewSession) {
await endSession();
}
return success;
} catch (e) {
_handleError(e);
return false;
}
}
}
/// 使用 BytesAudioSource 进行音频播放
/// 这种方式更简单可靠,适合已经收集完整的音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
final String _contentType;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(List<int> bytes, {String contentType = 'audio/mpeg'})
: _bytes = bytes is Uint8List ? bytes : Uint8List.fromList(bytes),
_contentType = contentType;
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
try {
start = start ?? 0;
end = end ?? _bytes.length;
// 确保范围有效
final int validStart = math.min(math.max(0, start), _bytes.length);
final int validEnd = math.min(math.max(validStart, end), _bytes.length);
// 提取请求的数据范围
final data = _bytes.sublist(validStart, validEnd);
if (kDebugMode && data.length > 1024) {
print('BytesAudioSource 请求数据: $validStart-$validEnd (${data.length} 字节)');
}
// 使用固定大小的块进行流式传输,提高兼容性
final chunks = <List<int>>[];
var offset = 0;
while (offset < data.length) {
final chunkSize = math.min(_bufferSize, data.length - offset);
chunks.add(data.sublist(offset, offset + chunkSize));
offset += chunkSize;
}
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: data.length,
offset: validStart,
stream: Stream.fromIterable(chunks),
contentType: _contentType,
);
} catch (e) {
if (kDebugMode) {
print('BytesAudioSource 请求处理错误: $e');
}
// 返回一个空响应而不是抛出异常,提高容错性
return StreamAudioResponse(
sourceLength: _bytes.length,
contentLength: 0,
offset: 0,
stream: Stream.value(Uint8List(0)),
contentType: _contentType,
);
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}