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

449 行
14 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:just_audio/just_audio.dart' as just_audio;
  7. import 'package:flutter_sound/flutter_sound.dart' as flutter_sound;
  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. flutter_sound.FlutterSoundRecorder? _soundRecorder;
  16. just_audio.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. flutter_sound.Codec _audiocodec = flutter_sound.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 = just_audio.AudioPlayer();
  46. _requestPermissions();
  47. _initRecorder();
  48. }
  49. // 初始化录音器
  50. void _initRecorder() async {
  51. try {
  52. _soundRecorder = flutter_sound.FlutterSoundRecorder();
  53. await _soundRecorder?.openRecorder();
  54. await _soundRecorder
  55. ?.setSubscriptionDuration(const Duration(milliseconds: 100));
  56. //检查编解码器是否支持
  57. if (!await _soundRecorder!
  58. .isEncoderSupported(flutter_sound.Codec.pcm16)) {
  59. _log("PCM16 codec is not supported on this device.");
  60. _audiocodec = flutter_sound.Codec.aacADTS;
  61. }
  62. setState(() {
  63. _isRecorderReady = true;
  64. });
  65. _log('录音器初始化成功');
  66. } catch (e) {
  67. _log('初始化录音器失败: $e');
  68. setState(() {
  69. _isRecorderReady = false;
  70. });
  71. }
  72. }
  73. // 请求麦克风权限
  74. void _requestPermissions() async {
  75. try {
  76. if (await Permission.microphone.request().isGranted) {
  77. _log('麦克风权限已授予');
  78. } else {
  79. _log('麦克风权限被拒绝');
  80. setState(() {
  81. _isRecorderReady = false;
  82. });
  83. }
  84. } catch (e) {
  85. _log('请求麦克风权限失败: $e');
  86. setState(() {
  87. _isRecorderReady = false;
  88. });
  89. }
  90. }
  91. // 开始录音
  92. void _startRecording() async {
  93. try {
  94. if (!_isRecorderReady) {
  95. _log('录音器未准备好');
  96. return;
  97. }
  98. if (_isRecording) return; // 防止重复调用
  99. final directory = await getTemporaryDirectory();
  100. final tempPath = '${directory.path}/recorded_audio.pcm';
  101. _log('录音文件路径: $tempPath');
  102. await _soundRecorder?.startRecorder(
  103. codec: _audiocodec,
  104. toStream: _audioDataStreamController.sink, // 将音频数据写入到 StreamController
  105. sampleRate: _sampleRate, // 设置采样率
  106. numChannels: _numChannels, // 设置声道数
  107. enableVoiceProcessing: true, // 启用音量监听
  108. );
  109. _soundRecorder?.onProgress!
  110. .listen((flutter_sound.RecordingDisposition event) {
  111. // _log('onProgress 回调触发, 分贝: ${event.decibels}');
  112. if (event.decibels != null) {
  113. setState(() {
  114. _volumeLevel = event.decibels!; //更新音量值
  115. });
  116. _checkSpeakingStatus(); // 检查说话状态
  117. }
  118. });
  119. // 监听音频数据流
  120. _audioDataStreamController.stream.listen((Uint8List audioData) {
  121. _processAudioData(audioData);
  122. // 这里可以进一步处理音频数据,例如保存到文件或上传到服务器
  123. });
  124. setState(() {
  125. _audioFilePath = tempPath;
  126. _isRecording = true;
  127. });
  128. _log('录音开始');
  129. } catch (e) {
  130. _log('录音开始 异常: $e');
  131. }
  132. }
  133. // 处理音频数据的方法
  134. Uint8List _audioBuffer = Uint8List(0); // 缓存音频数据
  135. void _processAudioData(Uint8List newData) {
  136. // 将新数据追加到缓存中
  137. _audioBuffer = Uint8List.fromList([..._audioBuffer, ...newData]);
  138. // 每次处理一帧数据(1280 字节)
  139. int frameSize = 1280; // 每帧的大小
  140. while (_isSpeaking && _audioBuffer.length >= frameSize) {
  141. // 取出一帧数据
  142. Uint8List frame = _audioBuffer.sublist(0, frameSize);
  143. _audioBuffer = _audioBuffer.sublist(frameSize); // 移除已处理的数据
  144. // 将帧数据传递给任务
  145. _lasttran.addAudioData(frame);
  146. }
  147. // 如果录音结束且缓存中还有剩余数据,则作为最后一帧发送
  148. if (!_isRecording && _audioBuffer.isNotEmpty) {
  149. _lasttran.addAudioData(_audioBuffer);
  150. _audioBuffer = Uint8List(0); // 清空缓存
  151. }
  152. }
  153. // 停止录音
  154. void _stopRecording() async {
  155. try {
  156. if (!_isRecording) return; // 防止重复调用
  157. _lasttran.endpuish();
  158. await _soundRecorder?.stopRecorder();
  159. await _soundRecorder?.closeRecorder();
  160. setState(() {
  161. _isRecording = false;
  162. _volumeLevel = 0.0; //重置音量值
  163. });
  164. _log('录音停止');
  165. } catch (e) {
  166. _log('录音停止 异常: $e');
  167. }
  168. }
  169. // 播放录音
  170. void _playRecording() async {
  171. // try {
  172. // if (_audioFilePath != null) {
  173. // await _audioPlayer?.play(DeviceFileSource(_audioFilePath!));
  174. // _log('播放录音');
  175. // }
  176. // } catch (e) {
  177. // _log('播放录音 异常: $e');
  178. // }
  179. }
  180. // 检查说话状态
  181. _checkSpeakingStatus() {
  182. if (_volumeLevel > _speakingThreshold && !_isSpeaking) {
  183. // 音量高于阈值,表示开始说话
  184. setState(() {
  185. _isSpeaking = true;
  186. });
  187. _log('开始说话');
  188. _stateSpeak = 1;
  189. _lasttran = _sdk.createTransTask(_taskchange);
  190. _trans.add(_lasttran);
  191. } else if (_volumeLevel < _silenceThreshold) {
  192. // 音量低于阈值
  193. if (_lastBelowThresholdTime == null) {
  194. // 记录第一次低于阈值的时间
  195. _lastBelowThresholdTime = DateTime.now();
  196. } else if (DateTime.now().difference(_lastBelowThresholdTime!) >
  197. _silenceDuration) {
  198. // 持续低于阈值超过指定时间,表示结束说话
  199. if (_isSpeaking) {
  200. setState(() {
  201. _isSpeaking = false;
  202. });
  203. _log('结束说话');
  204. _stateSpeak = 3;
  205. _lasttran.endpuish();
  206. }
  207. }
  208. } else {
  209. // 音量恢复到阈值以上,重置计时器
  210. _lastBelowThresholdTime = null;
  211. }
  212. _stateSpeak = 2;
  213. }
  214. //任务状态变化
  215. void _taskchange(ITaskTrans task) {
  216. if (task.state() == 3) {
  217. playAudioStream(task.translateAudio());
  218. }
  219. }
  220. // 添加日志信息并自动滚动
  221. void _log(String message) {
  222. print("输出日志:${message}");
  223. setState(() {
  224. _logs.add(message); // 从顶部插入新日志
  225. });
  226. _scrollToBottom();
  227. }
  228. // 滚动到底部
  229. void _scrollToBottom() {
  230. WidgetsBinding.instance.addPostFrameCallback((_) {
  231. if (_scrollController.hasClients) {
  232. _scrollController.animateTo(
  233. _scrollController.position.maxScrollExtent, // 滚动到底部
  234. duration: Duration(milliseconds: 200),
  235. curve: Curves.easeInOut,
  236. );
  237. }
  238. });
  239. }
  240. // 播放音频流
  241. void playAudioStream(Stream<Uint8List> audioStream) async {
  242. try {
  243. // 创建自定义的 AudioSource
  244. final audioSource = CustomStreamAudioSource(
  245. audioStream: audioStream,
  246. contentLength: null, // 如果不知道音频数据长度,设置为 null
  247. );
  248. // 设置音频源并播放
  249. await _audioPlayer?.setAudioSource(audioSource);
  250. await _audioPlayer?.play();
  251. print('音频流播放开始');
  252. } catch (e) {
  253. print('播放音频流失败: $e');
  254. }
  255. }
  256. @override
  257. void dispose() {
  258. _soundRecorder?.closeRecorder();
  259. _audioPlayer?.dispose();
  260. _scrollController.dispose();
  261. super.dispose();
  262. }
  263. @override
  264. Widget build(BuildContext context) {
  265. return Scaffold(
  266. appBar: AppBar(title: Text('录音与播放')),
  267. body: Column(
  268. children: [
  269. // 滑动面板(日志区域)
  270. Expanded(
  271. child: Padding(
  272. padding: const EdgeInsets.all(8.0),
  273. child: Container(
  274. decoration: BoxDecoration(
  275. border: Border.all(color: Colors.blue),
  276. borderRadius: BorderRadius.circular(10),
  277. ),
  278. child: ListView.builder(
  279. controller: _scrollController,
  280. itemCount: _trans.length,
  281. itemBuilder: (context, index) {
  282. // 语音消息
  283. return _buildAudioMessage(_trans[index]);
  284. },
  285. ),
  286. ),
  287. ),
  288. ),
  289. // 底部按钮区域
  290. Padding(
  291. padding: const EdgeInsets.all(20.0),
  292. child:
  293. Row(mainAxisAlignment: MainAxisAlignment.center, children: [
  294. // 音量图标和音量值
  295. Row(
  296. children: [
  297. Icon(
  298. Icons.volume_up,
  299. size: 30,
  300. ),
  301. SizedBox(width: 10), // 间距
  302. Text(
  303. '${_volumeLevel.toStringAsFixed(2)} dB', //显示音量值,保留两位小数
  304. style: TextStyle(fontSize: 16),
  305. ),
  306. ],
  307. ),
  308. SizedBox(width: 20), // 间距
  309. // 按钮区域
  310. GestureDetector(
  311. onTapDown: (details) {
  312. _startRecording(); // 按下时开始录音
  313. },
  314. onTapUp: (details) {
  315. _stopRecording(); // 抬起时停止录音并播放
  316. _playRecording(); // 播放录音
  317. },
  318. child: Container(
  319. margin: EdgeInsets.all(20),
  320. padding: EdgeInsets.all(20),
  321. decoration: BoxDecoration(
  322. color: Colors.blue,
  323. shape: BoxShape.circle,
  324. ),
  325. child: Icon(
  326. Icons.mic,
  327. color: Colors.white,
  328. size: 50,
  329. ),
  330. ),
  331. ),
  332. ]))
  333. ],
  334. ),
  335. );
  336. }
  337. // 构建语音消息
  338. Widget _buildAudioMessage(ITaskTrans msg) {
  339. return Padding(
  340. padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
  341. child: Column(
  342. crossAxisAlignment: CrossAxisAlignment.start,
  343. children: [
  344. // 音频时长显示
  345. Text(
  346. '2秒',
  347. style: TextStyle(fontSize: 14, color: Colors.grey),
  348. ),
  349. SizedBox(height: 5),
  350. // 音频播放按钮
  351. GestureDetector(
  352. onTap: () {
  353. // 这里可以实现点击播放音频的功能
  354. // print("播放音频: ${message['audioUrl']}");
  355. },
  356. child: Container(
  357. padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
  358. decoration: BoxDecoration(
  359. color: Colors.green,
  360. borderRadius: BorderRadius.circular(30),
  361. ),
  362. child: Row(
  363. children: [
  364. Icon(
  365. Icons.play_arrow,
  366. color: Colors.white,
  367. ),
  368. SizedBox(width: 10),
  369. Text(
  370. '播放音频',
  371. style: TextStyle(color: Colors.white),
  372. ),
  373. ],
  374. ),
  375. ),
  376. ),
  377. SizedBox(height: 5),
  378. // 文字内容
  379. Text(
  380. msg.originalText(),
  381. style: TextStyle(fontSize: 16),
  382. ),
  383. ],
  384. ),
  385. );
  386. }
  387. }
  388. class CustomStreamAudioSource extends just_audio.StreamAudioSource {
  389. final Stream<Uint8List> audioStream;
  390. final int? contentLength;
  391. CustomStreamAudioSource({
  392. required this.audioStream,
  393. this.contentLength,
  394. });
  395. @override
  396. Future<just_audio.StreamAudioResponse> request([int? start, int? end]) async {
  397. try {
  398. // 将 Stream<Uint8List> 转换为 Stream<List<int>>
  399. final stream = audioStream.map((uint8List) => uint8List.toList());
  400. // 处理范围请求
  401. if (start != null || end != null) {
  402. // 这里假设音频流支持范围请求
  403. // 如果音频流不支持范围请求,可以忽略 start 和 end 参数
  404. // 或者抛出一个异常
  405. throw UnsupportedError('Range requests are not supported');
  406. }
  407. // 返回 StreamAudioResponse
  408. return just_audio.StreamAudioResponse(
  409. stream: stream,
  410. contentLength: contentLength,
  411. sourceLength: null,
  412. offset: null,
  413. contentType: 'audio/mpeg',
  414. );
  415. } catch (e) {
  416. print('请求音频流失败: $e');
  417. rethrow;
  418. }
  419. }
  420. }