Browse Source

refactor(meeting): 优化会议记录控制器的音频处理和波形显示

- 重构音频数据处理逻辑,使用帧间隔和采样控制提升性能
- 改进波形显示算法,增加毫秒级时间精度
- 简化波形绘制逻辑,优化重绘性能
- 添加数据更新控制器统一管理动画和数据处理
weicu
lxm 1 year ago
parent
commit
0748414534
  1. 246
      lib/modules/meeting/controllers/meeting_record_controller.dart
  2. 271
      lib/modules/meeting/views/meeting_record_view.dart

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/asr_service.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
/// Enumerates the possible states of the recording timer /// 会议记录状态枚举
enum TimerStatus { enum TimerStatus {
stopped, // Timer is inactive stopped, // 停止
running, // Timer is actively counting running, // 运行中
paused, // Timer is paused paused, // 暂停
} }
/// Controller for meeting recording screen /// 会议记录控制器
/// Handles recording state, audio processing, transcription, and UI interactions /// 负责会议记录的状态管理、音频处理、语音识别和UI交互
class MeetingRecordController extends GetxController class MeetingRecordController extends GetxController
with GetSingleTickerProviderStateMixin { with GetTickerProviderStateMixin {
// Services // Services
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final BleManager _bleManager = Get.find<BleManager>(); final BleManager _bleManager = Get.find<BleManager>();
// Stream subscriptions // 流订阅
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
// Recording state // 记录状态
final Rx<TimerStatus> timerStatus = TimerStatus.stopped.obs; final Rx<TimerStatus> timerStatus = TimerStatus.stopped.obs;
final RxInt seconds = 0.obs; final RxInt seconds = 0.obs;
final RxInt milliseconds = 0.obs;
final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call final RxInt audioType = 0.obs; // 0=Live, 1=Media, 2=Call
final RxBool isMarked = false.obs; final RxBool isMarked = false.obs;
final RxBool showTextContent = false.obs; final RxBool showTextContent = false.obs;
@ -60,19 +61,29 @@ class MeetingRecordController extends GetxController
// 移除了未使用的变量和TabController // 移除了未使用的变量和TabController
StreamSubscription<Uint8List>? _audioDataSubscription; StreamSubscription<Uint8List>? _audioDataSubscription;
// 添加波形放大倍数变量 // 简化的音波数据管理
final double waveAmplifyFactor = 250.0; // 增加波动幅度 final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率
final RxDouble ursorPosition = 0.0.obs; // 当前播放位置 int maxVisibleBars = 60; // 最大可见波形条数量
final RxInt _currentPosition = 0.obs; // 原始位置索引 Timer? _audioFrameTimer; // 音频帧定时器
int maxPoints = 300; // 最大显示点数(根据屏幕宽度优化) double _currentFrameAmplitude = 0.0; // 当前帧累积振幅
int sampleRate = 16000; // 假设音频采样率(根据实际情况调整) int _currentFrameSamples = 0; // 当前帧样本数量
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
static const int _audioProcessInterval = 3; // 每3个音频包处理1次
// 波形流动动画控制器 // 波形流动动画控制器
late AnimationController waveformAnimationController; late AnimationController waveformAnimationController;
late Animation<double> waveformAnimation; 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 @override
@ -87,7 +98,9 @@ class MeetingRecordController extends GetxController
@override @override
void onClose() { void onClose() {
_cleanupResources(); _cleanupResources();
// 清理两个AnimationController
waveformAnimationController.dispose(); waveformAnimationController.dispose();
_dataUpdateController.dispose();
super.onClose(); super.onClose();
} }
@ -121,11 +134,36 @@ class MeetingRecordController extends GetxController
titleFocusNode.addListener(_handleTitleFocusChange); titleFocusNode.addListener(_handleTitleFocusChange);
} }
/// 初始化波形流动动画 /// 初始化波形流动动画和数据更新控制器
void _initializeWaveformAnimation() { 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( waveformAnimationController = AnimationController(
duration: const Duration(milliseconds: 100), // 更快的流动速度 duration: Duration(milliseconds: frameIntervalMs), // 与数据更新同步
vsync: this, vsync: this,
); );
@ -169,6 +207,8 @@ class MeetingRecordController extends GetxController
/// Cleans up resources when controller is disposed /// Cleans up resources when controller is disposed
void _cleanupResources() { void _cleanupResources() {
_timer?.cancel(); _timer?.cancel();
_audioFrameTimer?.cancel(); // 清理音频帧定时器
// AnimationController在onClose中统一清理
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
_audioDataSubscription?.cancel(); _audioDataSubscription?.cancel();
@ -217,10 +257,17 @@ class MeetingRecordController extends GetxController
/// Toggles important mark status /// Toggles important mark status
void toggleMarked() => isMarked.toggle(); void toggleMarked() => isMarked.toggle();
/// Formats seconds to MM:SS display format /// 格式化时间为 MM:SS.S 显示格式
String formatTime(int seconds) { String formatTime(int milliseconds) {
return '${(seconds ~/ 60).toString().padLeft(2, '0')}' // 使用毫秒级精度计算
':${(seconds % 60).toString().padLeft(2, '0')}'; 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 /// Starts/resumes recording based on current state
@ -263,7 +310,7 @@ class MeetingRecordController extends GetxController
await _asrService.enableRecord("${dir.path}$fullFileName.wav", await _asrService.enableRecord("${dir.path}$fullFileName.wav",
acceptAudioData: true); acceptAudioData: true);
// 监听音频数据流 // 通过 asrService 获取音频数据流
_audioDataSubscription = _audioDataSubscription =
_asrService.getAudioDataStream()?.listen((audioData) { _asrService.getAudioDataStream()?.listen((audioData) {
// 处理音频数据,例如: // 处理音频数据,例如:
@ -282,8 +329,9 @@ class MeetingRecordController extends GetxController
newName.value = fullFileName; newName.value = fullFileName;
titleEditingController.text = fullFileName; titleEditingController.text = fullFileName;
// 启动波形动画 // 启动波形动画和音频帧定时器
_startWaveformAnimation(); _startWaveformAnimation();
_startAudioFrameTimer();
// Start hardware components // Start hardware components
_startHardwareServices(); _startHardwareServices();
@ -311,6 +359,7 @@ class MeetingRecordController extends GetxController
Future<void> _pauseRecording() async { Future<void> _pauseRecording() async {
await _asrService.pauseRecord(); await _asrService.pauseRecord();
_stopWaveformAnimation(); // 暂停时停止动画 _stopWaveformAnimation(); // 暂停时停止动画
_stopAudioFrameTimer(); // 停止音频帧定时器
pauseTimer(); pauseTimer();
} }
@ -318,88 +367,93 @@ class MeetingRecordController extends GetxController
Future<void> _resumeRecording() async { Future<void> _resumeRecording() async {
await _asrService.resumeRecord(); await _asrService.resumeRecord();
_startWaveformAnimation(); // 恢复时启动动画 _startWaveformAnimation(); // 恢复时启动动画
_startAudioFrameTimer(); // 重启音频帧定时器
startTimer(); startTimer();
} }
/// 处理原始Uint8List音频数据 /// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化
void _processAudioData(Uint8List audioData) { void _processAudioData(Uint8List audioData) {
// 将Uint8List转换为Float32List(PCM16 -> 浮点) if (!isRecording.value) return;
final pcmData = _convertToPCM(audioData);
// 降采样和添加数据 // 性能优化:不是每个音频包都处理,减少CPU占用
_addAudioData(pcmData); _audioProcessCounter++;
if (_audioProcessCounter % _audioProcessInterval != 0) {
return; // 跳过这次处理
}
// 将音频数据转换为振幅值并累积到当前帧
final amplitude = _calculateAmplitude(audioData);
_currentFrameAmplitude += amplitude;
_currentFrameSamples++;
} }
/// 转换Uint8List为PCM浮点数组 /// 提交音频帧数据到波形显示
Float32List _convertToPCM(Uint8List uint8List) { void _commitAudioFrame() {
// 假设音频为16位PCM小端格式 // 计算当前帧的平均振幅
final byteData = ByteData.sublistView(uint8List); double frameAmplitude = 0.0;
final length = uint8List.length ~/ 2; if (_currentFrameSamples > 0) {
final pcmData = Float32List(length); frameAmplitude = _currentFrameAmplitude / _currentFrameSamples;
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;
} }
return pcmData; // 转换为显示数据
} final newAmplitude = frameAmplitude * 800.0;
// 更新波形数据
if (waveData.length >= maxVisibleBars) {
waveData.removeAt(0);
}
waveData.add(newAmplitude);
/// 降采样音频数据 // 重置当前帧数据
List<double> _downsampleAudio(Float32List audio) { _currentFrameAmplitude = 0.0;
if (audio.isEmpty) return []; _currentFrameSamples = 0;
}
final result = <double>[]; /// 计算音频数据的振幅
final step = (audio.length / maxPoints).ceil(); double _calculateAmplitude(Uint8List audioData) {
if (audioData.isEmpty) return 0.0;
// 计算每个区间的峰值(保留更多细节) // 使用RMS (Root Mean Square) 算法,更准确反映音频能量
for (int i = 0; i < audio.length; i += step) { double sumSquares = 0.0;
double maxVal = 0.0; int sampleCount = 0;
final end = min(i + step, audio.length);
for (int j = i; j < end; j++) { for (int i = 0; i < audioData.length; i += 4) {
final absVal = audio[j].abs(); // 跳跃采样,减少计算量
if (absVal > maxVal) maxVal = absVal; 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) /// 启动数据更新控制器 - 替代Timer
double get cursorPositionValue { void _startAudioFrameTimer() {
if (waveData.isEmpty) return 0.0; _audioFrameTimer?.cancel(); // 清理旧的Timer(如果有)
return _currentPosition.value / waveData.length; _audioFrameTimer = null;
}
/// 添加处理后的音频数据 - 优化全屏流动效果 // 启动数据更新控制器
void _addAudioData(Float32List newAudio) { _dataUpdateController.forward();
if (!isRecording.value) return; }
// 1. 降采样处理
final downsampled = _downsampleAudio(newAudio);
if (downsampled.isEmpty) return;
// 2. 逐个添加数据点,确保平滑流动 /// 停止数据更新控制器
for (final sample in downsampled) { void _stopAudioFrameTimer() {
waveData.add(sample); _audioFrameTimer?.cancel();
_audioFrameTimer = null;
// 维护合理的数据长度 - 保持足够的数据以填满整个屏幕 // 停止数据更新控制器
if (waveData.length > maxPoints * 2) { _dataUpdateController.stop();
waveData.removeAt(0); // 移除最旧的数据 _dataUpdateController.reset();
}
}
// 3. 确保动画持续运行 // 重置当前帧数据和计数器
_ensureAnimationRunning(); _currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
// 4. 更新位置信息 _audioProcessCounter = 0; // 重置音频处理计数器
_currentPosition.value = waveData.length;
} }
/// 启动波形流动动画 /// 启动波形流动动画
@ -415,17 +469,11 @@ class MeetingRecordController extends GetxController
waveformAnimationController.reset(); waveformAnimationController.reset();
} }
/// 确保动画持续运行
void _ensureAnimationRunning() {
if (isRecording.value && !waveformAnimationController.isAnimating) {
waveformAnimationController.forward();
}
}
/// Saves recording and cleans up resources /// Saves recording and cleans up resources
Future<void> saveRecording() async { Future<void> saveRecording() async {
resetTimer(); resetTimer();
_stopWaveformAnimation(); // 停止波形动画 _stopWaveformAnimation(); // 停止波形动画
_stopAudioFrameTimer(); // 停止音频帧定时器
if (audioType.value != 0) { if (audioType.value != 0) {
_bleManager.closeCodec(); _bleManager.closeCodec();
} }
@ -477,19 +525,22 @@ class MeetingRecordController extends GetxController
/// TIMER CONTROL SECTION /// /// TIMER CONTROL SECTION ///
/// Starts or resumes timer /// 启动定时器
void startTimer() { void startTimer() {
if (timerStatus.value != TimerStatus.running) { if (timerStatus.value != TimerStatus.running) {
timerStatus.value = TimerStatus.running; timerStatus.value = TimerStatus.running;
_lastStartTime = DateTime.now().millisecondsSinceEpoch; _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; if (timerStatus.value != TimerStatus.running) return;
final currentTime = DateTime.now().millisecondsSinceEpoch; final currentTime = DateTime.now().millisecondsSinceEpoch;
_elapsedMilliseconds += currentTime - _lastStartTime!; _elapsedMilliseconds += currentTime - _lastStartTime!;
_lastStartTime = currentTime; _lastStartTime = currentTime;
seconds.value = (_elapsedMilliseconds / 1000).round(); seconds.value = (_elapsedMilliseconds / 1000).round();
milliseconds.value = _elapsedMilliseconds; // 更新毫秒值
}); });
} }
} }
@ -527,7 +578,4 @@ class MeetingRecordController extends GetxController
return baseHeight + rand * 10; 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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -17,7 +17,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
if (didPop) { if (didPop) {
// 在返回时执行保存操作 // 在返回时执行保存操作
controller.saveTitleChange(); //controller.saveTitleChange();
//await controller.saveRecording(); //await controller.saveRecording();
} }
}, },
@ -108,17 +108,15 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
// 使用LayoutBuilder获取准确的屏幕宽度 // 使用LayoutBuilder获取准确的屏幕宽度
final screenWidth = constraints.maxWidth; final screenWidth = constraints.maxWidth;
// 初始化最大点数 // 初始化最大可见波形条数量
if (controller.maxPoints == 0) { controller.setMaxVisibleBars(screenWidth);
controller.setMaxPoints(screenWidth);
}
return Column( return Column(
children: [ children: [
SizedBox(height: 24.h), SizedBox(height: 24.h),
// 专业级实时流动波形显示区域 // 实时流动波形显示区域
Container( Container(
height: 120.h, height: 200.h,
margin: EdgeInsets.symmetric(horizontal: 16.w), margin: EdgeInsets.symmetric(horizontal: 16.w),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@ -136,7 +134,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
const Color(0xFFF8F9FA), const Color(0xFFF8F9FA),
], ],
), ),
borderRadius: BorderRadius.circular(12.r), //borderRadius: BorderRadius.circular(12.r),
border: Border.all( border: Border.all(
color: isDarkMode color: isDarkMode
? Colors.white.withValues(alpha: 0.1) ? Colors.white.withValues(alpha: 0.1)
@ -161,7 +159,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
_WaveformWidget( _WaveformWidget(
controller: controller, controller: controller,
isDarkMode: isDarkMode, 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), SizedBox(height: 24.h),
// 时间显示 - 只包装需要响应式更新的Text widget // 时间显示 - 只包装需要响应式更新的Text widget
Obx(() => Text( Obx(() => Text(
controller.formatTime(controller.seconds.value), controller.formatTime(controller.milliseconds.value),
style: TextStyle( style: TextStyle(
fontSize: 48.sp, fontSize: 48.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.normal,
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontFeatures: const [
FontFeature.tabularFigures()
], // 使用等宽数字,避免跳动
), ),
)), )),
const Spacer(), const Spacer(),
@ -449,7 +450,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
} }
} }
/// 实时流动波形绘制器 /// 简化的实时流动波形绘制器
class RealTimeWaveformPainter extends CustomPainter { class RealTimeWaveformPainter extends CustomPainter {
final List<double> waveData; final List<double> waveData;
final bool isDarkMode; final bool isDarkMode;
@ -466,22 +467,19 @@ class RealTimeWaveformPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final centerY = size.height / 2; final centerY = size.height / 2;
final centerX = size.width / 2;
// 绘制中央基准线 // 绘制中央基准线
_drawCenterLine(canvas, size, centerX, centerY); _drawCenterLine(canvas, size, centerY);
// 只在录音状态下且有波形数据时才绘制波形 // 只在录音状态下且有波形数据时才绘制波形
if (isRecording && waveData.isNotEmpty) { if (isRecording && waveData.isNotEmpty) {
_drawWaveform(canvas, size, centerX, centerY); _drawWaveform(canvas, size, centerY);
} }
} }
/// 绘制中央时间参考线(只保留水平线) /// 绘制中央基准线
void _drawCenterLine( void _drawCenterLine(Canvas canvas, Size size, double centerY) {
Canvas canvas, Size size, double centerX, double centerY) { final linePaint = Paint()
// 只绘制水平中线
final horizontalLinePaint = Paint()
..color = ..color =
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15) (isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15)
..strokeWidth = 1.0; ..strokeWidth = 1.0;
@ -489,134 +487,109 @@ class RealTimeWaveformPainter extends CustomPainter {
canvas.drawLine( canvas.drawLine(
Offset(0, centerY), Offset(0, centerY),
Offset(size.width, centerY), Offset(size.width, centerY),
horizontalLinePaint, linePaint,
); );
// 移除垂直时间参考线和三角形指示器
} }
/// 绘制实时流动波形(扩散到整个区域) /// 绘制简化的流动波形
void _drawWaveform(Canvas canvas, Size size, double centerX, double centerY) { void _drawWaveform(Canvas canvas, Size size, double centerY) {
final barWidth = 2.5; // 波形条宽度 const double barWidth = 3.0; // 波形条宽度
final barSpacing = 0.5; // 波形条间距 const double barSpacing = 1.0; // 波形条间距
final totalBarWidth = barWidth + barSpacing; const double totalBarWidth = barWidth + barSpacing;
// 计算整个宽度可以显示多少个波形条 // 计算可显示的波形条数量
final maxBars = (size.width / totalBarWidth).floor(); 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 flowOffset = animationValue * totalBarWidth;
// 绘制波形条,从左到右填满整个区域 // 从右到左绘制波形条
final displayCount = min(dataLength, maxBars); for (int i = 0; i < visibleBars; i++) {
final dataIndex = waveData.length - 1 - i;
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;
final amplitude = waveData[dataIndex]; final amplitude = waveData[dataIndex];
// 计算透明度:中央区域更亮,边缘渐暗 // 计算波形条的x位置(从右侧开始,向左排列)
final distanceFromCenter = (x - size.width / 2).abs() / (size.width / 2); final x = size.width - (i + 1) * totalBarWidth + flowOffset;
final opacity = (1.0 - distanceFromCenter * 0.4).clamp(0.3, 1.0);
// 跳过屏幕外的波形条
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, void _drawWaveBar(Canvas canvas, double x, double centerY, double amplitude,
double amplitude, double opacity) { double index, double totalBars) {
// 将振幅转换为显示高度(上下对称) const double barWidth = 2.0;
final maxHeight = 50.0; // 最大高度 const double maxHeight = 200.0;
final barHeight = (amplitude / 100.0) * maxHeight; 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( final rect = RRect.fromRectAndRadius(
Rect.fromCenter( Rect.fromCenter(
center: Offset(x + barWidth / 2, centerY), center: Offset(x + barWidth / 2, centerY),
width: barWidth, width: barWidth,
height: barHeight * 2, // 上下对称,总高度是barHeight的2倍 height: barHeight, // 使用优化后的高度
), ),
Radius.circular(barWidth / 2), const Radius.circular(1.5),
); );
// 创建渐变效果 // 创建渐变颜色
final gradient = LinearGradient( final Color baseColor = isDarkMode
colors: _getGradientColors(opacity), ? Colors.grey[500]! // 深灰色
begin: Alignment.topCenter, : Colors.grey[600]!; // 更深灰色
end: Alignment.bottomCenter,
);
final paint = Paint() final paint = Paint()
..shader = gradient.createShader(rect.outerRect) ..color = baseColor
..style = PaintingStyle.fill; ..style = PaintingStyle.fill;
canvas.drawRRect(rect, paint); canvas.drawRRect(rect, paint);
}
// 可选:添加高光效果(仅对较新的波形条) @override
if (opacity > 0.7) { bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) {
final highlightRect = RRect.fromRectAndRadius( // 只有在以下情况才重绘
Rect.fromCenter( if (oldDelegate.isDarkMode != isDarkMode ||
center: Offset(x + barWidth / 2, centerY), oldDelegate.isRecording != isRecording) {
width: barWidth * 0.4, return true;
height: barHeight * 2 * 0.8, }
),
Radius.circular(barWidth / 4),
);
final highlightPaint = Paint()
..color = Colors.white.withValues(alpha: 0.2 * opacity)
..style = PaintingStyle.fill;
canvas.drawRRect(highlightRect, highlightPaint); // 数据长度变化才重绘
if (oldDelegate.waveData.length != waveData.length) {
return true;
} }
}
/// 获取渐变颜色 // 数据内容变化(比较最后一个值)
List<Color> _getGradientColors(double opacity) { if (waveData.isNotEmpty &&
return isDarkMode oldDelegate.waveData.isNotEmpty &&
? [ (waveData.last - oldDelegate.waveData.last).abs() > 0.1) {
Colors.blue[400]!.withValues(alpha: opacity), return true;
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),
];
}
@override // 动画值每 0.1 变化才重绘(避免每帧都重绘)
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) { return (animationValue - oldDelegate.animationValue).abs() > 0.1;
return oldDelegate.waveData != waveData ||
oldDelegate.isDarkMode != isDarkMode ||
oldDelegate.isRecording != isRecording ||
oldDelegate.animationValue != animationValue;
} }
} }
/// 专门的波形Widget,正确管理GetX响应式状态 /// 优化的波形Widget,避免双重响应式更新
class _WaveformWidget extends StatelessWidget { class _WaveformWidget extends StatefulWidget {
final MeetingRecordController controller; final MeetingRecordController controller;
final bool isDarkMode; final bool isDarkMode;
final Size size; final Size size;
@ -627,25 +600,67 @@ class _WaveformWidget extends StatelessWidget {
required this.size, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// 使用AnimatedBuilder监听动画变化 return RepaintBoundary(
return AnimatedBuilder( child: AnimatedBuilder(
animation: controller.waveformAnimation, animation: widget.controller.waveformAnimation,
builder: (context, child) { builder: (context, child) {
// 使用Obx只监听需要响应式更新的数据
return Obx(() {
return CustomPaint( return CustomPaint(
size: widget.size,
painter: RealTimeWaveformPainter( painter: RealTimeWaveformPainter(
waveData: controller.waveData, waveData: _waveData,
isDarkMode: isDarkMode, isDarkMode: widget.isDarkMode,
isRecording: controller.isRecording.value, isRecording: _isRecording,
animationValue: controller.waveformAnimation.value, animationValue: widget.controller.waveformAnimation.value,
), ),
size: size, isComplex: true, // 告诉 Flutter 这个绘制较复杂,可缓存
willChange: false, // 数据不会频繁变化(我们已优化)
); );
}); },
}, ),
); );
} }
} }

Loading…
Cancel
Save