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

298 行
8.5 KiB

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