Browse Source

refactor(meeting): 替换自定义波形实现为audio_waveforms库

移除自定义波形动画和数据处理逻辑,改用audio_waveforms库实现
简化控制器代码,移除未使用的变量和动画控制器
更新视图层以适配新的波形显示组件
newdev_shunjiawei
lxm 9 months ago
parent
commit
19dd3ee0f5
  1. 225
      lib/modules/meeting/controllers/meeting_record_controller.dart
  2. 349
      lib/modules/meeting/views/meeting_record_view.dart

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

@ -5,6 +5,7 @@ import 'dart:math';
import 'dart:typed_data';
import '/core/utils/logger.dart';
import '/core/utils/permission_util.dart';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
@ -40,7 +41,9 @@ class MeetingRecordController extends GetxController
final RxBool showTextContent = false.obs;
final RxBool isRecording = false.obs;
//final RxBool isVoiceRecognitionActive = false.obs;
final RxList<double> waveData = <double>[].obs;
final RxDouble rms = 0.0.obs;
final RecorderController waveformController = RecorderController();
// Transcription content
final RxString intermediateContent = ''.obs;
@ -61,46 +64,23 @@ class MeetingRecordController extends GetxController
// 移除了未使用的变量和TabController
StreamSubscription<Uint8List>? _audioDataSubscription;
// 简化的音波数据管理
final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率
int maxVisibleBars = 60; // 最大可见波形条数量
Timer? _audioFrameTimer; // 音频帧定时器
double _currentFrameAmplitude = 0.0; // 当前帧累积振幅
int _currentFrameSamples = 0; // 当前帧样本数量
// 性能优化:音频处理采样控制
int _audioProcessCounter = 0;
static const int _audioProcessInterval = 1; // 每1个音频包处理1次
// 波形流动动画控制器
late AnimationController waveformAnimationController;
late Animation<double> waveformAnimation;
// 数据更新控制器 - 统一管理数据更新和动画
late AnimationController _dataUpdateController;
late Animation<double> _dataUpdateAnimation;
// 设置最大可见波形条数量(根据屏幕宽度)
void setMaxVisibleBars(double screenWidth) {
const barWidth = 3.0; // 波形条宽度
const barSpacing = 1.0; // 波形条间距
maxVisibleBars = (screenWidth / (barWidth + barSpacing)).floor();
}
double _smoothedRms = 0.0;
double _latestRms = 0.0;
Timer? _waveformTimer;
static const Duration _waveformTick = Duration(milliseconds: 40);
static const int _maxWaveSamples = 1200;
@override
void onInit() {
super.onInit();
_initializeComponents();
_setupFromArguments();
_initializeWaveformAnimation();
//generateWaveData();
}
@override
void onClose() {
_cleanupResources();
// 清理两个AnimationController
waveformAnimationController.dispose();
_dataUpdateController.dispose();
super.onClose();
}
@ -134,56 +114,6 @@ 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: Duration(milliseconds: frameIntervalMs), // 与数据更新同步
vsync: this,
);
waveformAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: waveformAnimationController,
curve: Curves.linear, // 线性动画确保匀速流动
));
// 监听动画完成,实现连续循环
waveformAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed && isRecording.value) {
waveformAnimationController.reset();
waveformAnimationController.forward();
}
});
}
/// Sets up initial state from navigation arguments
void _setupFromArguments() {
final Map<String, dynamic> args = Get.arguments ?? {};
@ -207,8 +137,7 @@ class MeetingRecordController extends GetxController
/// Cleans up resources when controller is disposed
void _cleanupResources() {
_timer?.cancel();
_audioFrameTimer?.cancel(); // 清理音频帧定时器
// AnimationController在onClose中统一清理
_stopWaveformTimer();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
_audioDataSubscription?.cancel();
@ -220,6 +149,11 @@ class MeetingRecordController extends GetxController
}
_asrService.stopRecord(true);
isRecording.value = false;
rms.value = 0.0;
_smoothedRms = 0.0;
_latestRms = 0.0;
waveformController.reset();
waveformController.dispose();
}
/// Handles focus changes in title text field
@ -321,8 +255,6 @@ class MeetingRecordController extends GetxController
// 1. 实时音频可视化
// 2. 音频质量检测
// 3. 发送到其他服务
Logger.d("AudioData", "接收到音频数据: ${audioData.length} 字节");
// 处理音频数据的逻辑
_processAudioData(audioData);
});
@ -333,12 +265,10 @@ class MeetingRecordController extends GetxController
newName.value = fullFileName;
titleEditingController.text = fullFileName;
// 启动波形动画和音频帧定时器
_startWaveformAnimation();
_startAudioFrameTimer();
// Start hardware components
_startHardwareServices();
_resetWaveform();
_startWaveformTimer();
startTimer();
}
@ -362,55 +292,59 @@ class MeetingRecordController extends GetxController
/// Pauses current recording session
Future<void> _pauseRecording() async {
await _asrService.pauseRecord();
_stopWaveformAnimation(); // 暂停时停止动画
_stopAudioFrameTimer(); // 停止音频帧定时器
_stopWaveformTimer();
rms.value = 0.0;
pauseTimer();
}
/// Resumes paused recording session
Future<void> _resumeRecording() async {
await _asrService.resumeRecord();
_startWaveformAnimation(); // 恢复时启动动画
_startAudioFrameTimer(); // 重启音频帧定时器
_startWaveformTimer();
startTimer();
}
/// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化
/// 处理原始Uint8List音频数据
void _processAudioData(Uint8List audioData) {
if (!isRecording.value) return;
// 性能优化:不是每个音频包都处理,减少CPU占用
_audioProcessCounter++;
if (_audioProcessCounter % _audioProcessInterval != 0) {
return; // 跳过这次处理
final next = _calculateAmplitude(audioData);
if (_smoothedRms == 0.0) {
_smoothedRms = next;
} else {
_smoothedRms = _smoothedRms * 0.8 + next * 0.2;
}
rms.value = _smoothedRms;
_latestRms = _smoothedRms;
}
// 将音频数据转换为振幅值并累积到当前帧
final amplitude = _calculateAmplitude(audioData);
_currentFrameAmplitude += amplitude;
_currentFrameSamples++;
void _resetWaveform() {
waveformController.reset();
waveformController.updateFrequency = _waveformTick;
}
/// 提交音频帧数据到波形显示
void _commitAudioFrame() {
// 计算当前帧的平均振幅
double frameAmplitude = 0.0;
if (_currentFrameSamples > 0) {
frameAmplitude = _currentFrameAmplitude / _currentFrameSamples;
}
void _startWaveformTimer() {
_waveformTimer?.cancel();
_waveformTimer = Timer.periodic(_waveformTick, (_) {
if (!isRecording.value) return;
if (timerStatus.value != TimerStatus.running) return;
_appendWaveSample(_latestRms);
});
}
// 转换为显示数据
final newAmplitude = frameAmplitude * 800.0;
void _stopWaveformTimer() {
_waveformTimer?.cancel();
_waveformTimer = null;
}
// 更新波形数据
if (waveData.length >= maxVisibleBars) {
waveData.removeAt(0);
void _appendWaveSample(double value) {
final normalized = value.clamp(0.0, 1.0);
final eased = sqrt(normalized);
final data = waveformController.waveData;
data.add(eased);
if (data.length > _maxWaveSamples) {
data.removeRange(0, data.length - _maxWaveSamples);
}
waveData.add(newAmplitude);
// 重置当前帧数据
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
waveformController.notifyListeners();
}
/// 计算音频数据的振幅
@ -436,48 +370,10 @@ class MeetingRecordController extends GetxController
return (rms / 32768.0).clamp(0.0, 1.0);
}
/// 启动数据更新控制器 - 替代Timer
void _startAudioFrameTimer() {
_audioFrameTimer?.cancel(); // 清理旧的Timer(如果有)
_audioFrameTimer = null;
// 启动数据更新控制器
_dataUpdateController.forward();
}
/// 停止数据更新控制器
void _stopAudioFrameTimer() {
_audioFrameTimer?.cancel();
_audioFrameTimer = null;
// 停止数据更新控制器
_dataUpdateController.stop();
_dataUpdateController.reset();
// 重置当前帧数据和计数器
_currentFrameAmplitude = 0.0;
_currentFrameSamples = 0;
_audioProcessCounter = 0; // 重置音频处理计数器
}
/// 启动波形流动动画
void _startWaveformAnimation() {
if (isRecording.value && !waveformAnimationController.isAnimating) {
waveformAnimationController.forward();
}
}
/// 停止波形动画
void _stopWaveformAnimation() {
waveformAnimationController.stop();
waveformAnimationController.reset();
}
/// Saves recording and cleans up resources
Future<void> saveRecording() async {
resetTimer();
_stopWaveformAnimation(); // 停止波形动画
_stopAudioFrameTimer(); // 停止音频帧定时器
_stopWaveformTimer();
if (audioType.value != 0) {
_bleManager.closeCodec();
}
@ -506,6 +402,10 @@ class MeetingRecordController extends GetxController
_resetToDefaultState();
isRecording.value = false;
rms.value = 0.0;
_smoothedRms = 0.0;
_latestRms = 0.0;
_resetWaveform();
}
/// Resets controller to initial state
@ -571,15 +471,4 @@ class MeetingRecordController extends GetxController
seconds.value = 0;
timerStatus.value = TimerStatus.stopped;
}
/// UI HELPERS SECTION ///
/// Generates mock waveform data for visualization
void generateWaveData() {
waveData.value = List.generate(120, (i) {
final rand = (i % 20) / 20;
final baseHeight = (sin(i / 5) * 0.5 + 0.5) * 25;
return baseHeight + rand * 10;
});
}
}

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

