import 'dart:async'; import 'dart:typed_data'; import 'package:demo001/xunfei/xunfei.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_sound/flutter_sound.dart'; import 'package:just_audio/just_audio.dart'; import 'package:permission_handler/permission_handler.dart'; class SoundRecordScene extends StatefulWidget { @override _SoundRecordSceneState createState() => _SoundRecordSceneState(); } class _SoundRecordSceneState extends State { late ISDK _sdk; FlutterSoundRecorder? _soundRecorder; AudioPlayer? _audioPlayer; bool _isRecorderReady = false; bool _isRecording = false; bool _isSpeaking = false; //是否说话 int _stateSpeak = 0; // 说话状态 0 未说话 1开始说话 2 说话中 3结束说话 String? _audioFilePath; double _volumeLevel = 0.0; // 当前音量值 DateTime? _lastBelowThresholdTime; // 上次音量低于阈值的时间 ScrollController _scrollController = ScrollController(); List _logs = []; List _trans = []; late ITaskTrans _lasttran; // 音量阈值 final double _speakingThreshold = 50.0; // 开始说话的阈值 final double _silenceThreshold = 30.0; // 结束说话的阈值 final Duration _silenceDuration = Duration(seconds: 1); // 持续低于阈值的时间 // 采样率和声道数 Codec _audiocodec = Codec.pcm16; final int _sampleRate = 16000; // 16kHz 采样率 final int _numChannels = 1; // 单声道 StreamController _audioDataStreamController = StreamController.broadcast(); @override void initState() { super.initState(); _sdk = Xunfei( appId: "137dc132", apiKey: "1c1891a475e71250ecd1320303ad6545", apiSecret: "MjZhNDA1NTI1NWZkZDQxOTMxYzMyN2Yw"); _audioPlayer = AudioPlayer(); _requestPermissions(); _initRecorder(); } // 初始化录音器 void _initRecorder() async { try { _soundRecorder = FlutterSoundRecorder(); await _soundRecorder?.openRecorder(); await _soundRecorder ?.setSubscriptionDuration(const Duration(milliseconds: 100)); //检查编解码器是否支持 if (!await _soundRecorder!.isEncoderSupported(Codec.pcm16)) { _log("PCM16 codec is not supported on this device."); _audiocodec = Codec.aacADTS; } setState(() { _isRecorderReady = true; }); _log('录音器初始化成功'); } catch (e) { _log('初始化录音器失败: $e'); setState(() { _isRecorderReady = false; }); } } // 请求麦克风权限 void _requestPermissions() async { try { if (await Permission.microphone.request().isGranted) { _log('麦克风权限已授予'); } else { _log('麦克风权限被拒绝'); setState(() { _isRecorderReady = false; }); } } catch (e) { _log('请求麦克风权限失败: $e'); setState(() { _isRecorderReady = false; }); } } // 开始录音 void _startRecording() async { try { if (!_isRecorderReady) { _log('录音器未准备好'); return; } if (_isRecording) return; // 防止重复调用 final directory = await getTemporaryDirectory(); final tempPath = '${directory.path}/recorded_audio.pcm'; _log('录音文件路径: $tempPath'); await _soundRecorder?.startRecorder( codec: _audiocodec, toStream: _audioDataStreamController.sink, // 将音频数据写入到 StreamController sampleRate: _sampleRate, // 设置采样率 numChannels: _numChannels, // 设置声道数 enableVoiceProcessing: true, // 启用音量监听 ); _soundRecorder?.onProgress!.listen((RecordingDisposition event) { // _log('onProgress 回调触发, 分贝: ${event.decibels}'); if (event.decibels != null) { setState(() { _volumeLevel = event.decibels!; //更新音量值 }); _checkSpeakingStatus(); // 检查说话状态 } }); // 监听音频数据流 _audioDataStreamController.stream.listen((Uint8List audioData) { _processAudioData(audioData); // 这里可以进一步处理音频数据,例如保存到文件或上传到服务器 }); setState(() { _audioFilePath = tempPath; _isRecording = true; }); _log('录音开始'); } catch (e) { _log('录音开始 异常: $e'); } } // 处理音频数据的方法 Uint8List _audioBuffer = Uint8List(0); // 缓存音频数据 void _processAudioData(Uint8List newData) { // 将新数据追加到缓存中 _audioBuffer = Uint8List.fromList([..._audioBuffer, ...newData]); // 每次处理一帧数据(1280 字节) int frameSize = 1280; // 每帧的大小 while (_isSpeaking && _audioBuffer.length >= frameSize) { // 取出一帧数据 Uint8List frame = _audioBuffer.sublist(0, frameSize); _audioBuffer = _audioBuffer.sublist(frameSize); // 移除已处理的数据 // 将帧数据传递给任务 _lasttran.addAudioData(frame); } // 如果录音结束且缓存中还有剩余数据,则作为最后一帧发送 if (!_isRecording && _audioBuffer.isNotEmpty) { _lasttran.addAudioData(_audioBuffer); _audioBuffer = Uint8List(0); // 清空缓存 } } // 停止录音 void _stopRecording() async { try { if (!_isRecording) return; // 防止重复调用 _lasttran.endpuish(); await _soundRecorder?.stopRecorder(); await _soundRecorder?.closeRecorder(); setState(() { _isRecording = false; _volumeLevel = 0.0; //重置音量值 }); _log('录音停止'); } catch (e) { _log('录音停止 异常: $e'); } } // 播放录音 void _playRecording() async { // try { // if (_audioFilePath != null) { // await _audioPlayer?.play(DeviceFileSource(_audioFilePath!)); // _log('播放录音'); // } // } catch (e) { // _log('播放录音 异常: $e'); // } } // 检查说话状态 _checkSpeakingStatus() { if (_volumeLevel > _speakingThreshold && !_isSpeaking) { // 音量高于阈值,表示开始说话 setState(() { _isSpeaking = true; }); _log('开始说话'); _stateSpeak = 1; _lasttran = _sdk.createTransTask(_taskchange); _trans.add(_lasttran); } else if (_volumeLevel < _silenceThreshold) { // 音量低于阈值 if (_lastBelowThresholdTime == null) { // 记录第一次低于阈值的时间 _lastBelowThresholdTime = DateTime.now(); } else if (DateTime.now().difference(_lastBelowThresholdTime!) > _silenceDuration) { // 持续低于阈值超过指定时间,表示结束说话 if (_isSpeaking) { setState(() { _isSpeaking = false; }); _log('结束说话'); _stateSpeak = 3; _lasttran.endpuish(); } } } else { // 音量恢复到阈值以上,重置计时器 _lastBelowThresholdTime = null; } _stateSpeak = 2; } //任务状态变化 void _taskchange(ITaskTrans task) { if (task.state() == 2) {} } // 添加日志信息并自动滚动 void _log(String message) { print("输出日志:${message}"); setState(() { _logs.add(message); // 从顶部插入新日志 }); _scrollToBottom(); } // 滚动到底部 void _scrollToBottom() { WidgetsBinding.instance.addPostFrameCallback((_) { if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, // 滚动到底部 duration: Duration(milliseconds: 200), curve: Curves.easeInOut, ); } }); } // 播放音频流 void playAudioStream(Stream audioStream) async { try { await for (var chunk in audioStream) { // 每次接收到音频数据块后,播放它 await _audioPlayer?.setUrl(Uri.dataFromBytes(chunk).toString()); await _audioPlayer?.play(); } } catch (e) { print("音频流播放错误: $e"); } } @override void dispose() { _soundRecorder?.closeRecorder(); _audioPlayer?.dispose(); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('录音与播放')), body: Column( children: [ // 滑动面板(日志区域) Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.blue), borderRadius: BorderRadius.circular(10), ), child: ListView.builder( controller: _scrollController, itemCount: _trans.length, itemBuilder: (context, index) { // 语音消息 return _buildAudioMessage(_trans[index]); }, ), ), ), ), // 底部按钮区域 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, ), ), ), ])) ], ), ); } // 构建语音消息 Widget _buildAudioMessage(ITaskTrans msg) { return Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 音频时长显示 Text( '2秒', style: TextStyle(fontSize: 14, color: Colors.grey), ), SizedBox(height: 5), // 音频播放按钮 GestureDetector( onTap: () { // 这里可以实现点击播放音频的功能 // print("播放音频: ${message['audioUrl']}"); }, child: Container( padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(30), ), child: Row( children: [ Icon( Icons.play_arrow, color: Colors.white, ), SizedBox(width: 10), Text( '播放音频', style: TextStyle(color: Colors.white), ), ], ), ), ), SizedBox(height: 5), // 文字内容 Text( msg.originalText(), style: TextStyle(fontSize: 16), ), ], ), ); } }