|
|
@ -5,6 +5,7 @@ import 'dart:math'; |
|
|
import 'dart:typed_data'; |
|
|
import 'dart:typed_data'; |
|
|
import '/core/utils/logger.dart'; |
|
|
import '/core/utils/logger.dart'; |
|
|
import '/core/utils/permission_util.dart'; |
|
|
import '/core/utils/permission_util.dart'; |
|
|
|
|
|
import 'package:audio_waveforms/audio_waveforms.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:intl/intl.dart'; |
|
|
import 'package:intl/intl.dart'; |
|
|
@ -40,7 +41,9 @@ class MeetingRecordController extends GetxController |
|
|
final RxBool showTextContent = false.obs; |
|
|
final RxBool showTextContent = false.obs; |
|
|
final RxBool isRecording = false.obs; |
|
|
final RxBool isRecording = false.obs; |
|
|
//final RxBool isVoiceRecognitionActive = 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 |
|
|
// Transcription content |
|
|
final RxString intermediateContent = ''.obs; |
|
|
final RxString intermediateContent = ''.obs; |
|
|
@ -61,46 +64,23 @@ class MeetingRecordController extends GetxController |
|
|
// 移除了未使用的变量和TabController |
|
|
// 移除了未使用的变量和TabController |
|
|
|
|
|
|
|
|
StreamSubscription<Uint8List>? _audioDataSubscription; |
|
|
StreamSubscription<Uint8List>? _audioDataSubscription; |
|
|
// 简化的音波数据管理 |
|
|
double _smoothedRms = 0.0; |
|
|
final int frameIntervalMs = 300; // 300ms间隔,更稳定的帧率 |
|
|
double _latestRms = 0.0; |
|
|
int maxVisibleBars = 60; // 最大可见波形条数量 |
|
|
Timer? _waveformTimer; |
|
|
Timer? _audioFrameTimer; // 音频帧定时器 |
|
|
static const Duration _waveformTick = Duration(milliseconds: 40); |
|
|
double _currentFrameAmplitude = 0.0; // 当前帧累积振幅 |
|
|
static const int _maxWaveSamples = 1200; |
|
|
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(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
void onInit() { |
|
|
void onInit() { |
|
|
super.onInit(); |
|
|
super.onInit(); |
|
|
_initializeComponents(); |
|
|
_initializeComponents(); |
|
|
_setupFromArguments(); |
|
|
_setupFromArguments(); |
|
|
_initializeWaveformAnimation(); |
|
|
|
|
|
//generateWaveData(); |
|
|
//generateWaveData(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
void onClose() { |
|
|
void onClose() { |
|
|
_cleanupResources(); |
|
|
_cleanupResources(); |
|
|
// 清理两个AnimationController |
|
|
|
|
|
waveformAnimationController.dispose(); |
|
|
|
|
|
_dataUpdateController.dispose(); |
|
|
|
|
|
super.onClose(); |
|
|
super.onClose(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -134,56 +114,6 @@ class MeetingRecordController extends GetxController |
|
|
titleFocusNode.addListener(_handleTitleFocusChange); |
|
|
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 |
|
|
/// Sets up initial state from navigation arguments |
|
|
void _setupFromArguments() { |
|
|
void _setupFromArguments() { |
|
|
final Map<String, dynamic> args = Get.arguments ?? {}; |
|
|
final Map<String, dynamic> args = Get.arguments ?? {}; |
|
|
@ -207,8 +137,7 @@ 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(); // 清理音频帧定时器 |
|
|
_stopWaveformTimer(); |
|
|
// AnimationController在onClose中统一清理 |
|
|
|
|
|
_recognitionSubscription?.cancel(); |
|
|
_recognitionSubscription?.cancel(); |
|
|
_recognitionSubscription = null; |
|
|
_recognitionSubscription = null; |
|
|
_audioDataSubscription?.cancel(); |
|
|
_audioDataSubscription?.cancel(); |
|
|
@ -220,6 +149,11 @@ class MeetingRecordController extends GetxController |
|
|
} |
|
|
} |
|
|
_asrService.stopRecord(true); |
|
|
_asrService.stopRecord(true); |
|
|
isRecording.value = false; |
|
|
isRecording.value = false; |
|
|
|
|
|
rms.value = 0.0; |
|
|
|
|
|
_smoothedRms = 0.0; |
|
|
|
|
|
_latestRms = 0.0; |
|
|
|
|
|
waveformController.reset(); |
|
|
|
|
|
waveformController.dispose(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// Handles focus changes in title text field |
|
|
/// Handles focus changes in title text field |
|
|
@ -321,8 +255,6 @@ class MeetingRecordController extends GetxController |
|
|
// 1. 实时音频可视化 |
|
|
// 1. 实时音频可视化 |
|
|
// 2. 音频质量检测 |
|
|
// 2. 音频质量检测 |
|
|
// 3. 发送到其他服务 |
|
|
// 3. 发送到其他服务 |
|
|
Logger.d("AudioData", "接收到音频数据: ${audioData.length} 字节"); |
|
|
|
|
|
|
|
|
|
|
|
// 处理音频数据的逻辑 |
|
|
// 处理音频数据的逻辑 |
|
|
_processAudioData(audioData); |
|
|
_processAudioData(audioData); |
|
|
}); |
|
|
}); |
|
|
@ -333,12 +265,10 @@ class MeetingRecordController extends GetxController |
|
|
newName.value = fullFileName; |
|
|
newName.value = fullFileName; |
|
|
titleEditingController.text = fullFileName; |
|
|
titleEditingController.text = fullFileName; |
|
|
|
|
|
|
|
|
// 启动波形动画和音频帧定时器 |
|
|
|
|
|
_startWaveformAnimation(); |
|
|
|
|
|
_startAudioFrameTimer(); |
|
|
|
|
|
|
|
|
|
|
|
// Start hardware components |
|
|
// Start hardware components |
|
|
_startHardwareServices(); |
|
|
_startHardwareServices(); |
|
|
|
|
|
_resetWaveform(); |
|
|
|
|
|
_startWaveformTimer(); |
|
|
startTimer(); |
|
|
startTimer(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -362,55 +292,59 @@ class MeetingRecordController extends GetxController |
|
|
/// Pauses current recording session |
|
|
/// Pauses current recording session |
|
|
Future<void> _pauseRecording() async { |
|
|
Future<void> _pauseRecording() async { |
|
|
await _asrService.pauseRecord(); |
|
|
await _asrService.pauseRecord(); |
|
|
_stopWaveformAnimation(); // 暂停时停止动画 |
|
|
_stopWaveformTimer(); |
|
|
_stopAudioFrameTimer(); // 停止音频帧定时器 |
|
|
rms.value = 0.0; |
|
|
pauseTimer(); |
|
|
pauseTimer(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// Resumes paused recording session |
|
|
/// Resumes paused recording session |
|
|
Future<void> _resumeRecording() async { |
|
|
Future<void> _resumeRecording() async { |
|
|
await _asrService.resumeRecord(); |
|
|
await _asrService.resumeRecord(); |
|
|
_startWaveformAnimation(); // 恢复时启动动画 |
|
|
_startWaveformTimer(); |
|
|
_startAudioFrameTimer(); // 重启音频帧定时器 |
|
|
|
|
|
startTimer(); |
|
|
startTimer(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 处理原始Uint8List音频数据 - 只累积,不更新UI,性能优化 |
|
|
/// 处理原始Uint8List音频数据 |
|
|
void _processAudioData(Uint8List audioData) { |
|
|
void _processAudioData(Uint8List audioData) { |
|
|
if (!isRecording.value) return; |
|
|
if (!isRecording.value) return; |
|
|
|
|
|
final next = _calculateAmplitude(audioData); |
|
|
// 性能优化:不是每个音频包都处理,减少CPU占用 |
|
|
if (_smoothedRms == 0.0) { |
|
|
_audioProcessCounter++; |
|
|
_smoothedRms = next; |
|
|
if (_audioProcessCounter % _audioProcessInterval != 0) { |
|
|
} else { |
|
|
return; // 跳过这次处理 |
|
|
_smoothedRms = _smoothedRms * 0.8 + next * 0.2; |
|
|
} |
|
|
} |
|
|
|
|
|
rms.value = _smoothedRms; |
|
|
|
|
|
_latestRms = _smoothedRms; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 将音频数据转换为振幅值并累积到当前帧 |
|
|
void _resetWaveform() { |
|
|
final amplitude = _calculateAmplitude(audioData); |
|
|
waveformController.reset(); |
|
|
_currentFrameAmplitude += amplitude; |
|
|
waveformController.updateFrequency = _waveformTick; |
|
|
_currentFrameSamples++; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 提交音频帧数据到波形显示 |
|
|
void _startWaveformTimer() { |
|
|
void _commitAudioFrame() { |
|
|
_waveformTimer?.cancel(); |
|
|
// 计算当前帧的平均振幅 |
|
|
_waveformTimer = Timer.periodic(_waveformTick, (_) { |
|
|
double frameAmplitude = 0.0; |
|
|
if (!isRecording.value) return; |
|
|
if (_currentFrameSamples > 0) { |
|
|
if (timerStatus.value != TimerStatus.running) return; |
|
|
frameAmplitude = _currentFrameAmplitude / _currentFrameSamples; |
|
|
_appendWaveSample(_latestRms); |
|
|
} |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 转换为显示数据 |
|
|
void _stopWaveformTimer() { |
|
|
final newAmplitude = frameAmplitude * 800.0; |
|
|
_waveformTimer?.cancel(); |
|
|
|
|
|
_waveformTimer = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// 更新波形数据 |
|
|
void _appendWaveSample(double value) { |
|
|
if (waveData.length >= maxVisibleBars) { |
|
|
final normalized = value.clamp(0.0, 1.0); |
|
|
waveData.removeAt(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); |
|
|
waveformController.notifyListeners(); |
|
|
|
|
|
|
|
|
// 重置当前帧数据 |
|
|
|
|
|
_currentFrameAmplitude = 0.0; |
|
|
|
|
|
_currentFrameSamples = 0; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 计算音频数据的振幅 |
|
|
/// 计算音频数据的振幅 |
|
|
@ -436,48 +370,10 @@ class MeetingRecordController extends GetxController |
|
|
return (rms / 32768.0).clamp(0.0, 1.0); |
|
|
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 |
|
|
/// Saves recording and cleans up resources |
|
|
Future<void> saveRecording() async { |
|
|
Future<void> saveRecording() async { |
|
|
resetTimer(); |
|
|
resetTimer(); |
|
|
_stopWaveformAnimation(); // 停止波形动画 |
|
|
_stopWaveformTimer(); |
|
|
_stopAudioFrameTimer(); // 停止音频帧定时器 |
|
|
|
|
|
if (audioType.value != 0) { |
|
|
if (audioType.value != 0) { |
|
|
_bleManager.closeCodec(); |
|
|
_bleManager.closeCodec(); |
|
|
} |
|
|
} |
|
|
@ -506,6 +402,10 @@ class MeetingRecordController extends GetxController |
|
|
|
|
|
|
|
|
_resetToDefaultState(); |
|
|
_resetToDefaultState(); |
|
|
isRecording.value = false; |
|
|
isRecording.value = false; |
|
|
|
|
|
rms.value = 0.0; |
|
|
|
|
|
_smoothedRms = 0.0; |
|
|
|
|
|
_latestRms = 0.0; |
|
|
|
|
|
_resetWaveform(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// Resets controller to initial state |
|
|
/// Resets controller to initial state |
|
|
@ -571,15 +471,4 @@ class MeetingRecordController extends GetxController |
|
|
seconds.value = 0; |
|
|
seconds.value = 0; |
|
|
timerStatus.value = TimerStatus.stopped; |
|
|
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; |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|