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.

743 lines
22 KiB

import 'dart:async';
import 'dart:convert';
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:web_socket_channel/web_socket_channel.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../core/utils/logger.dart';
/// 火山语音服务异常
class VolcanoTtsException implements Exception {
final String message;
VolcanoTtsException(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) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length);
}
/// 火山语音合成服务
class VolcanoTtsService extends GetxService {
static const String _host = 'openspeech.bytedance.com';
static const String _apiUrl = 'wss://$_host/api/v1/tts/ws_binary';
// 配置参数
final String _appId;
final String _token;
final String _cluster;
final String _voiceType;
// 音频播放器
late AudioPlayer _audioPlayer;
final ConcatenatingAudioSource _playlist = ConcatenatingAudioSource(children: []);
final isEnabled = true.obs; // 默认启用
final isPlaying = false.obs;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// WebSocket 相关
WebSocketChannel? _channel;
bool _isDisposed = false;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
static final _sentenceBreaks = RegExp(r'[。!?.!?,,、]'); // 添加更多标点符号
bool _isFetching = false;
static const int _maxSegmentLength = 100; // 最大分段长度
static const int _optimalSegmentLength = 50; // 最佳分段长度
// WebSocket连接配置
static final Map<String, String> _wsHeaders = {
'User-Agent': 'DeepVoice/1.0',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Extensions': 'permessage-deflate',
'Origin': 'https://openspeech.bytedance.com',
};
// 二进制协议头部字段定义
static const int PROTOCOL_VERSION = 0x1; // 0b0001 - 版本1
static const int HEADER_SIZE = 0x1; // 0b0001 - 4字节
static const int MESSAGE_TYPE = 0x1; // 0b0001 - full client request
static const int MESSAGE_FLAGS = 0x0; // 0b0000 - 无特殊标记
static const int SERIALIZATION = 0x1; // 0b0001 - JSON
static const int COMPRESSION = 0x0; // 0b0000 - 无压缩
static const int RESERVED = 0x0; // 0b0000 - 保留字段
// 构建二进制协议头
static Uint8List _buildHeader() {
final header = ByteData(4); // 4字节的头部
// 第一个字节: [协议版本(4位) | 报头大小(4位)]
header.setUint8(0, (PROTOCOL_VERSION << 4) | HEADER_SIZE);
// 第二个字节: [消息类型(4位) | 消息标记(4位)]
header.setUint8(1, (MESSAGE_TYPE << 4) | MESSAGE_FLAGS);
// 第三个字节: [序列化方法(4位) | 压缩方法(4位)]
header.setUint8(2, (SERIALIZATION << 4) | COMPRESSION);
// 第四个字节: [保留字段(8位)]
header.setUint8(3, RESERVED);
return header.buffer.asUint8List();
}
VolcanoTtsService() :
_appId = dotenv.env['VOLCANO_TTS_APP_ID'] ?? '',
_token = dotenv.env['VOLCANO_TTS_APP_TOKEN'] ?? '',
_cluster = dotenv.env['VOLCANO_TTS_CLUSTER'] ?? '',
_voiceType = dotenv.env['VOLCANO_TTS_VOICE_TYPE'] ?? 'zh_male_M392_conversation_wvae_bigtts' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) {
throw Exception('火山语音配置信息不完整,请检查环境变量');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initAudioSession();
await _initAudioPlayer();
await useDefaultAudioDevice();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
Future<void> _initAudioSession() async {
_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,
androidWillPauseWhenDucked: true,
));
// 确保音频会话激活并设置正确的音频模式
if (Platform.isAndroid) {
await _audioSession?.setActive(true, avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation);
} else {
await _audioSession?.setActive(true);
}
}
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,
);
_setupEventListeners();
}
Future<void> speak(String text) async {
if (!isEnabled.value || text.trim().isEmpty) return;
try {
_pendingText += text;
_extractFullSentences();
await _startPreloadIfNeeded();
} catch (e, stack) {
print('语音合成失败: $e');
await _reinitializePlayer();
}
}
void _extractFullSentences() {
if (_pendingText.isEmpty) return;
final matches = _sentenceBreaks.allMatches(_pendingText).toList();
if (matches.isEmpty) {
// 如果没有找到句子分隔符,按长度分割
_splitByLength(_pendingText);
_pendingText = '';
return;
}
int lastPos = 0;
String currentBatch = '';
for (final match in matches) {
final end = match.end;
final sentence = _pendingText.substring(lastPos, end).trim();
if (sentence.isEmpty) {
lastPos = end;
continue;
}
// 如果当前批次为空,直接添加句子
if (currentBatch.isEmpty) {
currentBatch = sentence;
}
// 如果当前批次加上新句子不超过30字符,则合并
else if ((currentBatch + sentence).length <= 30) {
currentBatch += sentence;
}
// 如果超过30字符,将当前批次加入队列,开始新批次
else {
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
currentBatch = sentence;
}
lastPos = end;
}
// 处理最后一个批次
if (currentBatch.isNotEmpty) {
_sentenceQueue.add(currentBatch);
}
// 处理剩余的文本
if (lastPos < _pendingText.length) {
final remaining = _pendingText.substring(lastPos).trim();
if (remaining.isNotEmpty) {
if (remaining.length <= 30) {
// 尝试与最后一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + remaining).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += remaining;
} else {
_sentenceQueue.add(remaining);
}
} else {
_splitByLength(remaining);
}
}
}
_pendingText = '';
}
void _splitByLength(String text) {
if (text.isEmpty) return;
int start = 0;
while (start < text.length) {
int end = math.min(start + 30, text.length);
// 尝试在标点或空格处分割
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) {
// 尝试与前一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + segment).length <= 30) {
_sentenceQueue[_sentenceQueue.length - 1] += segment;
} else {
_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) {
print('获取音频片段: $segment');
final audioData = await _synthesize(segment);
if (audioData != null) {
print('成功获取音频数据: ${audioData.length} bytes');
try {
final audioSource = BytesAudioSource(audioData);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
// 如果是第一个片段且播放器未在播放,开始播放
if (!_audioPlayer.playing && _playlist.length == 1) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 设置播放参数
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 开始播放
await _audioPlayer.play();
}
} catch (e) {
print('添加音频源失败: $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) {
print('获取音频失败: $e');
print('Stack trace: $stackTrace');
_isFetching = false;
}
}
Future<Uint8List?> _synthesize(String text) async {
WebSocket? socket;
StreamSubscription? subscription;
try {
final requestJson = {
'app': {
'appid': _appId,
'token': _token,
'cluster': _cluster,
},
'user': {
'uid': '${DateTime.now().millisecondsSinceEpoch}',
},
'audio': {
'voice_type': _voiceType,
'encoding': 'mp3',
'speed_ratio': 1.0,
'volume_ratio': 1.0,
'pitch_ratio': 1.0,
'language': 'zh',
},
'request': {
'reqid': '${DateTime.now().millisecondsSinceEpoch}',
'text': text,
'text_type': 'plain',
'operation': 'query',
},
};
final jsonBytes = utf8.encode(json.encode(requestJson));
final header = _buildHeader();
final fullRequest = Uint8List(header.length + 4 + jsonBytes.length);
fullRequest.setAll(0, header);
final lengthBytes = ByteData(4)..setInt32(0, jsonBytes.length, Endian.big);
fullRequest.setAll(header.length, lengthBytes.buffer.asUint8List());
fullRequest.setAll(header.length + 4, jsonBytes);
final wsUrl = Uri.parse(_apiUrl);
final completer = Completer<Uint8List?>();
socket = await WebSocket.connect(
wsUrl.toString(),
headers: {
..._wsHeaders,
'Authorization': 'Bearer;$_token',
},
).timeout(
const Duration(seconds: 5),
onTimeout: () {
throw TimeoutException('WebSocket连接超时');
},
);
subscription = socket.listen(
(response) {
if (!completer.isCompleted && response is Uint8List) {
if (response.length < 2) return;
final messageType = response[1] >> 4;
if (messageType == 0x0B) {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length > payloadStart) {
final audioData = response.sublist(payloadStart);
completer.complete(audioData);
}
} else if (messageType == 0x0F) {
try {
final headerSize = response[0] & 0x0F;
final payloadStart = headerSize * 4;
if (response.length < payloadStart + 4) {
throw VolcanoTtsException('错误响应数据不完整');
}
final jsonData = response.sublist(payloadStart + 4);
final jsonString = utf8.decode(jsonData, allowMalformed: true);
if (jsonString.trim().startsWith('{')) {
final errorMap = json.decode(jsonString);
throw VolcanoTtsException(errorMap['error'] as String? ?? '未知错误');
}
} catch (e) {
if (e is VolcanoTtsException) rethrow;
throw VolcanoTtsException('服务返回错误: $e');
}
}
}
},
onError: (error) {
if (!completer.isCompleted) {
completer.completeError(error);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete(null);
}
},
cancelOnError: false,
);
socket.add(fullRequest);
return await completer.future.timeout(
const Duration(seconds: 10),
onTimeout: () => null,
);
} catch (e) {
print('合成请求失败: $e');
return null;
} finally {
await subscription?.cancel();
await socket?.close();
}
}
Future<void> _playAudio(Uint8List audioData) async {
if (_isDisposed) {
print('TTS服务已销毁,跳过播放');
return;
}
try {
print('准备播放音频数据: ${audioData.length} bytes');
final audioSource = BytesAudioSource(audioData);
print('清除播放列表');
await _playlist.clear();
print('添加新的音频源到播放列表');
await _playlist.add(audioSource);
if (!_audioPlayer.playing) {
print('设置音量和播放位置');
await _audioPlayer.setVolume(1.0);
await _audioPlayer.seek(Duration.zero, index: 0);
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
print('等待50ms确保音频准备就绪');
await Future.delayed(const Duration(milliseconds: 50));
print('开始播放');
await _audioPlayer.play();
print('播放命令已发送');
} else {
print('播放器已在播放中');
}
} catch (e, stack) {
print('播放音频失败: $e');
print('错误堆栈: $stack');
await _reinitializePlayer();
}
}
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
isPlaying.value = false;
// 确保清空所有待处理的音频数据
try {
// 取消所有正在进行的WebSocket连接
await _channel?.sink.close();
_channel = null;
// 重置播放器状态
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
print('已清空所有待播放的文字和音频');
} catch (e) {
print('清空音频缓存时出错: $e');
}
} catch (e) {
print('停止播放失败: $e');
}
}
// 添加一个方法来检查音频播放器是否真正在播放
bool isActuallyPlaying() {
try {
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 只有当播放器正在播放且播放列表不为空时,才认为真正在播放
final actuallyPlaying = isPlayerPlaying && hasAudioSource;
// 如果状态不一致,记录日志
if (isPlaying.value != actuallyPlaying) {
print('音频播放状态不一致: isPlaying.value=${isPlaying.value}, 实际播放状态=$actuallyPlaying (播放器状态=$isPlayerPlaying, 播放列表长度=${_playlist.length})');
}
return actuallyPlaying;
} catch (e) {
print('检查实际播放状态失败: $e');
// 如果检查失败,返回当前的 isPlaying 值
return isPlaying.value;
}
}
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化音频会话
await _initAudioSession();
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(1.0);
await _audioPlayer.setLoopMode(LoopMode.off);
// 重新配置Android特定的播放参数
await _audioPlayer.setAndroidAudioAttributes(const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
));
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
print('播放器重新初始化成功');
} catch (e) {
print('播放器重新初始化失败: $e');
}
}
void _setupEventListeners() {
_playbackEventSubscription?.cancel();
_playerStateSubscription?.cancel();
_playbackEventSubscription = _audioPlayer.playbackEventStream.listen(
(event) {
if (_isDisposed) return;
if (event.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
isPlaying.value = state.playing;
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
_reinitializePlayer();
},
);
}
Future<void> _tryPlayNext() async {
if (_isDisposed || _playlist.length <= 0) return;
try {
if (!_audioPlayer.playing) {
// 确保音频会话处于激活状态
await _audioSession?.setActive(true);
// 减少延迟时间
await Future.delayed(const Duration(milliseconds: 30));
if (!_isDisposed && !_audioPlayer.playing) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
}
}
} catch (e) {
if (e is! StateError) {
print('播放错误: $e');
await _reinitializePlayer();
}
}
}
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await _channel?.sink.close();
await stop();
await _audioPlayer.dispose();
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
/// 使用系统默认音频设备
Future<bool> useDefaultAudioDevice() async {
try {
if (Platform.isAndroid) {
// Android 平台 - 重置为正常模式,让系统自动选择
final audioManager = AndroidAudioManager();
await audioManager.setMode(AndroidAudioHardwareMode.normal);
await audioManager.stopBluetoothSco();
await audioManager.setBluetoothScoOn(false);
// 不显式设置扬声器状态,让系统自动选择
await audioManager.setSpeakerphoneOn(false);
return true;
} else if (Platform.isIOS) {
// iOS 平台 - 清除任何输出端口覆盖
final avAudioSession = AVAudioSession();
await avAudioSession.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
return true;
}
return false;
} catch (e) {
Logger.error('设置默认音频设备失败: $e');
return false;
}
}
}