@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
@ -108,16 +108,21 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
// 使用LayoutBuilder获取准确的屏幕宽度
final screenWidth = constraints.maxWidth;
// 初始化最大可见波形条数量
controller.setMaxVisibleBars(screenWidth);
return Column(
children: [
SizedBox(height: 24.h),
SizedBox(height: 16.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Align(
alignment: Alignment.centerLeft,
child: _buildRecordTypeBadge(isDarkMode),
),
),
SizedBox(height: 12.h),
// 实时流动波形显示区域
Container(
height: 200.h,
margin: EdgeInsets.symmetric(horizontal: 16.w),
margin: EdgeInsets.zero,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
@ -155,12 +160,49 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
borderRadius: BorderRadius.circular(12.r),
child: Stack(
children: [
// 实时流动波形绘制组件
_WaveformWidget(
controller: controller,
isDarkMode: isDarkMode,
size: Size(screenWidth - 32.w, 200.h),
),
Obx(() {
final size = Size(screenWidth, 200.h);
final waveColor =
isDarkMode ? Colors.white : Colors.black87;
final shader = ui.Gradient.linear(
const Offset(0, 0),
Offset(size.width, 0),
[
waveColor.withValues(alpha: 0.0),
waveColor.withValues(alpha: 0.25),
waveColor.withValues(alpha: 0.6),
waveColor.withValues(alpha: 0.95),
],
const [0.0, 0.55, 0.8, 1.0],
);
return Center(
child: AudioWaveforms(
size: size,
recorderController: controller.waveformController,
backgroundColor: Colors.transparent,
waveStyle: WaveStyle(
waveThickness: 1.w,
spacing: 2.w,
waveCap: StrokeCap.round,
showMiddleLine: true,
middleLineColor: Colors.redAccent.withValues(
alpha: controller.timerStatus.value ==
TimerStatus.running
? 0.9
: 0.0,
),
middleLineThickness: 1.w,
showTop: true,
showBottom: true,
extendWaveform: true,
// gradient: shader,
waveColor: waveColor,
scaleFactor: 90,
),
),
);
}),
// 录音状态指示器
Obx(() {
@ -217,7 +259,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
fontWeight: FontWeight.normal,
color: isDarkMode ? Colors.white : Colors.black87,
fontFeatures: const [
FontFeature.tabularFigures()
ui.FontFeature.tabularFigures()
], // 使用等宽数字,避免跳动
),
)),
@ -250,6 +292,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
tooltip: controller.isMarked.value
? 'unmarkFavorite'.tr
: 'markFavorite'.tr,
isDarkMode: isDarkMode,
),
// 录音按钮
_buildActionButton(
@ -265,6 +308,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
tooltip: controller.timerStatus.value == TimerStatus.running
? 'pauseRecording'.tr
: 'startRecording'.tr,
isDarkMode: isDarkMode,
),
// 完成按钮
_buildActionButton(
@ -276,6 +320,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
radius: 32.w,
tooltip: 'finishRecording'.tr,
isDarkMode: isDarkMode,
),
],
),
@ -288,11 +333,14 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
required Widget child,
required double radius,
required String tooltip,
required bool isDarkMode,
}) {
final Color bgColor = isDarkMode ? const Color(0xFF1E1E1E) : Colors.white;
return Tooltip(
message: tooltip,
child: Material(
color: Colors.white,
color: bgColor,
shape: const CircleBorder(),
elevation: 4,
child: InkWell(
@ -300,7 +348,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
customBorder: const CircleBorder(),
child: CircleAvatar(
radius: radius,
backgroundColor: Colors.white,
backgroundColor: bgColor,
child: child,
),
),
@ -308,6 +356,60 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
);
}
Widget _buildRecordTypeBadge(bool isDarkMode) {
return Obx(() {
String label;
IconData icon;
Color color;
switch (controller.audioType.value) {
case 0:
label = 'liveRecording'.tr;
icon = Icons.mic_rounded;
color = Colors.red;
break;
case 1:
label = 'audioVideoRecording'.tr;
icon = Icons.videocam_rounded;
color = Colors.green;
break;
case 2:
default:
label = 'callRecording'.tr;
icon = Icons.phone_in_talk_rounded;
color = Colors.orange;
break;
}
return Container(
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 6.h),
decoration: BoxDecoration(
color: color.withValues(alpha: isDarkMode ? 0.25 : 0.12),
borderRadius: BorderRadius.circular(999.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 18.sp,
color: isDarkMode ? Colors.white : color,
),
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
],
),
);
});
}
// 底部编辑弹窗
void _bottomSheet(BuildContext context, bool isDarkMode) {
Get.bottomSheet(
@ -450,218 +552,3 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
);
}
}
/// 简化的实时流动波形绘制器
class RealTimeWaveformPainter extends CustomPainter {
final List<double> waveData;
final bool isDarkMode;
final bool isRecording;
final double animationValue; // 动画值,用于流动效果
RealTimeWaveformPainter({
required this.waveData,
required this.isDarkMode,
required this.isRecording,
this.animationValue = 0.0,
});
@override
void paint(Canvas canvas, Size size) {
final centerY = size.height / 2;
// 绘制中央基准线
_drawCenterLine(canvas, size, centerY);
// 只在录音状态下且有波形数据时才绘制波形
if (isRecording && waveData.isNotEmpty) {
_drawWaveform(canvas, size, centerY);
}
}
/// 绘制中央基准线
void _drawCenterLine(Canvas canvas, Size size, double centerY) {
final linePaint = Paint()
..color =
(isDarkMode ? Colors.white : Colors.black).withValues(alpha: 0.15)
..strokeWidth = 1.0;
canvas.drawLine(
Offset(0, centerY),
Offset(size.width, centerY),
linePaint,
);
}
/// 绘制简化的流动波形
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;
// 计算流动偏移量(从右向左移动)
final flowOffset = animationValue * totalBarWidth;
// 从右到左绘制波形条
for (int i = 0; i < visibleBars; i++) {
final dataIndex = waveData.length - 1 - i;
final amplitude = waveData[dataIndex];
// 计算波形条的x位置(从右侧开始,向左排列)
final x = size.width - (i + 1) * totalBarWidth + flowOffset;
// 跳过屏幕外的波形条
if (x < -barWidth || x > size.width) continue;
_drawWaveBar(
canvas, x, centerY, amplitude, i.toDouble(), visibleBars.toDouble());
}
}
/// 绘制单个波形条
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, // 使用优化后的高度
),
const Radius.circular(1.5),
);
// 创建渐变颜色
final Color baseColor = isDarkMode
? Colors.grey[500]! // 深灰色
: Colors.grey[600]!; // 更深灰色
final paint = Paint()
..color = baseColor
..style = PaintingStyle.fill;
canvas.drawRRect(rect, paint);
}
@override
bool shouldRepaint(covariant RealTimeWaveformPainter oldDelegate) {
// 只有在以下情况才重绘
if (oldDelegate.isDarkMode != isDarkMode ||
oldDelegate.isRecording != isRecording) {
return true;
}
// 数据长度变化才重绘
if (oldDelegate.waveData.length != waveData.length) {
return true;
}
// 数据内容变化(比较最后一个值)
if (waveData.isNotEmpty &&
oldDelegate.waveData.isNotEmpty &&
(waveData.last - oldDelegate.waveData.last).abs() > 0.1) {
return true;
}
// 动画值每 0.1 变化才重绘(避免每帧都重绘)
return (animationValue - oldDelegate.animationValue).abs() > 0.1;
}
}
/// 优化的波形Widget,避免双重响应式更新
class _WaveformWidget extends StatefulWidget {
final MeetingRecordController controller;
final bool isDarkMode;
final Size size;
const _WaveformWidget({
required this.controller,
required this.isDarkMode,
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) {
return RepaintBoundary(
child: AnimatedBuilder(
animation: widget.controller.waveformAnimation,
builder: (context, child) {
return CustomPaint(
size: widget.size,
painter: RealTimeWaveformPainter(
waveData: _waveData,
isDarkMode: widget.isDarkMode,
isRecording: _isRecording,
animationValue: widget.controller.waveformAnimation.value,
),
isComplex: true, // 告诉 Flutter 这个绘制较复杂,可缓存
willChange: false, // 数据不会频繁变化(我们已优化)
);
},
),
);
}
}

Loading…
Cancel
Save