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