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.

801 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/services.dart';
/// 火山语音服务异常
class VolcanoTtsException implements Exception {
final String message;
final dynamic originalError;
VolcanoTtsException(this.message, [this.originalError]);
@override
String toString() => originalError != null
? '$message (原始错误: $originalError)'
: message;
}
/// 自定义音频源,用于从字节数组读取音频数据
class BytesAudioSource extends StreamAudioSource {
final Uint8List _bytes;
static const int _bufferSize = 4096; // 4KB buffer size for better Android compatibility
BytesAudioSource(this._bytes);
// 添加 PCM 头信息,转换为 WAV 格式
Uint8List _addWavHeader(Uint8List pcmData) {
// PCM 参数
const int sampleRate = 16000; // 采样率
const int numChannels = 1; // 单声道
const int bitsPerSample = 16; // 16位采样
// 计算数据大小
final int dataSize = pcmData.length;
final int fileSize = 36 + dataSize;
// 创建 WAV 头
final ByteData header = ByteData(44);
// RIFF 头
header.setUint8(0, 'R'.codeUnitAt(0));
header.setUint8(1, 'I'.codeUnitAt(0));
header.setUint8(2, 'F'.codeUnitAt(0));
header.setUint8(3, 'F'.codeUnitAt(0));
// 文件大小
header.setUint32(4, fileSize, Endian.little);
// WAVE 标识
header.setUint8(8, 'W'.codeUnitAt(0));
header.setUint8(9, 'A'.codeUnitAt(0));
header.setUint8(10, 'V'.codeUnitAt(0));
header.setUint8(11, 'E'.codeUnitAt(0));
// fmt 子块
header.setUint8(12, 'f'.codeUnitAt(0));
header.setUint8(13, 'm'.codeUnitAt(0));
header.setUint8(14, 't'.codeUnitAt(0));
header.setUint8(15, ' '.codeUnitAt(0));
// 子块大小
header.setUint32(16, 16, Endian.little);
// 音频格式 (PCM = 1)
header.setUint16(20, 1, Endian.little);
// 声道数
header.setUint16(22, numChannels, Endian.little);
// 采样率
header.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 声道数 * 采样位数 / 8
header.setUint32(28, sampleRate * numChannels * bitsPerSample ~/ 8, Endian.little);
// 块对齐 = 声道数 * 采样位数 / 8
header.setUint16(32, numChannels * bitsPerSample ~/ 8, Endian.little);
// 采样位数
header.setUint16(34, bitsPerSample, Endian.little);
// data 子块
header.setUint8(36, 'd'.codeUnitAt(0));
header.setUint8(37, 'a'.codeUnitAt(0));
header.setUint8(38, 't'.codeUnitAt(0));
header.setUint8(39, 'a'.codeUnitAt(0));
// 数据大小
header.setUint32(40, dataSize, Endian.little);
// 创建完整的 WAV 数据
final Uint8List wavData = Uint8List(44 + dataSize);
wavData.setRange(0, 44, header.buffer.asUint8List());
wavData.setRange(44, 44 + dataSize, pcmData);
return wavData;
}
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
// 添加 WAV 头信息
final Uint8List wavData = _addWavHeader(_bytes);
start = start ?? 0;
end = end ?? wavData.length;
try {
final subData = wavData.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: wavData.length,
contentLength: subData.length,
offset: start,
stream: Stream.fromIterable(chunks),
contentType: 'audio/wav',
);
} catch (e) {
print('BytesAudioSource request error: $e');
rethrow;
}
}
@override
Future<int> get length => Future.value(_bytes.length + 44); // PCM 数据长度 + WAV 头长度
}
/// 火山语音合成服务 (SDK版本)
class VolcanoTtsService extends GetxService {
// 平台通道
static const MethodChannel _channel = MethodChannel('com.example.deep_voice/volcano_tts');
// 配置参数
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 _playingStateController = StreamController<bool>.broadcast();
Stream<bool> get playingStateStream => _playingStateController.stream;
StreamSubscription? _playbackEventSubscription;
StreamSubscription? _playerStateSubscription;
AudioSession? _audioSession;
// 句子管理
String _pendingText = '';
final List<String> _sentenceQueue = [];
// 优化的句子分割正则表达式,包含更多中英文标点
static final _sentenceBreaks = RegExp(r'[。!?.!?;;::,,、]');
// 优化的句子分割参数
static const int _maxSegmentLength = 150; // 最大分段长度
static const int _optimalSegmentLength = 80; // 最佳分段长度
static const int _minSegmentLength = 5; // 最小分段长度
bool _isFetching = false;
int _consecutiveErrorCount = 0;
static const int _maxConsecutiveErrors = 3;
// 初始化状态
bool _isInitialized = false;
bool _isDisposed = false;
// 性能优化参数
final int _preloadCount = 2; // 预加载片段数量
final int _maxRetryAttempts = 1; // 不进行重试
// 音频质量参数
final double _defaultVolume = 1.0;
final double _defaultSpeed = 1.0;
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'] ?? '' {
if (_appId.isEmpty || _token.isEmpty || _cluster.isEmpty) {
throw VolcanoTtsException('火山语音配置信息不完整,请检查环境变量 VOLCANO_TTS_APP_ID, VOLCANO_TTS_APP_KEY 和 VOLCANO_TTS_CLUSTER');
}
}
@override
Future<void> onInit() async {
super.onInit();
try {
await _initAudioSession();
await _initAudioPlayer();
await _initializeTtsEngine();
} catch (e, stackTrace) {
print('初始化TTS服务失败: $e');
print('Stack trace: $stackTrace');
}
}
/// 初始化TTS引擎
Future<void> _initializeTtsEngine() async {
try {
print('开始初始化火山语音TTS引擎,APP_ID: ${_appId.substring(0, math.min(3, _appId.length))}***,APP_KEY: ${_token.length > 10 ? "${_token.substring(0, 5)}..." : _token}');
final result = await _channel.invokeMethod<bool>('initialize', {
'appId': _appId,
'token': _token,
'cluster': _cluster,
});
_isInitialized = result ?? false;
print('火山语音TTS引擎初始化${_isInitialized ? '成功' : '失败'}');
if (!_isInitialized) {
throw VolcanoTtsException('TTS引擎初始化失败');
}
} catch (e) {
print('初始化火山语音TTS引擎失败: $e');
_isInitialized = false;
throw VolcanoTtsException('初始化失败', e);
}
}
Future<void> _initAudioSession() async {
_audioSession = await AudioSession.instance;
// 针对不同平台优化音频会话配置
if (Platform.isAndroid) {
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
usage: AndroidAudioUsage.media,
flags: AndroidAudioFlags.audibilityEnforced,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
} else if (Platform.isIOS) {
await _audioSession?.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playback,
avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.duckOthers,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.notifyOthersOnDeactivation,
));
}
// 确保音频会话激活并设置正确的音频模式
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(_defaultVolume);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(_defaultSpeed);
// 设置音频缓冲配置,启用预加载以减少延迟
await _audioPlayer.setAudioSource(
_playlist,
initialPosition: Duration.zero,
preload: true,
);
_setupEventListeners();
}
/// 合成并播放文本
///
/// [text] 要合成的文本
/// [voiceType] 可选的语音类型,如果提供,将覆盖默认语音类型
Future<void> speak(String text, {String? voiceType}) async {
if (!isEnabled.value || text.trim().isEmpty) return;
try {
// 如果提供了语音类型,记录日志
if (voiceType != null && voiceType != _voiceType) {
print('使用临时语音类型: $voiceType (默认: $_voiceType)');
}
_pendingText += text;
_extractSentences();
await _startPreloadIfNeeded(voiceType: voiceType);
} catch (e, stack) {
print('语音合成失败: $e');
print('Stack trace: $stack');
_consecutiveErrorCount++;
if (_consecutiveErrorCount >= _maxConsecutiveErrors) {
print('连续错误次数过多,尝试重新初始化播放器');
await _reinitializePlayer();
_consecutiveErrorCount = 0;
}
}
}
/// 优化的文本分段算法
void _extractSentences() {
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;
}
// 如果当前批次加上新句子不超过最佳长度,则合并
else if ((currentBatch + sentence).length <= _optimalSegmentLength) {
currentBatch += sentence;
}
// 如果超过最佳长度,将当前批次加入队列,开始新批次
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 <= _optimalSegmentLength) {
// 尝试与最后一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + remaining).length <= _optimalSegmentLength) {
_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 + _optimalSegmentLength, text.length);
// 如果没有到达文本末尾,尝试在标点或空格处分割
if (end < text.length) {
// 在理想长度附近查找标点
int searchStart = math.max(start + _minSegmentLength, end - 20);
int searchEnd = math.min(text.length, end + 20);
String searchText = text.substring(searchStart, searchEnd);
final breakMatch = _sentenceBreaks.firstMatch(searchText);
if (breakMatch != null) {
end = searchStart + breakMatch.end;
} else {
// 如果没有找到标点,尝试在空格处分割
final spacePos = text.lastIndexOf(' ', end);
if (spacePos > start && spacePos > end - 20) {
end = spacePos + 1; // 包含空格
}
}
}
final segment = text.substring(start, end).trim();
if (segment.isNotEmpty) {
// 尝试与前一个批次合并
if (_sentenceQueue.isNotEmpty &&
(_sentenceQueue.last + segment).length <= _optimalSegmentLength) {
_sentenceQueue[_sentenceQueue.length - 1] += segment;
} else {
_sentenceQueue.add(segment);
}
}
start = end;
}
}
Future<void> _startPreloadIfNeeded({String? voiceType}) async {
if (_isFetching || _sentenceQueue.isEmpty) return;
_isFetching = true;
await _fetchNextSegment(voiceType: voiceType);
}
Future<void> _fetchNextSegment({String? voiceType}) async {
if (_sentenceQueue.isEmpty) {
_isFetching = false;
return;
}
try {
// 预加载多个片段以实现流畅播放
final segments = <String>[];
while (_sentenceQueue.isNotEmpty && segments.length < _preloadCount) {
segments.add(_sentenceQueue.removeAt(0));
}
for (final segment in segments) {
print('获取音频片段: $segment');
try {
// 使用提供的语音类型或默认语音类型,只尝试一次
final audioData = await _synthesize(segment, voiceType: voiceType);
if (audioData != null) {
print('成功获取音频数据: ${audioData.length} bytes');
try {
final audioSource = BytesAudioSource(audioData);
await _playlist.add(audioSource);
print('音频片段已添加到播放列表');
// 如果是第一个片段且播放器未在播放,开始播放
if (!_audioPlayer.playing && _playlist.length == 1) {
print('开始播放');
await _audioPlayer.play();
}
} catch (e) {
print('添加音频到播放列表失败: $e');
}
} else {
print('获取音频失败: 合成返回空数据');
}
} catch (e) {
print('获取音频失败: $e');
// 不重试,继续处理下一个片段
}
}
} catch (e) {
print('预加载音频片段失败: $e');
} finally {
_isFetching = false;
}
}
/// 合成文本为音频数据
Future<Uint8List?> _synthesize(String text, {String? voiceType}) async {
if (text.isEmpty) {
return null;
}
// 使用指定的语音类型,不使用任何回退
final effectiveVoiceType = voiceType ?? _voiceType;
try {
// 调用原生方法合成语音
final result = await _channel.invokeMethod<Uint8List>('synthesizeSync', {
'text': text,
'voiceType': effectiveVoiceType,
});
if (result != null && result.isNotEmpty) {
return result;
}
// 如果没有结果,返回null
return null;
} catch (e) {
// 不处理授权错误,直接抛出所有错误
print('调用原生合成方法失败: $e');
throw VolcanoTtsException('调用原生合成方法失败', e);
}
}
/// 停止播放并清空队列
Future<void> stop() async {
try {
print('停止播放');
await _audioPlayer.stop();
await _playlist.clear();
_pendingText = '';
_sentenceQueue.clear();
_isFetching = false;
// 通知播放状态变化
_notifyPlayingStateChanged(false);
// 确保清空所有待处理的音频数据
try {
// 重置播放器状态
await _audioPlayer.pause();
await _audioPlayer.seek(Duration.zero);
print('已清空所有待播放的文字和音频');
} catch (e) {
print('清空音频缓存时出错: $e');
}
} catch (e) {
print('停止播放失败: $e');
}
}
/// 检查是否实际正在播放
bool isActuallyPlaying() {
try {
// 检查是否有活跃的播放任务
final hasActiveTask = _sentenceQueue.isNotEmpty || _isFetching;
// 检查播放器状态
final isPlayerPlaying = _audioPlayer.playing;
// 检查是否有待处理的文本
final hasPendingText = _pendingText.isNotEmpty;
// 检查播放列表是否为空
final hasAudioSource = _playlist.length > 0;
// 检查当前播放位置
final currentPosition = _audioPlayer.position;
final currentDuration = _audioPlayer.duration ?? Duration.zero;
// 检查是否已经播放到末尾但播放器状态未更新
final isAtEnd = currentDuration.inMilliseconds > 0 &&
currentPosition.inMilliseconds >= currentDuration.inMilliseconds - 100;
// 如果播放器显示正在播放,但已经到达音频末尾,则认为实际上没有播放
if (isPlayerPlaying && isAtEnd) {
// 如果检测到这种情况,尝试自动修复播放器状态
_fixPlayerState();
return false;
}
// 如果有活跃任务、播放器正在播放且未到末尾、有待处理文本,则认为正在播放
return hasActiveTask || (isPlayerPlaying && !isAtEnd) || hasPendingText;
} catch (e) {
print('检查TTS实际播放状态出错: $e');
return false;
}
}
/// 修复播放器状态
void _fixPlayerState() {
try {
// 如果播放器显示正在播放,但实际上已经到达音频末尾,尝试修复状态
Future.microtask(() async {
try {
// 如果没有更多内容要播放,停止播放器
if (_sentenceQueue.isEmpty && _pendingText.isEmpty && !_isFetching) {
await _audioPlayer.stop();
// 如果播放列表中只有已播放完的项目,清空播放列表
if (_playlist.length > 0) {
await _playlist.clear();
}
// 通知播放状态变化
_notifyPlayingStateChanged(false);
} else {
// 如果还有内容要播放,尝试播放下一个
_tryPlayNext();
}
} catch (e) {
print('修复播放器状态失败: $e');
}
});
} catch (e) {
print('尝试修复播放器状态时出错: $e');
}
}
/// 通知播放状态变化
void _notifyPlayingStateChanged(bool isPlaying) {
try {
_playingStateController.add(isPlaying);
} catch (e) {
print('通知播放状态变化失败: $e');
}
}
/// 重新初始化播放器
Future<void> _reinitializePlayer() async {
try {
await stop();
await _audioPlayer.dispose();
// 重新初始化音频会话
await _initAudioSession();
_audioPlayer = AudioPlayer();
await _audioPlayer.setVolume(_defaultVolume);
await _audioPlayer.setLoopMode(LoopMode.off);
await _audioPlayer.setSpeed(_defaultSpeed);
// 重新配置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,
);
_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;
print('播放事件流错误: $error');
_reinitializePlayer();
},
);
_playerStateSubscription = _audioPlayer.playerStateStream.listen(
(state) {
if (_isDisposed) return;
// 通知播放状态变化
final isPlaying = state.playing && _playlist.length > 0;
_notifyPlayingStateChanged(isPlaying);
if (state.processingState == ProcessingState.completed) {
_tryPlayNext();
}
},
onError: (error) {
if (_isDisposed) return;
print('播放状态流错误: $error');
_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: 20));
if (!_isDisposed && !_audioPlayer.playing) {
await _audioPlayer.seek(Duration.zero, index: 0);
await _audioPlayer.play();
}
}
} catch (e) {
if (e is! StateError) {
print('播放错误: $e');
await _reinitializePlayer();
}
}
}
/// 切换TTS启用状态
void toggleEnabled() {
isEnabled.toggle();
if (!isEnabled.value) {
stop();
}
}
/// 设置播放速度
Future<void> setSpeed(double speed) async {
if (speed < 0.5 || speed > 2.0) {
throw VolcanoTtsException('播放速度必须在0.5到2.0之间');
}
try {
await _audioPlayer.setSpeed(speed);
} catch (e) {
print('设置播放速度失败: $e');
}
}
/// 设置音量
Future<void> setVolume(double volume) async {
if (volume < 0.0 || volume > 1.0) {
throw VolcanoTtsException('音量必须在0.0到1.0之间');
}
try {
await _audioPlayer.setVolume(volume);
} catch (e) {
print('设置音量失败: $e');
}
}
/// 清理资源
Future<void> _cleanupResources() async {
_isDisposed = true;
await _playbackEventSubscription?.cancel();
await _playerStateSubscription?.cancel();
await _audioSession?.setActive(false);
await stop();
await _audioPlayer.dispose();
await _playingStateController.close();
// 释放原生资源
try {
await _channel.invokeMethod('dispose');
} catch (e) {
print('释放原生TTS资源失败: $e');
}
}
@override
void onClose() async {
print('关闭TTS服务');
await _cleanupResources();
super.onClose();
}
/// 获取当前语音类型
String getCurrentVoiceType() {
return _voiceType;
}
}