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
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;
|
|
}
|
|
|
|
}
|