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.
 
 
 
 
 
 

337 lines
10 KiB

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:math';
  4. import 'dart:typed_data';
  5. import 'package:demo001/tools/audio_tool.dart';
  6. import 'package:demo001/xunfei/recognition_result/recognition_result.dart';
  7. import 'package:demo001/xunfei/streamtrans_result/streamtrans_result.dart';
  8. import 'package:intl/intl.dart';
  9. import 'package:demo001/xunfei/utils.dart';
  10. import 'package:web_socket_channel/web_socket_channel.dart';
  11. //讯飞翻译
  12. class XunFeiTranslate {
  13. final int _chunkSize = 1280; // 每次发送的数据大小
  14. // 音量阈值
  15. final double _speakingThreshold = 1000.0; // 开始说话的阈值
  16. final double _silenceThreshold = 500.0; // 结束说话的阈值
  17. final Duration _silenceDuration = Duration(seconds: 1); // 持续低于阈值的时间
  18. DateTime? _lastBelowThresholdTime; // 上次音量低于阈值的时间
  19. double _volume = 0; //当前原因
  20. Uint8List _buff = Uint8List(0); //音频缓存区
  21. bool isRecording = false; //是否录音
  22. bool isspeaking = false; //是否说话
  23. Timer? _timer;
  24. XunFeiTranslateTask? currtask;
  25. late Function(RecognitionResult) onRecognitionResult;
  26. late Function(StreamtransResult) onStreamtransResult;
  27. late Function(AudioModel) onAudioResult;
  28. final String appId;
  29. final String apiKey;
  30. final String apiSecret;
  31. final String host = "ws-api.xf-yun.com";
  32. final String requestUri = "/v1/private/simult_interpretation";
  33. //静态变量保存唯一实例
  34. static XunFeiTranslate? _instance;
  35. XunFeiTranslate._internal(
  36. {required this.appId,
  37. required this.apiKey,
  38. required this.apiSecret,
  39. required this.onRecognitionResult,
  40. required this.onStreamtransResult,
  41. required this.onAudioResult});
  42. //工厂构造函数
  43. factory XunFeiTranslate({
  44. required String appId,
  45. required String apiKey,
  46. required String apiSecret,
  47. required Function(RecognitionResult) onRecognitionResult,
  48. required Function(StreamtransResult) onStreamtransResult,
  49. required Function(AudioModel) onAudioResult,
  50. }) {
  51. _instance ??= XunFeiTranslate._internal(
  52. appId: appId,
  53. apiKey: apiKey,
  54. apiSecret: apiSecret,
  55. onRecognitionResult: onRecognitionResult,
  56. onStreamtransResult: onStreamtransResult,
  57. onAudioResult: onAudioResult,
  58. );
  59. return _instance!;
  60. }
  61. //获取链接地址
  62. String _geturl() {
  63. final now = DateTime.now();
  64. final date =
  65. DateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'").format(now.toUtc());
  66. final signatureOrigin =
  67. "host: $host\ndate: $date\nGET $requestUri HTTP/1.1";
  68. // 使用 HmacUtil 计算 HMAC-SHA256 签名
  69. final signature = XunfeiUtils.hmacSha256(apiSecret, signatureOrigin);
  70. final authorization = base64.encode(utf8.encode(
  71. "api_key=\"$apiKey\", algorithm=\"hmac-sha256\", headers=\"host date request-line\", signature=\"$signature\""));
  72. final queryParams = {
  73. "authorization": authorization,
  74. "date": date,
  75. "host": host,
  76. "serviceId": "simult_interpretation"
  77. };
  78. final wsUri =
  79. 'ws://$host$requestUri?${Uri(queryParameters: queryParams).query}';
  80. return wsUri;
  81. }
  82. //创建参数
  83. Map<String, dynamic> _createParams(int status, Uint8List audio) {
  84. final param = {
  85. "header": {
  86. "app_id": appId,
  87. "status": status,
  88. },
  89. "parameter": {
  90. "ist": {
  91. "accent": "mandarin",
  92. "domain": "ist_ed_open",
  93. "language": "zh_cn",
  94. "vto": 15000,
  95. "eos": 150000
  96. },
  97. "streamtrans": {"from": "cn", "to": "en"},
  98. "tts": {
  99. "vcn": "x2_catherine",
  100. "tts_results": {
  101. "encoding": "lame",
  102. "sample_rate": 16000,
  103. "channels": 1,
  104. "bit_depth": 16,
  105. "frame_size": 0
  106. }
  107. }
  108. },
  109. "payload": {
  110. "data": {
  111. "audio": base64.encode(audio),
  112. "encoding": "raw",
  113. "sample_rate": 16000,
  114. "seq": 1,
  115. "status": status
  116. }
  117. }
  118. };
  119. return param;
  120. }
  121. //开始同时翻译
  122. Future<void> starttranslate(Stream<Uint8List> stream) async {
  123. isRecording = true;
  124. stream.listen((data) {
  125. if (isRecording) {
  126. _buff = _appendToBuffer(data);
  127. }
  128. });
  129. _timer = Timer.periodic(Duration(milliseconds: 40), (timer) async {
  130. //每40毫秒读取一次数据
  131. var frame = _getAudioData();
  132. _volume = _calculateAmplitude(frame);
  133. var state = _checkSpeakingStatus();
  134. // print("当前状态: volume:$_volume state:$state ---------------------------");
  135. if (state == 1) {
  136. //开始说话
  137. currtask = XunFeiTranslateTask(_geturl(), _handleData);
  138. currtask?.sendaudio(_createParams(0, frame));
  139. print("发送第一帧---------------------------");
  140. } else if (state == 2) {
  141. //结束说话
  142. currtask?.sendaudio(_createParams(1, frame));
  143. } else if (state == 3) {
  144. //结束说话
  145. currtask?.sendaudio(_createParams(2, frame));
  146. currtask = null;
  147. print("发送最后一帧---------------------------");
  148. }
  149. });
  150. return;
  151. }
  152. //结束翻译
  153. Future<void> stoptranslate() async {
  154. isRecording = false;
  155. _timer?.cancel();
  156. _timer = null;
  157. if (currtask != null) {
  158. var _frame = _getAudioData();
  159. currtask?.sendaudio(_createParams(2, _frame));
  160. print("发送最后一帧---------------------------");
  161. currtask = null;
  162. }
  163. isspeaking = false;
  164. _lastBelowThresholdTime = null;
  165. _buff = Uint8List(0);
  166. return;
  167. }
  168. //写入音频数据到缓存区中
  169. Uint8List _appendToBuffer(Uint8List newData) {
  170. var newBuffer = Uint8List(_buff.length + newData.length);
  171. newBuffer.setAll(0, _buff);
  172. newBuffer.setAll(_buff.length, newData);
  173. return newBuffer;
  174. }
  175. //读取缓存区中一帧数据
  176. Uint8List _getAudioData() {
  177. if (_buff.length >= _chunkSize) {
  178. // 从缓冲区中读取1280字节的数据
  179. var data = _buff.sublist(0, _chunkSize);
  180. // 移除已读取的数据
  181. _buff = _buff.sublist(_chunkSize);
  182. return data;
  183. } else if (_buff.length >= 0) {
  184. return _buff;
  185. } else {
  186. // 如果数据不足,返回空数据
  187. return Uint8List(0);
  188. }
  189. }
  190. //当前音量计算
  191. double _calculateAmplitude(Uint8List data) {
  192. Int16List samples = Int16List(data.length ~/ 2);
  193. ByteData byteData = ByteData.view(data.buffer);
  194. for (int i = 0; i < samples.length; i++) {
  195. samples[i] = byteData.getInt16(i * 2, Endian.little);
  196. }
  197. double sum = 0;
  198. for (int sample in samples) {
  199. sum += (sample * sample);
  200. }
  201. double rms = sqrt(sum / samples.length);
  202. return rms;
  203. }
  204. // 检查说话状态
  205. int _checkSpeakingStatus() {
  206. if (isRecording) {
  207. //正在录音
  208. if (_volume > _speakingThreshold && !isspeaking) {
  209. // 音量高于阈值,表示开始说话
  210. isspeaking = true;
  211. return 1;
  212. } else if (_volume < _silenceThreshold && isspeaking) {
  213. // 音量低于阈值
  214. if (_lastBelowThresholdTime == null) {
  215. // 记录第一次低于阈值的时间
  216. _lastBelowThresholdTime = DateTime.now();
  217. } else if (DateTime.now().difference(_lastBelowThresholdTime!) >
  218. _silenceDuration) {
  219. // 持续低于阈值超过指定时间,表示结束说话
  220. isspeaking = false;
  221. return 3;
  222. }
  223. } else {
  224. // 音量恢复到阈值以上,重置计时器
  225. _lastBelowThresholdTime = null;
  226. }
  227. }
  228. if (!isspeaking) {
  229. return 0;
  230. } else {
  231. return 2;
  232. }
  233. }
  234. //处理返回结果
  235. void _handleData(dynamic json) {
  236. final status = json['header']['status'];
  237. final sid = json['header']['sid'];
  238. var payload = json['payload'];
  239. if (payload != null) {
  240. payload = json['payload'] as Map<String, dynamic>;
  241. //转文字的结果
  242. if (payload.containsKey('recognition_results')) {
  243. final model = RecognitionResult.fromJson(json);
  244. if (model.payload?.recognitionResults?.text == null ||
  245. model.payload?.recognitionResults?.text?.trim() == '') return;
  246. print("收到识别数据 ${model.toString()}");
  247. onRecognitionResult(model);
  248. //翻译好的结果
  249. } else if (payload.containsKey('streamtrans_results')) {
  250. final model = StreamtransResult.fromJson(json);
  251. if (model.payload?.streamtransResults?.text == null ||
  252. model.payload?.streamtransResults?.text?.trim() == '') return;
  253. onStreamtransResult(model);
  254. //合成语音
  255. } else if (payload.containsKey('tts_results')) {
  256. final bytes = base64Decode(payload['tts_results']['audio']);
  257. if (bytes.isEmpty) return;
  258. onAudioResult(AudioModel(status: status, sid: sid, data: bytes));
  259. }
  260. }
  261. }
  262. }
  263. //讯飞翻译任务
  264. class XunFeiTranslateTask {
  265. late WebSocketChannel _channel;
  266. bool isconnected = false;
  267. late Function(dynamic) _handle;
  268. XunFeiTranslateTask(String url, Function(dynamic) handle) {
  269. _handle = handle;
  270. _channel = WebSocketChannel.connect(Uri.parse(url));
  271. _channel.stream.timeout(Duration(seconds: 10)); //设置超时时间
  272. _channel.stream.listen(
  273. (message) {
  274. onMessage(message);
  275. },
  276. onError: (error) {
  277. isconnected = false;
  278. print('连接失败: $error');
  279. },
  280. onDone: () {
  281. isconnected = false;
  282. print('WebSocket 连接已关闭');
  283. print('Close code: ${_channel.closeCode}');
  284. print('Close reason: ${_channel.closeReason}');
  285. },
  286. cancelOnError: true,
  287. );
  288. isconnected = true;
  289. }
  290. Future<void> sendaudio(Map<String, dynamic> data) async {
  291. if (isconnected) {
  292. _channel.sink.add(json.encode(data));
  293. }
  294. }
  295. Future<void> onMessage(String message) async {
  296. try {
  297. // print("收到的消息:$message");
  298. // 对结果进行解析
  299. var messageMap = json.decode(message);
  300. var status = messageMap["header"]["status"];
  301. _handle(messageMap);
  302. if (status == 2) {
  303. print("任务已结束!------------------------------------------");
  304. _channel.sink.close();
  305. }
  306. } catch (e) {
  307. print("收到的消息 异常:$e");
  308. }
  309. return;
  310. }
  311. }