您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

327 行
10 KiB

  1. import 'dart:async';
  2. import 'dart:typed_data';
  3. import 'package:demo001/xunfei/xunfei.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:path_provider/path_provider.dart';
  6. import 'package:flutter_sound/flutter_sound.dart';
  7. import 'package:audioplayers/audioplayers.dart';
  8. import 'package:permission_handler/permission_handler.dart';
  9. class SoundRecordScene extends StatefulWidget {
  10. @override
  11. _SoundRecordSceneState createState() => _SoundRecordSceneState();
  12. }
  13. class _SoundRecordSceneState extends State<SoundRecordScene> {
  14. late ISDK _sdk;
  15. FlutterSoundRecorder? _soundRecorder;
  16. AudioPlayer? _audioPlayer;
  17. bool _isRecorderReady = false;
  18. bool _isRecording = false;
  19. bool _isSpeaking = false; //是否说话
  20. int _stateSpeak = 0; // 说话状态 0 未说话 1开始说话 2 说话中 3结束说话
  21. String? _audioFilePath;
  22. double _volumeLevel = 0.0; // 当前音量值
  23. DateTime? _lastBelowThresholdTime; // 上次音量低于阈值的时间
  24. ScrollController _scrollController = ScrollController();
  25. List<String> _logs = [];
  26. List<ITaskTrans> _trans = [];
  27. late ITaskTrans _lasttran;
  28. // 音量阈值
  29. final double _speakingThreshold = 50.0; // 开始说话的阈值
  30. final double _silenceThreshold = 30.0; // 结束说话的阈值
  31. final Duration _silenceDuration = Duration(seconds: 1); // 持续低于阈值的时间
  32. // 采样率和声道数
  33. Codec _audiocodec = Codec.pcm16;
  34. final int _sampleRate = 16000; // 16kHz 采样率
  35. final int _numChannels = 1; // 单声道
  36. StreamController<Uint8List> _audioDataStreamController = StreamController<Uint8List>.broadcast();
  37. //暴露音频数据流
  38. Stream<Uint8List> get audioDataStream => _audioDataStreamController.stream;
  39. @override
  40. void initState() {
  41. super.initState();
  42. _sdk = Xunfei(
  43. appId: "137dc132",
  44. apiKey: "1c1891a475e71250ecd1320303ad6545",
  45. apiSecret: "MjZhNDA1NTI1NWZkZDQxOTMxYzMyN2Yw");
  46. _audioPlayer = AudioPlayer();
  47. _requestPermissions();
  48. _initRecorder();
  49. }
  50. // 初始化录音器
  51. void _initRecorder() async {
  52. try {
  53. _soundRecorder = FlutterSoundRecorder();
  54. await _soundRecorder?.openRecorder();
  55. await _soundRecorder?.setSubscriptionDuration(const Duration(milliseconds: 100));
  56. //检查编解码器是否支持
  57. if (!await _soundRecorder!.isEncoderSupported(Codec.pcm16)) {
  58. _log("PCM16 codec is not supported on this device.");
  59. _audiocodec = Codec.aacADTS;
  60. }
  61. setState(() {
  62. _isRecorderReady = true;
  63. });
  64. _log('录音器初始化成功');
  65. } catch (e) {
  66. _log('初始化录音器失败: $e');
  67. setState(() {
  68. _isRecorderReady = false;
  69. });
  70. }
  71. }
  72. // 请求麦克风权限
  73. void _requestPermissions() async {
  74. try {
  75. if (await Permission.microphone.request().isGranted) {
  76. _log('麦克风权限已授予');
  77. } else {
  78. _log('麦克风权限被拒绝');
  79. setState(() {
  80. _isRecorderReady = false;
  81. });
  82. }
  83. } catch (e) {
  84. _log('请求麦克风权限失败: $e');
  85. setState(() {
  86. _isRecorderReady = false;
  87. });
  88. }
  89. }
  90. // 开始录音
  91. void _startRecording() async {
  92. try {
  93. if (!_isRecorderReady) {
  94. _log('录音器未准备好');
  95. return;
  96. }
  97. if (_isRecording) return; // 防止重复调用
  98. final directory = await getTemporaryDirectory();
  99. final tempPath = '${directory.path}/recorded_audio.pcm';
  100. _log('录音文件路径: $tempPath');
  101. await _soundRecorder?.startRecorder(
  102. codec: _audiocodec,
  103. toStream: _audioDataStreamController.sink, // 将音频数据写入到 StreamController
  104. sampleRate: _sampleRate, // 设置采样率
  105. numChannels: _numChannels, // 设置声道数
  106. enableVoiceProcessing: true, // 启用音量监听
  107. );
  108. _soundRecorder?.onProgress!.listen((RecordingDisposition event) {
  109. // _log('onProgress 回调触发, 分贝: ${event.decibels}');
  110. if (event.decibels != null) {
  111. setState(() {
  112. _volumeLevel = event.decibels!; //更新音量值
  113. });
  114. _checkSpeakingStatus(); // 检查说话状态
  115. }
  116. });
  117. // 监听音频数据流
  118. _audioDataStreamController.stream.listen((Uint8List audioData) {
  119. if (_isSpeaking){
  120. // _log('Received audio data: ${audioData.length} bytes');
  121. _lasttran.addAudioData(List.from(audioData));
  122. }
  123. // 这里可以进一步处理音频数据,例如保存到文件或上传到服务器
  124. });
  125. setState(() {
  126. _audioFilePath = tempPath;
  127. _isRecording = true;
  128. });
  129. _log('录音开始');
  130. } catch (e) {
  131. _log('录音开始 异常: $e');
  132. }
  133. }
  134. // 停止录音
  135. void _stopRecording() async {
  136. try {
  137. if (!_isRecording) return; // 防止重复调用
  138. await _soundRecorder?.stopRecorder();
  139. await _soundRecorder?.closeRecorder();
  140. await _lasttran.close();
  141. setState(() {
  142. _isRecording = false;
  143. _volumeLevel = 0.0; //重置音量值
  144. });
  145. _log('录音停止');
  146. } catch (e) {
  147. _log('录音停止 异常: $e');
  148. }
  149. }
  150. // 播放录音
  151. void _playRecording() async {
  152. // try {
  153. // if (_audioFilePath != null) {
  154. // await _audioPlayer?.play(DeviceFileSource(_audioFilePath!));
  155. // _log('播放录音');
  156. // }
  157. // } catch (e) {
  158. // _log('播放录音 异常: $e');
  159. // }
  160. }
  161. // 检查说话状态
  162. _checkSpeakingStatus() {
  163. if (_volumeLevel > _speakingThreshold && !_isSpeaking) {
  164. // 音量高于阈值,表示开始说话
  165. setState(() {
  166. _isSpeaking = true;
  167. });
  168. _log('开始说话');
  169. _stateSpeak = 1;
  170. _lasttran = _sdk.createTransTask();
  171. } else if (_volumeLevel < _silenceThreshold) {
  172. // 音量低于阈值
  173. if (_lastBelowThresholdTime == null) {
  174. // 记录第一次低于阈值的时间
  175. _lastBelowThresholdTime = DateTime.now();
  176. } else if (DateTime.now().difference(_lastBelowThresholdTime!) >
  177. _silenceDuration) {
  178. // 持续低于阈值超过指定时间,表示结束说话
  179. if (_isSpeaking) {
  180. setState(() {
  181. _isSpeaking = false;
  182. });
  183. _log('结束说话');
  184. _stateSpeak = 3;
  185. _lasttran.close();
  186. }
  187. }
  188. } else {
  189. // 音量恢复到阈值以上,重置计时器
  190. _lastBelowThresholdTime = null;
  191. }
  192. _stateSpeak = 2;
  193. }
  194. // 添加日志信息并自动滚动
  195. void _log(String message) {
  196. print("输出日志:${message}");
  197. setState(() {
  198. _logs.add(message); // 从顶部插入新日志
  199. });
  200. _scrollToBottom();
  201. }
  202. // 滚动到底部
  203. void _scrollToBottom() {
  204. WidgetsBinding.instance.addPostFrameCallback((_) {
  205. if (_scrollController.hasClients) {
  206. _scrollController.animateTo(
  207. _scrollController.position.maxScrollExtent, // 滚动到底部
  208. duration: Duration(milliseconds: 200),
  209. curve: Curves.easeInOut,
  210. );
  211. }
  212. });
  213. }
  214. @override
  215. void dispose() {
  216. _soundRecorder?.closeRecorder();
  217. _audioPlayer?.dispose();
  218. _scrollController.dispose();
  219. super.dispose();
  220. }
  221. @override
  222. Widget build(BuildContext context) {
  223. return Scaffold(
  224. appBar: AppBar(title: Text('录音与播放')),
  225. body: Column(
  226. children: [
  227. // 滑动面板(日志区域)
  228. Expanded(
  229. child: Padding(
  230. padding: const EdgeInsets.all(8.0),
  231. child: Container(
  232. decoration: BoxDecoration(
  233. border: Border.all(color: Colors.blue),
  234. borderRadius: BorderRadius.circular(10),
  235. ),
  236. child: ListView.builder(
  237. controller: _scrollController,
  238. itemCount: _logs.length,
  239. itemBuilder: (context, index) {
  240. return Padding(
  241. padding: const EdgeInsets.symmetric(
  242. vertical: 0.0), // 设置日志项间的垂直间距
  243. child: ListTile(
  244. title: Text(_logs[index]),
  245. ),
  246. );
  247. },
  248. ),
  249. ),
  250. ),
  251. ),
  252. // 底部按钮区域
  253. Padding(
  254. padding: const EdgeInsets.all(20.0),
  255. child:
  256. Row(mainAxisAlignment: MainAxisAlignment.center, children: [
  257. // 音量图标和音量值
  258. Row(
  259. children: [
  260. Icon(
  261. Icons.volume_up,
  262. size: 30,
  263. ),
  264. SizedBox(width: 10), // 间距
  265. Text(
  266. '${_volumeLevel.toStringAsFixed(2)} dB', //显示音量值,保留两位小数
  267. style: TextStyle(fontSize: 16),
  268. ),
  269. ],
  270. ),
  271. SizedBox(width: 20), // 间距
  272. // 按钮区域
  273. GestureDetector(
  274. onTapDown: (details) {
  275. _startRecording(); // 按下时开始录音
  276. },
  277. onTapUp: (details) {
  278. _stopRecording(); // 抬起时停止录音并播放
  279. _playRecording(); // 播放录音
  280. },
  281. child: Container(
  282. margin: EdgeInsets.all(20),
  283. padding: EdgeInsets.all(20),
  284. decoration: BoxDecoration(
  285. color: Colors.blue,
  286. shape: BoxShape.circle,
  287. ),
  288. child: Icon(
  289. Icons.mic,
  290. color: Colors.white,
  291. size: 50,
  292. ),
  293. ),
  294. ),
  295. ]))
  296. ],
  297. ),
  298. );
  299. }
  300. // 根据音量值动态调整图标颜色
  301. Color _getVolumeColor() {
  302. if (_volumeLevel < -40) {
  303. return Colors.green; // 低音量
  304. } else if (_volumeLevel < -20) {
  305. return Colors.yellow; // 中音量
  306. } else {
  307. return Colors.red; // 高音量
  308. }
  309. }
  310. }