You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

402 lines
12 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:just_audio/just_audio.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 =
  37. StreamController<Uint8List>.broadcast();
  38. @override
  39. void initState() {
  40. super.initState();
  41. _sdk = Xunfei(
  42. appId: "137dc132",
  43. apiKey: "1c1891a475e71250ecd1320303ad6545",
  44. apiSecret: "MjZhNDA1NTI1NWZkZDQxOTMxYzMyN2Yw");
  45. _audioPlayer = AudioPlayer();
  46. _requestPermissions();
  47. _initRecorder();
  48. }
  49. // 初始化录音器
  50. void _initRecorder() async {
  51. try {
  52. _soundRecorder = FlutterSoundRecorder();
  53. await _soundRecorder?.openRecorder();
  54. await _soundRecorder
  55. ?.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. _processAudioData(audioData);
  120. // 这里可以进一步处理音频数据,例如保存到文件或上传到服务器
  121. });
  122. setState(() {
  123. _audioFilePath = tempPath;
  124. _isRecording = true;
  125. });
  126. _log('录音开始');
  127. } catch (e) {
  128. _log('录音开始 异常: $e');
  129. }
  130. }
  131. // 处理音频数据的方法
  132. Uint8List _audioBuffer = Uint8List(0); // 缓存音频数据
  133. void _processAudioData(Uint8List newData) {
  134. // 将新数据追加到缓存中
  135. _audioBuffer = Uint8List.fromList([..._audioBuffer, ...newData]);
  136. // 每次处理一帧数据(1280 字节)
  137. int frameSize = 1280; // 每帧的大小
  138. while (_isSpeaking && _audioBuffer.length >= frameSize) {
  139. // 取出一帧数据
  140. Uint8List frame = _audioBuffer.sublist(0, frameSize);
  141. _audioBuffer = _audioBuffer.sublist(frameSize); // 移除已处理的数据
  142. // 将帧数据传递给任务
  143. _lasttran.addAudioData(frame);
  144. }
  145. // 如果录音结束且缓存中还有剩余数据,则作为最后一帧发送
  146. if (!_isRecording && _audioBuffer.isNotEmpty) {
  147. _lasttran.addAudioData(_audioBuffer);
  148. _audioBuffer = Uint8List(0); // 清空缓存
  149. }
  150. }
  151. // 停止录音
  152. void _stopRecording() async {
  153. try {
  154. if (!_isRecording) return; // 防止重复调用
  155. _lasttran.endpuish();
  156. await _soundRecorder?.stopRecorder();
  157. await _soundRecorder?.closeRecorder();
  158. setState(() {
  159. _isRecording = false;
  160. _volumeLevel = 0.0; //重置音量值
  161. });
  162. _log('录音停止');
  163. } catch (e) {
  164. _log('录音停止 异常: $e');
  165. }
  166. }
  167. // 播放录音
  168. void _playRecording() async {
  169. // try {
  170. // if (_audioFilePath != null) {
  171. // await _audioPlayer?.play(DeviceFileSource(_audioFilePath!));
  172. // _log('播放录音');
  173. // }
  174. // } catch (e) {
  175. // _log('播放录音 异常: $e');
  176. // }
  177. }
  178. // 检查说话状态
  179. _checkSpeakingStatus() {
  180. if (_volumeLevel > _speakingThreshold && !_isSpeaking) {
  181. // 音量高于阈值,表示开始说话
  182. setState(() {
  183. _isSpeaking = true;
  184. });
  185. _log('开始说话');
  186. _stateSpeak = 1;
  187. _lasttran = _sdk.createTransTask(_taskchange);
  188. _trans.add(_lasttran);
  189. } else if (_volumeLevel < _silenceThreshold) {
  190. // 音量低于阈值
  191. if (_lastBelowThresholdTime == null) {
  192. // 记录第一次低于阈值的时间
  193. _lastBelowThresholdTime = DateTime.now();
  194. } else if (DateTime.now().difference(_lastBelowThresholdTime!) >
  195. _silenceDuration) {
  196. // 持续低于阈值超过指定时间,表示结束说话
  197. if (_isSpeaking) {
  198. setState(() {
  199. _isSpeaking = false;
  200. });
  201. _log('结束说话');
  202. _stateSpeak = 3;
  203. _lasttran.endpuish();
  204. }
  205. }
  206. } else {
  207. // 音量恢复到阈值以上,重置计时器
  208. _lastBelowThresholdTime = null;
  209. }
  210. _stateSpeak = 2;
  211. }
  212. //任务状态变化
  213. void _taskchange(ITaskTrans task) {
  214. if (task.state() == 2) {}
  215. }
  216. // 添加日志信息并自动滚动
  217. void _log(String message) {
  218. print("输出日志:${message}");
  219. setState(() {
  220. _logs.add(message); // 从顶部插入新日志
  221. });
  222. _scrollToBottom();
  223. }
  224. // 滚动到底部
  225. void _scrollToBottom() {
  226. WidgetsBinding.instance.addPostFrameCallback((_) {
  227. if (_scrollController.hasClients) {
  228. _scrollController.animateTo(
  229. _scrollController.position.maxScrollExtent, // 滚动到底部
  230. duration: Duration(milliseconds: 200),
  231. curve: Curves.easeInOut,
  232. );
  233. }
  234. });
  235. }
  236. // 播放音频流
  237. void playAudioStream(Stream<Uint8List> audioStream) async {
  238. try {
  239. await for (var chunk in audioStream) {
  240. // 每次接收到音频数据块后,播放它
  241. await _audioPlayer?.setUrl(Uri.dataFromBytes(chunk).toString());
  242. await _audioPlayer?.play();
  243. }
  244. } catch (e) {
  245. print("音频流播放错误: $e");
  246. }
  247. }
  248. @override
  249. void dispose() {
  250. _soundRecorder?.closeRecorder();
  251. _audioPlayer?.dispose();
  252. _scrollController.dispose();
  253. super.dispose();
  254. }
  255. @override
  256. Widget build(BuildContext context) {
  257. return Scaffold(
  258. appBar: AppBar(title: Text('录音与播放')),
  259. body: Column(
  260. children: [
  261. // 滑动面板(日志区域)
  262. Expanded(
  263. child: Padding(
  264. padding: const EdgeInsets.all(8.0),
  265. child: Container(
  266. decoration: BoxDecoration(
  267. border: Border.all(color: Colors.blue),
  268. borderRadius: BorderRadius.circular(10),
  269. ),
  270. child: ListView.builder(
  271. controller: _scrollController,
  272. itemCount: _trans.length,
  273. itemBuilder: (context, index) {
  274. // 语音消息
  275. return _buildAudioMessage(_trans[index]);
  276. },
  277. ),
  278. ),
  279. ),
  280. ),
  281. // 底部按钮区域
  282. Padding(
  283. padding: const EdgeInsets.all(20.0),
  284. child:
  285. Row(mainAxisAlignment: MainAxisAlignment.center, children: [
  286. // 音量图标和音量值
  287. Row(
  288. children: [
  289. Icon(
  290. Icons.volume_up,
  291. size: 30,
  292. ),
  293. SizedBox(width: 10), // 间距
  294. Text(
  295. '${_volumeLevel.toStringAsFixed(2)} dB', //显示音量值,保留两位小数
  296. style: TextStyle(fontSize: 16),
  297. ),
  298. ],
  299. ),
  300. SizedBox(width: 20), // 间距
  301. // 按钮区域
  302. GestureDetector(
  303. onTapDown: (details) {
  304. _startRecording(); // 按下时开始录音
  305. },
  306. onTapUp: (details) {
  307. _stopRecording(); // 抬起时停止录音并播放
  308. _playRecording(); // 播放录音
  309. },
  310. child: Container(
  311. margin: EdgeInsets.all(20),
  312. padding: EdgeInsets.all(20),
  313. decoration: BoxDecoration(
  314. color: Colors.blue,
  315. shape: BoxShape.circle,
  316. ),
  317. child: Icon(
  318. Icons.mic,
  319. color: Colors.white,
  320. size: 50,
  321. ),
  322. ),
  323. ),
  324. ]))
  325. ],
  326. ),
  327. );
  328. }
  329. // 构建语音消息
  330. Widget _buildAudioMessage(ITaskTrans msg) {
  331. return Padding(
  332. padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
  333. child: Column(
  334. crossAxisAlignment: CrossAxisAlignment.start,
  335. children: [
  336. // 音频时长显示
  337. Text(
  338. '2秒',
  339. style: TextStyle(fontSize: 14, color: Colors.grey),
  340. ),
  341. SizedBox(height: 5),
  342. // 音频播放按钮
  343. GestureDetector(
  344. onTap: () {
  345. // 这里可以实现点击播放音频的功能
  346. // print("播放音频: ${message['audioUrl']}");
  347. },
  348. child: Container(
  349. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
  350. decoration: BoxDecoration(
  351. color: Colors.green,
  352. borderRadius: BorderRadius.circular(30),
  353. ),
  354. child: Row(
  355. children: [
  356. Icon(
  357. Icons.play_arrow,
  358. color: Colors.white,
  359. ),
  360. SizedBox(width: 10),
  361. Text(
  362. '播放音频',
  363. style: TextStyle(color: Colors.white),
  364. ),
  365. ],
  366. ),
  367. ),
  368. ),
  369. SizedBox(height: 5),
  370. // 文字内容
  371. Text(
  372. msg.originalText(),
  373. style: TextStyle(fontSize: 16),
  374. ),
  375. ],
  376. ),
  377. );
  378. }
  379. }