Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

weicu
fdp 1 year ago
parent
commit
3c22577e64
  1. 257
      lib/core/utils/permission_util.dart
  2. 19
      lib/data/services/version_update_service.dart
  3. 246
      lib/modules/meeting/controllers/meeting_record_controller.dart
  4. 271
      lib/modules/meeting/views/meeting_record_view.dart
  5. 31
      local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift

257
lib/core/utils/permission_util.dart

@ -3,6 +3,7 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'logger.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
/// 权限请求工具类
///
@ -11,6 +12,7 @@ import 'package:get/get.dart';
/// 2. 权限解释横幅显示(悬浮在UI顶部的解释性UI)
/// 3. 权限请求执行
/// 4. 永久拒绝后的设置引导
/// 5. 智能拒绝次数统计和熔断机制
///
/// 使用单例模式:`PermissionUtil.instance`
@ -21,8 +23,29 @@ class PermissionUtil {
/// 私有构造函数
PermissionUtil._();
/// 权限解释横幅的叠加层入口
//OverlayEntry? _explanationOverlayEntry;
/// 引入 get_storage 用于持久化权限拒绝次数
final _storage = GetStorage();
/// 权限拒绝次数阈值(达到此次数后视为永久拒绝)
static const int _denialThreshold = 2;
/// 获取权限拒绝次数的存储键
String _getDenialCountKey(Permission permission) {
return 'permission_denial_count_${permission.toString()}';
}
/// 重置指定权限的拒绝次数
///
/// 在某些场景下可能需要手动重置拒绝次数,比如用户主动要求重新请求权限
Future<void> resetDenialCount(Permission permissionType) async {
await _storage.remove(_getDenialCountKey(permissionType));
Logger.d('PermissionUtil', '已重置 ${permissionType.toString()} 的拒绝次数');
}
/// 获取指定权限的拒绝次数
int getDenialCount(Permission permissionType) {
return _storage.read<int>(_getDenialCountKey(permissionType)) ?? 0;
}
/// 请求指定权限
///
@ -37,7 +60,6 @@ class PermissionUtil {
///
/// 返回值:`Future<bool>` 权限是否最终授予
Future<bool> requestPermission({
// required BuildContext context,
required Permission permissionType,
required String permissionName,
required String explanationText,
@ -49,103 +71,82 @@ class PermissionUtil {
final String finalSettingsButtonText =
settingsButtonText ?? 'goToSettings'.tr;
final String finalCancelButtonText = cancelButtonText ?? 'cancel'.tr;
bool isOverlayShown = false;
try {
// 检查初始权限状态
final PermissionStatus status = await permissionType.status;
PermissionStatus status = await permissionType.status;
Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status');
// 处理已授权状态(包括授予状态和临时通知权限)
if (status == PermissionStatus.granted ||
status == PermissionStatus.provisional) {
Logger.i('PermissionUtil', '$permissionName 权限已授予或临时授权');
// 1. <--- 已授权处理 --->(包括授予状态和临时通知权限)
if (status.isGranted) {
Logger.i('PermissionUtil', '$permissionName 权限已授予或临时授予');
// 如果已经授权,清零之前的拒绝记录,以防用户在设置中开启后又关闭
await resetDenialCount(permissionType); // 清零拒绝记录
return true;
}
// 显示权限解释横幅(覆盖所有状态的处理)
// 2. <--- 熔断机制:检查我们自己的拒绝次数 --->
final denialCount = getDenialCount(permissionType);
Logger.d('PermissionUtil', '$permissionName 当前拒绝次数: $denialCount');
// 如果拒绝次数达到阈值或系统返回了 permanentlyDenied,直接视为永久拒绝
if (status.isPermanentlyDenied || denialCount >= _denialThreshold) {
Logger.w('PermissionUtil', '$permissionName 已被系统永久拒绝或自定义计数达到阈值($denialCount),触发设置引导');
await _showPermanentDenialDialog(
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
// 引导用户后,返回false,因为权限当前未被授予
return false;
}
// 3. <--- 发起正常的权限请求 (此时 status 必然是 denied 或受限) --->
_showExplanationOverlay(
//context: context,
permissionName: permissionName,
explanationText: explanationText,
);
isOverlayShown = true;
final PermissionStatus newStatus = await permissionType.request();
Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus');
_hideExplanationOverlay();
isOverlayShown = false;
// 处理永久拒绝状态(先显示说明横幅,再引导用户去设置)
if (status == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框');
// 4. <--- 处理请求后的结果 --->
if (newStatus.isGranted) {
Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予');
await resetDenialCount(permissionType); // 授权成功,清零
return true;
}
// 显示永久拒绝对话框(在解释横幅下方显示)
final result = await _showPermanentDenialDialog(
// context: context,
if (newStatus.isPermanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限在请求后被系统永久拒绝');
await _showPermanentDenialDialog(
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
// 移除解释横幅
_hideExplanationOverlay();
return result;
}
// 处理初始拒绝/未决定状态
if (status == PermissionStatus.denied) {
Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求');
// _showExplanationOverlay(
// context: context,
// permissionName: permissionName,
// explanationText: explanationText,
// );
//请求权限
final PermissionStatus newStatus = await permissionType.request();
Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus');
//_hideExplanationOverlay();
// 处理请求后的状态
if (newStatus == PermissionStatus.granted ||
newStatus == PermissionStatus.provisional) {
Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予');
_hideExplanationOverlay();
return true;
}
// 处理请求后变为永久拒绝
else if (newStatus == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝');
final result = await _showPermanentDenialDialog(
// context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
_hideExplanationOverlay();
return result;
}
// 其他拒绝状态
_hideExplanationOverlay();
return false;
}
// 处理受限状态
if (status == PermissionStatus.restricted ||
status == PermissionStatus.limited) {
Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status');
await _showRestrictedPermissionDialog(permissionName);
_hideExplanationOverlay();
return false;
if (newStatus.isDenied) {
Logger.w('PermissionUtil', '$permissionName 权限被拒绝,增加拒绝计数');
await _storage.write(_getDenialCountKey(permissionType), denialCount + 1);
}
// 其他未知状态处理
Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status');
_hideExplanationOverlay();
// 所有未授权的情况最终都返回 false
return false;
} catch (e, stackTrace) {
Logger.e(
'PermissionUtil',
'请求 $permissionName 权限失败: $e',
e,
stackTrace,
);
_hideExplanationOverlay();
Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e', e, stackTrace);
if (isOverlayShown) {
_hideExplanationOverlay();
}
return false;
}
}
@ -168,7 +169,7 @@ class PermissionUtil {
borderRadius: BorderRadius.circular(12),
elevation: 8,
color: Colors.grey[300],
shadowColor: Colors.black.withOpacity(0.3),
shadowColor: Colors.black.withValues(alpha: 0.3),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@ -196,64 +197,23 @@ class PermissionUtil {
),
],
),
// 防止用户通过点击背景关闭解释横幅
barrierDismissible: false,
);
// _hideExplanationOverlay(); // 确保移除旧叠加层
// _explanationOverlayEntry = OverlayEntry(
// builder: (context) => Positioned(
// top: MediaQuery.of(context).padding.top + 10,
// left: 16,
// right: 16,
// child: Material(
// borderRadius: BorderRadius.circular(12),
// elevation: 8,
// color: Theme.of(context).colorScheme.background,
// shadowColor: Colors.black.withOpacity(0.3),
// child: Padding(
// padding: const EdgeInsets.all(16),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Text(
// '需要$permissionName权限',
// style: const TextStyle(
// fontWeight: FontWeight.bold,
// fontSize: 16,
// ),
// ),
// const SizedBox(height: 8),
// Text(
// explanationText,
// style: const TextStyle(fontSize: 14),
// ),
// ],
// ),
// ),
// ),
// ),
// );
// Future.delayed(Duration.zero, () {
// if (_explanationOverlayEntry != null) {
// Overlay.of(context).insert(_explanationOverlayEntry!);
// Logger.i('PermissionUtil', '权限说明叠加层已显示 ($permissionName)');
// }
// });
}
/// 隐藏权限解释横幅
void _hideExplanationOverlay() {
Get.back();
//_explanationOverlayEntry!.remove();
Logger.d('PermissionUtil', '权限说明叠加层已移除');
if (Get.isDialogOpen ?? false) {
Get.back();
Logger.d('PermissionUtil', '权限说明叠加层已移除');
}
}
/// 显示永久拒绝后的设置引导对话框
///
/// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限
Future<bool> _showPermanentDenialDialog({
//required BuildContext context,
Future<void> _showPermanentDenialDialog({
required String permissionName,
required String permanentDenialText,
required String settingsButtonText,
@ -287,59 +247,10 @@ class PermissionUtil {
barrierDismissible: false,
);
// bool? userChoice = await showDialog<bool>(
// context: context,
// barrierDismissible: false,
// builder: (context) =>
// );
// 用户选择前往设置
if (userChoice == true) {
AppSettings.openAppSettings();
return false; // 不能立即获取权限,需要用户手动设置
}
// 用户取消
return false;
}
/// 显示受限权限对话框
///
/// 在权限受限状态(如iOS上的受限照片访问)时调用
Future<void> _showRestrictedPermissionDialog(String permissionName) async {
Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)');
Get.dialog(
AlertDialog(
title: Text(permissionName + 'permissionUtilRestricted'.tr),
content: Text('permissionUtilRestrictedMessage'.tr),
actions: [
TextButton(
onPressed: () {
Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)');
// Navigator.pop(context);
Get.back();
},
child: Text('permissionUtilConfirm'.tr),
),
],
),
);
// await showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: Text('$permissionName权限受限'),
// content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'),
// actions: [
// TextButton(
// onPressed: () {
// Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)');
// Navigator.pop(context);
// },
// child: const Text('确定'),
// ),
// ],
// ),
// );
}
// 移除了多余的_showRestrictedPermissionDialog,因为永久拒绝流程已能覆盖
}

19
lib/data/services/version_update_service.dart

@ -24,6 +24,7 @@ class VersionUpdateService extends GetxService {
static bool? _hasUpdate = false; //是否有更新
static bool? _isForceUpdate = false; //是否强制更新
static String? _updateUrl = ""; //更新链接
static AppInfo? _appInfo; //应用信息
static bool? _isShowTourists = false; //是否显示游客登录
static DateTime? _lastCheckTime; //上次检查时间
static const int _cacheIntervalMinutes = 10; // 可配置化
@ -62,6 +63,9 @@ class VersionUpdateService extends GetxService {
/// 获取缓存的更新地址
String get cachedUpdateUrl => _updateUrl ?? '';
/// 获取缓存的应用信息
AppInfo? get cachedAppInfo => _appInfo;
/// 获取缓存的游客登录状态
bool get cachedShowTourists => _isShowTourists ?? false;
@ -183,10 +187,13 @@ class VersionUpdateService extends GetxService {
if (_lastCheckTime != null &&
DateTime.now().difference(_lastCheckTime!).inMinutes <
_cacheIntervalMinutes) {
// [1]没有超过10分钟,返回缓存数据
_logger.d('没有超过10分钟,返回缓存数据');
return VersionUpdateInfo(
hasUpdate: _hasUpdate ?? false,
currentVersion: _appVersion ?? '1.0.0',
latestVersion: (_latestVersion ?? '1.0.0').split('+')[0],
appInfo: _appInfo,
updateType: _hasUpdate!
? (_isForceUpdate! ? UpdateType.force : UpdateType.optional)
: UpdateType.none,
@ -208,6 +215,8 @@ class VersionUpdateService extends GetxService {
_logger.d('获取渠道包信息:$response');
if (response == null || !response.containsKey('app')) {
//[2]获取渠道包信息失败: 响应为空或不包含app字段
_logger.w('获取渠道包信息失败: 响应为空或不包含app字段');
return VersionUpdateInfo(
hasUpdate: false,
currentVersion: currentVersion,
@ -219,6 +228,7 @@ class VersionUpdateService extends GetxService {
//final channelAppResponse = ChannelAppResponse.fromJson(response);
// 直接从response中获取app信息
final appInfo = AppInfo.fromJson(response['app']);
_appInfo = appInfo;
_logger.d('获取到的最新版本信息: ${appInfo.toJson()}');
_latestVersion = appInfo.version;
_updateUrl = appInfo.address;
@ -246,7 +256,7 @@ class VersionUpdateService extends GetxService {
_isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version);
updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional;
}
//[3]返回版本更新信息
return VersionUpdateInfo(
hasUpdate: _hasUpdate!,
currentVersion: currentVersion.split('+')[0],
@ -343,11 +353,18 @@ class VersionUpdateService extends GetxService {
/// 执行更新操作
Future<bool> performUpdate(VersionUpdateInfo updateInfo) async {
print('lxm---updateInfo详情:');
print('lxm---hasUpdate: ${updateInfo.hasUpdate}');
print('lxm---currentVersion: ${updateInfo.currentVersion}');
print('lxm---latestVersion: ${updateInfo.latestVersion}');
print('lxm---updateType: ${updateInfo.updateType}');
print('lxm---appInfo: ${updateInfo.appInfo?.toJson()}');
if (!updateInfo.hasUpdate || updateInfo.appInfo == null) {
return false;
}
try {
_logger.d('lxm---执行更新操作,下载地址: ${updateInfo.appInfo!.address}');
return await _openDownloadUrl(updateInfo.appInfo!.address!);
} catch (e) {
_logger.e('执行更新失败: $e');

246
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -13,27 +13,28 @@ import 'package:permission_handler/permission_handler.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart';
/// Enumerates the possible states of the recording timer
/// 会议记录状态枚举
enum TimerStatus {
stopped, // Timer is inactive
running, // Timer is actively counting
paused, // Timer is paused
stopped, // 停止
running, // 运行中
paused, // 暂停
}
/// Controller for meeting recording screen
/// Handles recording state, audio processing, transcription, and UI interactions
/// 会议记录控制器
/// 负责会议记录的状态管理、音频处理、语音识别和UI交互
class MeetingRecordController extends GetxController
with GetSingleTickerProviderStateMixin {
with GetTickerProviderStateMixin {
// Services
final AsrService _asrService = Get.find<AsrService>();
final BleManager _bleManager = Get.find<BleManager>();
// Stream subscriptions
// 流订阅
StreamSubscription? _recognitionSubscription;
// Recording state
// 记录状态
final Rx<TimerStatus> timerStatus = TimerStatus.stopped.obs;
final RxInt seconds = 0.obs;
final RxInt milliseconds = 0.obs;
final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call
final RxBool isMarked = false.obs;
final RxBool showTextContent = false.obs;
@ -60,19 +61,29 @@ class MeetingRecordController extends GetxController
// 移除了未使用的变量和TabController
StreamSubscription<Uint8List>? _audioDataSubscription;
// 添加波形放大倍数变量
final double waveAmplifyFactor = 250.0; // 增加波动幅度
final RxDouble ursorPosition = 0.0.obs; // 当前播放位置
final RxInt _currentPosition = 0.obs; // 原始位置索引
int maxPoints = 300; // 最大显示点数(根据屏幕宽度优化)
int sampleRate = 16000; // 假设音频采样率(根据实际情况调整)
// 简化的音波数据管理
final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率
int maxVisibleBars = 60; // 最大可见波形条数量
Timer? _audioFrameTimer; // 音频帧定时器
double _currentFrameAmplitude = 0.0; // 当前帧累积振幅
int _currentFrameSamples = 0; // 当前帧样本数量
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
static const int _audioProcessInterval = 3; // 每3个音频包处理1次
// 波形流动动画控制器
late AnimationController waveformAnimationController;
late Animation<double> waveformAnimation;
// 设置最大点数(根据屏幕宽度)
void setMaxPoints(double screenWidth) {
maxPoints = screenWidth ~/ 3; // 增加数据点密度以填满整个区域
// 数据更新控制器 - 统一管理数据更新和动画
late AnimationController _dataUpdateController;
late Animation<double> _dataUpdateAnimation;
// 设置最大可见波形条数量(根据屏幕宽度)
void setMaxVisibleBars(double screenWidth) {
const barWidth = 3.0; // 波形条宽度
const barSpacing = 1.0; // 波形条间距
maxVisibleBars = (screenWidth / (barWidth + barSpacing)).floor();
}
@override
@ -87,7 +98,9 @@ class MeetingRecordController extends GetxController
@override
void onClose() {
_cleanupResources();
// 清理两个AnimationController
waveformAnimationController.dispose();
_dataUpdateController.dispose();
super.onClose();
}
@ -121,11 +134,36 @@ class MeetingRecordController extends GetxController
titleFocusNode.addListener(_handleTitleFocusChange);
}
/// 初始化波形流动动画
/// 初始化波形流动动画和数据更新控制器
void _initializeWaveformAnimation() {
// 设置动画持续时间,控制流动速度
// 1. 数据更新控制器 - 控制数据提交频率
_dataUpdateController = AnimationController(
duration: Duration(milliseconds: frameIntervalMs), // 250ms 一次数据更新
vsync: this,
);
_dataUpdateAnimation =
Tween<double>(begin: 0.0, end: 1.0).animate(_dataUpdateController);
// 监听数据更新动画,每个周期结束时提交数据
_dataUpdateAnimation.addListener(() {
if (!isRecording.value) return;
// 每个动画周期结束时,才提交一次数据
if (_dataUpdateController.value >= 1.0) {
_commitAudioFrame();
// 重置动画,继续循环
_dataUpdateController.reset();
if (isRecording.value) {
_dataUpdateController.forward();
}
}
});
// 2. 波形流动动画控制器 - 控制视觉流动效果
waveformAnimationController = AnimationController(
duration: const Duration(milliseconds: 100), // 更快的流动速度
duration: Duration(milliseconds: frameIntervalMs), // 与数据更新同步
vsync: this,
);
@ -169,6 +207,8 @@ class MeetingRecordController extends GetxController
/// Cleans up resources when controller is disposed
void _cleanupResources() {
_timer?.cancel();
_audioFrameTimer?.cancel(); // 清理音频帧定时器
// AnimationController在onClose中统一清理
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
_audioDataSubscription?.cancel();
@ -217,10 +257,17 @@ class MeetingRecordController extends GetxController
/// Toggles important mark status
void toggleMarked() => isMarked.toggle();
/// Formats seconds to MM:SS display format
String formatTime(int seconds) {
return '${(seconds ~/ 60).toString().padLeft(2, '0')}'
':${(seconds % 60).toString().padLeft(2, '0')}';
/// 格式化时间为 MM:SS.S 显示格式
String formatTime(int milliseconds) {
// 使用毫秒级精度计算
final totalMs = milliseconds;
final minutes = (totalMs ~/ 60000);
final secs = ((totalMs % 60000) ~/ 1000);
final centiseconds = ((totalMs % 1000) ~/ 100); // 取十分之一秒(一位小数)
return '${minutes.toString().padLeft(2, '0')}'
':${secs.toString().padLeft(2, '0')}'
'.${centiseconds.toString()}';
}
/// Starts/resumes recording based on current state
@ -263,7 +310,7 @@ class MeetingRecordController extends GetxController
await _asrService.enableRecord("${dir.path}$fullFileName.wav",
acceptAudioData: true);
// 监听音频数据流
// 通过 asrService 获取音频数据流
_audioDataSubscription =
_asrService.getAudioDataStream()?.listen((audioData) {
// 处理音频数据,例如:
@ -282,8 +329,9 @@ class MeetingRecordController extends GetxController
newName.value = fullFileName;
titleEditingController.text = fullFileName;
// 启动波形动画
// 启动波形动画和音频帧定时器
_startWaveformAnimation();
_startAudioFrameTimer();
// Start hardware components
_startHardwareServices();
@ -311,6 +359,7 @@ class MeetingRecordController extends GetxController
Future<void> _pauseRecording() async {
await _asrService.pauseRecord();
_stopWaveformAnimation(); // 暂停时停止动画
_stopAudioFrameTimer(); // 停止音频帧定时器
pauseTimer();
}
@ -318,88 +367,93 @@ class MeetingRecordController extends GetxController
Future<void> _resumeRecording() async {
await _asrService.resumeRecord();
_startWaveformAnimation(); // 恢复时启动动画
_startAudioFrameTimer(); // 重启音频帧定时器
startTimer();
}
/// 处理原始Uint8List音频数据
/// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化
void _processAudioData(Uint8List audioData) {
// 将Uint8List转换为Float32List(PCM16 -> 浮点)
final pcmData = _convertToPCM(audioData);
if (!isRecording.value) return;
// 降采样和添加数据
_addAudioData(pcmData);
// 性能优化:不是每个音频包都处理,减少CPU占用
_audioProcessCounter++;
if (_audioProcessCounter % _audioProcessInterval != 0) {
return; // 跳过这次处理
}
// 将音频数据转换为振幅值并累积到当前帧
final amplitude = _calculateAmplitude(audioData);
_currentFrameAmplitude += amplitude;
_currentFrameSamples++;
}
/// 转换Uint8List为PCM浮点数组
Float32List _convertToPCM(Uint8List uint8List) {
// 假设音频为16位PCM小端格式
final byteData = ByteData.sublistView(uint8List);
final length = uint8List.length ~/ 2;
final pcmData = Float32List(length);
for (int i = 0; i < length; i++) {
// 读取16位有符号整数 (-32768 到 32767)
final int intValue = byteData.getInt16(i * 2, Endian.little);
// 转换为浮点数 (-1.0 到 1.0)
pcmData[i] = intValue / 32768.0;
/// 提交音频帧数据到波形显示
void _commitAudioFrame() {
// 计算当前帧的平均振幅
double frameAmplitude = 0.0;
if (_currentFrameSamples > 0) {
frameAmplitude = _currentFrameAmplitude / _currentFrameSamples;
}
return pcmData;
}
// 转换为显示数据
final newAmplitude = frameAmplitude * 800.0;
// 更新波形数据
if (waveData.length >= maxVisibleBars) {
waveData.removeAt(0);
}
waveData.add(newAmplitude);
/// 降采样音频数据
List<double> _downsampleAudio(Float32List audio) {
if (audio.isEmpty) return [];
// 重置当前帧数据
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
}
final result = <double>[];
final step = (audio.length / maxPoints).ceil();
/// 计算音频数据的振幅
double _calculateAmplitude(Uint8List audioData) {
if (audioData.isEmpty) return 0.0;
// 计算每个区间的峰值(保留更多细节)
for (int i = 0; i < audio.length; i += step) {
double maxVal = 0.0;
final end = min(i + step, audio.length);
// 使用RMS (Root Mean Square) 算法,更准确反映音频能量
double sumSquares = 0.0;
int sampleCount = 0;
for (int j = i; j < end; j++) {
final absVal = audio[j].abs();
if (absVal > maxVal) maxVal = absVal;
for (int i = 0; i < audioData.length; i += 4) {
// 跳跃采样,减少计算量
if (i + 1 < audioData.length) {
final int sample = (audioData[i + 1] << 8) | audioData[i];
final int signedSample = sample > 32767 ? sample - 65536 : sample;
sumSquares += (signedSample * signedSample);
sampleCount++;
}
// 应用放大倍数增强波动效果
result.add(maxVal * waveAmplifyFactor);
}
return result;
if (sampleCount == 0) return 0.0;
final rms = sqrt(sumSquares / sampleCount);
return (rms / 32768.0).clamp(0.0, 1.0);
}
// 获取当前光标位置(0.0 - 1.0)
double get cursorPositionValue {
if (waveData.isEmpty) return 0.0;
return _currentPosition.value / waveData.length;
}
/// 启动数据更新控制器 - 替代Timer
void _startAudioFrameTimer() {
_audioFrameTimer?.cancel(); // 清理旧的Timer(如果有)
_audioFrameTimer = null;
/// 添加处理后的音频数据 - 优化全屏流动效果
void _addAudioData(Float32List newAudio) {
if (!isRecording.value) return;
// 1. 降采样处理
final downsampled = _downsampleAudio(newAudio);
if (downsampled.isEmpty) return;
// 启动数据更新控制器
_dataUpdateController.forward();
}
// 2. 逐个添加数据点,确保平滑流动
for (final sample in downsampled) {
waveData.add(sample);
/// 停止数据更新控制器
void _stopAudioFrameTimer() {
_audioFrameTimer?.cancel();
_audioFrameTimer = null;
// 维护合理的数据长度 - 保持足够的数据以填满整个屏幕
if (waveData.length > maxPoints * 2) {
waveData.removeAt(0); // 移除最旧的数据
}
}
// 停止数据更新控制器
_dataUpdateController.stop();
_dataUpdateController.reset();
// 3. 确保动画持续运行
_ensureAnimationRunning();
// 4. 更新位置信息
_currentPosition.value = waveData.length;
// 重置当前帧数据和计数器
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
_audioProcessCounter = 0; // 重置音频处理计数器
}
/// 启动波形流动动画
@ -415,17 +469,11 @@ class MeetingRecordController extends GetxController
waveformAnimationController.reset();
}
/// 确保动画持续运行
void _ensureAnimationRunning() {
if (isRecording.value && !waveformAnimationController.isAnimating) {
waveformAnimationController.forward();
}
}
/// Saves recording and cleans up resources
Future<void> saveRecording() async {
resetTimer();
_stopWaveformAnimation(); // 停止波形动画
_stopAudioFrameTimer(); // 停止音频帧定时器
if (audioType.value != 0) {
_bleManager.closeCodec();
}
@ -477,19 +525,22 @@ class MeetingRecordController extends GetxController
/// TIMER CONTROL SECTION ///
/// Starts or resumes timer
/// 启动定时器
void startTimer() {
if (timerStatus.value != TimerStatus.running) {
timerStatus.value = TimerStatus.running;
_lastStartTime = DateTime.now().millisecondsSinceEpoch;
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
_timer = Timer.periodic(const Duration(milliseconds: 100), (_) {
// 改为100ms更新频率
if (timerStatus.value != TimerStatus.running) return;
final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = currentTime;
seconds.value = (_elapsedMilliseconds / 1000).round();
milliseconds.value = _elapsedMilliseconds; // 更新毫秒值
});
}
}
@ -527,7 +578,4 @@ class MeetingRecordController extends GetxController
return baseHeight + rand * 10;
});
}
/// Current playback position in waveform (0-1)
double get cursorPosition => _currentPosition / waveData.length;
}

271
lib/modules/meeting/views/meeting_record_view.dart

@ -1,5 +1,5 @@
import 'dart:math';
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
@ -17,7 +17,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
// 在返回时执行保存操作
controller.saveTitleChange();
//controller.saveTitleChange();
//await controller.saveRecording();
}
},
@ -108,17 +108,15 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
// 使用LayoutBuilder获取准确的屏幕宽度
final screenWidth = constraints.maxWidth;
// 初始化最大点数
if (controller.maxPoints == 0) {
controller.setMaxPoints(screenWidth);
}
// 初始化最大可见波形条数量
controller.setMaxVisibleBars(screenWidth);
return Column(
children: [
SizedBox(height: 24.h),
// 专业级实时流动波形显示区域
// 实时流动波形显示区域
Container(
height: 120.h,
height: 200.h,
margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration(
gradient: LinearGradient(
@ -136,7 +134,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
const Color(0xFFF8F9FA),
],
),
borderRadius: BorderRadius.circular(12.r),
//borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: isDarkMode
? Colors.white.withValues(alpha: 0.1)
@ -161,7 +159,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
_WaveformWidget(
controller: controller,
isDarkMode: isDarkMode,
size: Size(screenWidth - 32.w, 120.h),
size: Size(screenWidth - 32.w, 200.h),
),
// 录音状态指示器
@ -213,11 +211,14 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
SizedBox(height: 24.h),
// 时间显示 - 只包装需要响应式更新的Text widget
Obx(() => Text(
controller.formatTime(controller.seconds.value),
controller.formatTime(controller.milliseconds.value),
style: TextStyle(
fontSize: 48.sp,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.normal,
color: isDarkMode ? Colors.white : Colors.black87,
fontFeatures: const [
FontFeature.tabularFigures()
], // 使用等宽数字,避免跳动
),
)),
const Spacer(),
@ -449,7 +450,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
}
}
/// 实时流动波形绘制器
/// 简化的实时流动波形绘制器
class RealTimeWaveformPainter extends CustomPainter {
final List<double> waveData;
final bool isDarkMode;
@ -466,22 +467,19 @@ class RealTimeWaveformPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final centerY = size.height / 2;
final centerX = size.width / 2;
// 绘制中央基准线
_drawCenterLine(canvas, size, centerX, centerY);
_drawCenterLine(canvas, size, centerY);
// 只在录音状态下且有波形数据时才绘制波形
if (isRecording && waveData.isNotEmpty) {
_drawWaveform(canvas, size, centerX, centerY);
_drawWaveform(canvas, size, centerY);
}
}
/// 绘制中央时间参考线(只保留水平线)
void _drawCenterLine(
Canvas canvas, Size size, double centerX, double centerY) {
// 只绘制水平中线
final horizontalLinePaint = Paint()
/// 绘制中央基准线
void _drawCenterLine(Canvas canvas, Size size, double centerY) {
final linePaint = Paint()
..color =
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15)
..strokeWidth = 1.0;
@ -489,134 +487,109 @@ class RealTimeWaveformPainter extends CustomPainter {
canvas.drawLine(
Offset(0, centerY),
Offset(size.width, centerY),
horizontalLinePaint,
linePaint,
);
// 移除垂直时间参考线和三角形指示器
}
/// 绘制实时流动波形(扩散到整个区域)
void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) {
final barWidth = 2.5; // 波形条宽度
final barSpacing = 0.5; // 波形条间距
final totalBarWidth = barWidth + barSpacing;
/// 绘制简化的流动波形
void _drawWaveform(Canvas canvas, Size size, double centerY) {
const double barWidth = 3.0; // 波形条宽度
const double barSpacing = 1.0; // 波形条间距
const double totalBarWidth = barWidth + barSpacing;
// 计算整个宽度可以显示多少个波形条
// 计算可显示的波形条数量
final maxBars = (size.width / totalBarWidth).floor();
final visibleBars = waveData.length > maxBars ? maxBars : waveData.length;
// 绘制扩散到整个区域的波形
_drawFullWaveform(
canvas, size, centerY, barWidth, totalBarWidth, maxBars);
}
/// 绘制扩散到整个区域的波形
void _drawFullWaveform(Canvas canvas, Size size, double centerY,
double barWidth, double totalBarWidth, int maxBars) {
final dataLength = waveData.length;
if (dataLength == 0) return;
// 计算流动偏移量
// 计算流动偏移量(从右向左移动)
final flowOffset = animationValue * totalBarWidth;
// 绘制波形条,从左到右填满整个区域
final displayCount = min(dataLength, maxBars);
for (int i = 0; i < displayCount; i++) {
// 计算波形条的x位置:从左边开始排列,加上流动偏移
final x = i * totalBarWidth + flowOffset;
// 如果波形条完全移出屏幕右侧,跳过绘制
if (x > size.width) continue;
// 如果波形条完全移出屏幕左侧,跳过绘制
if (x + barWidth < 0) continue;
// 获取对应的音频数据(循环使用数据以填满整个区域)
final dataIndex = (dataLength - 1 - (i % dataLength)) % dataLength;
// 从右到左绘制波形条
for (int i = 0; i < visibleBars; i++) {
final dataIndex = waveData.length - 1 - i;
final amplitude = waveData[dataIndex];
// 计算透明度:中央区域更亮,边缘渐暗
final distanceFromCenter = (x - size.width / 2).abs() / (size.width / 2);
final opacity = (1.0 - distanceFromCenter * 0.4).clamp(0.3, 1.0);
// 计算波形条的x位置(从右侧开始,向左排列)
final x = size.width - (i + 1) * totalBarWidth + flowOffset;
// 跳过屏幕外的波形条
if (x < -barWidth || x > size.width) continue;
_drawWaveBar(canvas, x, centerY, barWidth, amplitude, opacity);
_drawWaveBar(
canvas, x, centerY, amplitude, i.toDouble(), visibleBars.toDouble());
}
}
/// 绘制单个上下对称的波形条
void _drawWaveBar(Canvas canvas, double x, double centerY, double barWidth,
double amplitude, double opacity) {
// 将振幅转换为显示高度(上下对称)
final maxHeight = 50.0; // 最大高度
final barHeight = (amplitude / 100.0) * maxHeight;
/// 绘制单个波形条
void _drawWaveBar(Canvas canvas, double x, double centerY, double amplitude,
double index, double totalBars) {
const double barWidth = 2.0;
const double maxHeight = 200.0;
const double minHeight = 4.0; // 最小基础高度
const double containerPadding = 10.0; // 容器内边距,防止超出边界
// 创建上下对称的圆角矩形
// 计算可用的最大高度(考虑容器边界)
final availableHeight = (centerY - containerPadding) * 2; // 上下对称
final effectiveMaxHeight = math.min(maxHeight, availableHeight);
// 优化波形条高度计算:确保有基础高度 + 振幅变化
final normalizedAmplitude = (amplitude / 100.0).clamp(0.0, 1.0);
final dynamicHeight =
normalizedAmplitude * (effectiveMaxHeight - minHeight);
final barHeight = minHeight + dynamicHeight;
// 计算透明度(越新的波形条越不透明)
//final opacity = (1.0 - (index / totalBars) * 0.6).clamp(0.3, 1.0);
// 创建波形条矩形
final rect = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(x + barWidth / 2, centerY),
width: barWidth,
height: barHeight * 2, // 上下对称,总高度是barHeight的2倍
height: barHeight, // 使用优化后的高度
),
Radius.circular(barWidth / 2),
const Radius.circular(1.5),
);
// 创建渐变效果
final gradient = LinearGradient(
colors: _getGradientColors(opacity),
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
);
// 创建渐变颜色
final Color baseColor = isDarkMode
? Colors.grey[500]! // 深灰色
: Colors.grey[600]!; // 更深灰色
final paint = Paint()
..shader = gradient.createShader(rect.outerRect)
..color = baseColor
..style = PaintingStyle.fill;
canvas.drawRRect(rect, paint);
}
// 可选:添加高光效果(仅对较新的波形条)
if (opacity > 0.7) {
final highlightRect = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(x + barWidth / 2, centerY),
width: barWidth * 0.4,
height: barHeight * 2 * 0.8,
),
Radius.circular(barWidth / 4),
);
final highlightPaint = Paint()
..color = Colors.white.withValues(alpha: 0.2 * opacity)
..style = PaintingStyle.fill;
@override
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) {
// 只有在以下情况才重绘
if (oldDelegate.isDarkMode != isDarkMode ||
oldDelegate.isRecording != isRecording) {
return true;
}
canvas.drawRRect(highlightRect, highlightPaint);
// 数据长度变化才重绘
if (oldDelegate.waveData.length != waveData.length) {
return true;
}
}
/// 获取渐变颜色
List<Color> _getGradientColors(double opacity) {
return isDarkMode
? [
Colors.blue[400]!.withValues(alpha: opacity),
Colors.blue[200]!.withValues(alpha: opacity * 0.7),
Colors.blue[100]!.withValues(alpha: opacity * 0.4),
]
: [
Colors.blue[500]!.withValues(alpha: opacity),
Colors.blue[300]!.withValues(alpha: opacity * 0.7),
Colors.blue[100]!.withValues(alpha: opacity * 0.4),
];
}
// 数据内容变化(比较最后一个值)
if (waveData.isNotEmpty &&
oldDelegate.waveData.isNotEmpty &&
(waveData.last - oldDelegate.waveData.last).abs() > 0.1) {
return true;
}
@override
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) {
return oldDelegate.waveData != waveData ||
oldDelegate.isDarkMode != isDarkMode ||
oldDelegate.isRecording != isRecording ||
oldDelegate.animationValue != animationValue;
// 动画值每 0.1 变化才重绘(避免每帧都重绘)
return (animationValue - oldDelegate.animationValue).abs() > 0.1;
}
}
/// 专门的波形Widget,正确管理GetX响应式状态
class _WaveformWidget extends StatelessWidget {
/// 优化的波形Widget,避免双重响应式更新
class _WaveformWidget extends StatefulWidget {
final MeetingRecordController controller;
final bool isDarkMode;
final Size size;
@ -627,25 +600,67 @@ class _WaveformWidget extends StatelessWidget {
required this.size,
});
@override
State<_WaveformWidget> createState() => _WaveformWidgetState();
}
class _WaveformWidgetState extends State<_WaveformWidget> {
late List<double> _waveData;
late bool _isRecording;
StreamSubscription? _waveDataSubscription;
StreamSubscription? _isRecordingSubscription;
@override
void initState() {
super.initState();
_waveData = List.from(widget.controller.waveData);
_isRecording = widget.controller.isRecording.value;
// 监听 waveData 变化
_waveDataSubscription = widget.controller.waveData.listen((data) {
if (mounted) {
setState(() {
_waveData = List.from(data);
});
}
});
// 监听 isRecording 变化
_isRecordingSubscription = widget.controller.isRecording.listen((value) {
if (mounted) {
setState(() {
_isRecording = value;
});
}
});
}
@override
void dispose() {
_waveDataSubscription?.cancel();
_isRecordingSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 使用AnimatedBuilder监听动画变化
return AnimatedBuilder(
animation: controller.waveformAnimation,
builder: (context, child) {
// 使用Obx只监听需要响应式更新的数据
return Obx(() {
return RepaintBoundary(
child: AnimatedBuilder(
animation: widget.controller.waveformAnimation,
builder: (context, child) {
return CustomPaint(
size: widget.size,
painter: RealTimeWaveformPainter(
waveData: controller.waveData,
isDarkMode: isDarkMode,
isRecording: controller.isRecording.value,
animationValue: controller.waveformAnimation.value,
waveData: _waveData,
isDarkMode: widget.isDarkMode,
isRecording: _isRecording,
animationValue: widget.controller.waveformAnimation.value,
),
size: size,
isComplex: true, // 告诉 Flutter 这个绘制较复杂,可缓存
willChange: false, // 数据不会频繁变化(我们已优化)
);
});
},
},
),
);
}
}

31
local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift

@ -358,7 +358,7 @@ import QPlayAutoSDK
return
}
// if isLoginOK {
if isLoginOK {
// 已登录,检查连接状态
if isConnected {
// 已连接,直接执行操作
@ -380,20 +380,21 @@ import QPlayAutoSDK
}
}
}
// } else {
// // 未登录,先登录
// print("QPlayAutoManager: 未登录,尝试登录...")
// performLogin { [weak self] success in
// guard let self = self else { return }
// if success {
// print("QPlayAutoManager: 登录成功,执行操作")
// operation()
// } else {
// print("QPlayAutoManager: 登录失败,无法执行操作")
// onFailure?(.loginFailed)
// }
// }
// }
} else {
// 未登录,先登录
print("QPlayAutoManager: 未登录,尝试登录...")
performLogin { [weak self] success in
guard let self = self else { return }
if success {
print("QPlayAutoManager: 登录成功,执行操作")
operation()
} else {
print("QPlayAutoManager: 登录失败,无法执行操作")
onFailure?(.loginFailed)
stop()
}
}
}
}
private func performLogin(completion: @escaping (Bool) -> Void) {

Loading…
Cancel
Save