|
|
@@ -1,4 +1,5 @@ |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:path_provider/path_provider.dart'; |
|
|
|
import 'package:flutter_sound/flutter_sound.dart'; |
|
|
|
import 'package:audioplayers/audioplayers.dart'; |
|
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
@@ -11,65 +12,168 @@ class SoundRecordScene extends StatefulWidget { |
|
|
|
class _SoundRecordSceneState extends State<SoundRecordScene> { |
|
|
|
FlutterSoundRecorder? _soundRecorder; |
|
|
|
AudioPlayer? _audioPlayer; |
|
|
|
bool _isRecorderReady = false; |
|
|
|
bool _isRecording = false; |
|
|
|
bool _isSpeaking = false; // 是否正在说话 |
|
|
|
String? _audioFilePath; |
|
|
|
double _volumeLevel = 0.0; // 当前音量值 |
|
|
|
DateTime? _lastBelowThresholdTime; // 上次音量低于阈值的时间 |
|
|
|
ScrollController _scrollController = ScrollController(); |
|
|
|
List<String> _logs = []; |
|
|
|
|
|
|
|
// 音量阈值 |
|
|
|
final double _speakingThreshold = -30.0; // 开始说话的阈值 |
|
|
|
final double _silenceThreshold = -40.0; // 结束说话的阈值 |
|
|
|
final Duration _silenceDuration = Duration(seconds: 1); // 持续低于阈值的时间 |
|
|
|
|
|
|
|
// 采样率和声道数 |
|
|
|
final int _sampleRate = 16000; // 16kHz 采样率 |
|
|
|
final int _numChannels = 1; // 单声道 |
|
|
|
|
|
|
|
@override |
|
|
|
void initState() { |
|
|
|
super.initState(); |
|
|
|
_soundRecorder = FlutterSoundRecorder(); |
|
|
|
// 监听音量变化 |
|
|
|
_soundRecorder?.onProgress?.listen((event) { |
|
|
|
_log('onProgress 回调触发, 分贝: ${event.decibels}'); |
|
|
|
if (event.decibels != null) { |
|
|
|
setState(() { |
|
|
|
_volumeLevel = event.decibels!; // 更新音量值 |
|
|
|
}); |
|
|
|
_checkSpeakingStatus(); // 检查说话状态 |
|
|
|
} |
|
|
|
}); |
|
|
|
_audioPlayer = AudioPlayer(); |
|
|
|
_requestPermissions(); |
|
|
|
_initRecorder(); |
|
|
|
} |
|
|
|
|
|
|
|
// 初始化录音器 |
|
|
|
void _initRecorder() async { |
|
|
|
try { |
|
|
|
await _soundRecorder?.openRecorder(); |
|
|
|
setState(() { |
|
|
|
_isRecorderReady = true; |
|
|
|
}); |
|
|
|
_log('录音器初始化成功'); |
|
|
|
} catch (e) { |
|
|
|
_log('初始化录音器失败: $e'); |
|
|
|
setState(() { |
|
|
|
_isRecorderReady = false; |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 请求麦克风权限 |
|
|
|
void _requestPermissions() async { |
|
|
|
try { |
|
|
|
await Permission.microphone.request(); |
|
|
|
if (await Permission.microphone.request().isGranted) { |
|
|
|
_log('麦克风权限已授予'); |
|
|
|
} else { |
|
|
|
_log('麦克风权限被拒绝'); |
|
|
|
setState(() { |
|
|
|
_isRecorderReady = false; |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
_log('播放录音失败: $e'); |
|
|
|
_log('请求麦克风权限失败: $e'); |
|
|
|
setState(() { |
|
|
|
_isRecorderReady = false; |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 开始录音 |
|
|
|
void _startRecording() async { |
|
|
|
try { |
|
|
|
_log('录音开始'); |
|
|
|
final tempPath = |
|
|
|
'/data/user/0/com.example.flutter_app/cache/recorded_audio.aac'; // 可根据需要调整路径 |
|
|
|
if (!_isRecorderReady) { |
|
|
|
_log('录音器未准备好'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (_isRecording) return; // 防止重复调用 |
|
|
|
|
|
|
|
final directory = await getTemporaryDirectory(); |
|
|
|
final tempPath = '${directory.path}/recorded_audio.aac'; |
|
|
|
_log('录音文件路径: $tempPath'); |
|
|
|
|
|
|
|
await _soundRecorder?.startRecorder( |
|
|
|
toFile: tempPath, |
|
|
|
codec: Codec.aacADTS, |
|
|
|
sampleRate: _sampleRate, // 设置采样率 |
|
|
|
numChannels: _numChannels, // 设置声道数 |
|
|
|
enableVoiceProcessing: false, // 启用音量监听 |
|
|
|
); |
|
|
|
setState(() { |
|
|
|
_audioFilePath = tempPath; |
|
|
|
_isRecording = true; |
|
|
|
}); |
|
|
|
_log('录音开始'); |
|
|
|
} catch (e) { |
|
|
|
_log('播放录音失败: $e'); |
|
|
|
_log('录音开始 异常: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 停止录音 |
|
|
|
void _stopRecording() async { |
|
|
|
try { |
|
|
|
if (!_isRecording) return; // 防止重复调用 |
|
|
|
await _soundRecorder?.stopRecorder(); |
|
|
|
setState(() { |
|
|
|
_isRecording = false; |
|
|
|
_volumeLevel = 0.0; //重置音量值 |
|
|
|
}); |
|
|
|
_log('录音停止'); |
|
|
|
} catch (e) { |
|
|
|
_log('播放录音失败: $e'); |
|
|
|
_log('录音停止 异常: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 播放录音 |
|
|
|
void _playRecording() async { |
|
|
|
if (_audioFilePath != null) { |
|
|
|
await _audioPlayer?.play(DeviceFileSource(_audioFilePath!)); |
|
|
|
_log('播放录音'); |
|
|
|
try { |
|
|
|
if (_audioFilePath != null) { |
|
|
|
await _audioPlayer?.play(DeviceFileSource(_audioFilePath!)); |
|
|
|
_log('播放录音'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
_log('播放录音 异常: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 检查说话状态 |
|
|
|
void _checkSpeakingStatus() { |
|
|
|
if (_volumeLevel > _speakingThreshold && !_isSpeaking) { |
|
|
|
// 音量高于阈值,表示开始说话 |
|
|
|
setState(() { |
|
|
|
_isSpeaking = true; |
|
|
|
}); |
|
|
|
_log('开始说话'); |
|
|
|
} else if (_volumeLevel < _silenceThreshold) { |
|
|
|
// 音量低于阈值 |
|
|
|
if (_lastBelowThresholdTime == null) { |
|
|
|
// 记录第一次低于阈值的时间 |
|
|
|
_lastBelowThresholdTime = DateTime.now(); |
|
|
|
} else if (DateTime.now().difference(_lastBelowThresholdTime!) > |
|
|
|
_silenceDuration) { |
|
|
|
// 持续低于阈值超过指定时间,表示结束说话 |
|
|
|
if (_isSpeaking) { |
|
|
|
setState(() { |
|
|
|
_isSpeaking = false; |
|
|
|
}); |
|
|
|
_log('结束说话'); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
// 音量恢复到阈值以上,重置计时器 |
|
|
|
_lastBelowThresholdTime = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 添加日志信息并自动滚动 |
|
|
|
void _log(String message) { |
|
|
|
print("输出日志:${message}"); |
|
|
|
setState(() { |
|
|
|
_logs.add(message); // 从顶部插入新日志 |
|
|
|
}); |
|
|
@@ -128,31 +232,64 @@ class _SoundRecordSceneState extends State<SoundRecordScene> { |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
// 按钮区域 |
|
|
|
GestureDetector( |
|
|
|
onTapDown: (details) { |
|
|
|
_startRecording(); // 按下时开始录音 |
|
|
|
}, |
|
|
|
onTapUp: (details) { |
|
|
|
_stopRecording(); // 抬起时停止录音并播放 |
|
|
|
_playRecording(); // 播放录音 |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
margin: EdgeInsets.all(20), |
|
|
|
padding: EdgeInsets.all(20), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.blue, |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.mic, |
|
|
|
color: Colors.white, |
|
|
|
size: 50, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
|
|
|
|
// 底部按钮区域 |
|
|
|
Padding( |
|
|
|
padding: const EdgeInsets.all(20.0), |
|
|
|
child: |
|
|
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [ |
|
|
|
// 音量图标和音量值 |
|
|
|
Row( |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.volume_up, |
|
|
|
size: 30, |
|
|
|
), |
|
|
|
SizedBox(width: 10), // 间距 |
|
|
|
Text( |
|
|
|
'${_volumeLevel.toStringAsFixed(2)} dB', //显示音量值,保留两位小数 |
|
|
|
style: TextStyle(fontSize: 16), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
SizedBox(width: 20), // 间距 |
|
|
|
// 按钮区域 |
|
|
|
GestureDetector( |
|
|
|
onTapDown: (details) { |
|
|
|
_startRecording(); // 按下时开始录音 |
|
|
|
}, |
|
|
|
onTapUp: (details) { |
|
|
|
_stopRecording(); // 抬起时停止录音并播放 |
|
|
|
_playRecording(); // 播放录音 |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
margin: EdgeInsets.all(20), |
|
|
|
padding: EdgeInsets.all(20), |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Colors.blue, |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
Icons.mic, |
|
|
|
color: Colors.white, |
|
|
|
size: 50, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
])) |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 根据音量值动态调整图标颜色 |
|
|
|
Color _getVolumeColor() { |
|
|
|
if (_volumeLevel < -40) { |
|
|
|
return Colors.green; // 低音量 |
|
|
|
} else if (_volumeLevel < -20) { |
|
|
|
return Colors.yellow; // 中音量 |
|
|
|
} else { |
|
|
|
return Colors.red; // 高音量 |
|
|
|
} |
|
|
|
} |
|
|
|
} |