持续创作,加速成长!这是我参与「日新计划 6 月更文挑战」的第2天,点击查看活动详情。

前言

大家好,上次画了一个会说话的粽子使用了讯飞语音技术,那么今天这篇文章就带你解析如何在Flutter跨平台中让粽子开口说话,无需集成任何SDK,从而非常轻量级的实现给你的应用装上嘴巴的功能。

创建应用

首先我们需要到讯飞注册账号登录点击控制台创建一个应用,填写ios应用商店应用所approve属信息即可,非常简单。

Flutter实现讯飞在线语音合成(WebSocket流式版)

以我这里创建了一个Flutter_Demo应用为例,点击语音合成之后,我们会得到右边一共三个参数和API调用接口,这些参数在以后生成鉴权参数以及合成语音的时候会用到。

Flutter实现讯飞在线语音合成(WebSocket流式版)

到这里,应用就创建完成并且得到了专属的参数,是不是很简单。

创建webSocket连接

创建完应用之后,我们查看文档,因为demo是没有dart语言版本的,所以我们需要跟着文档步https和http的区别骤一步一步实现,我们可以看到主要是接口加密鉴权签名这里是比较麻烦的,其他符合要求即可。

Flutter实现讯飞在线语音合成(WebSocket流式版)

鉴权规则:查看文档可以看到,我们需要在API接口后面拼接这仨参数http协议,前两个都好说,第三个就是通过hmac-sha256规则加密在进行base64编码一系列骚操作得出,反正就是为了接口的安全,有兴趣的小伙伴可以到官网了解。
Flutter实现讯飞在线语音合成(WebSocket流式版)

使用dart语言,那么这里加密鉴权我们实体类型需要引入以下两个插件辅助:

# 格式时间戳
intl: ^0.17.0
# 加密
crypto: ^3.0.2

鉴权核心代码:

///获取鉴权地址
String getAuthUrl(String host, String apiKey, String apiSecret) {
  final DateFormat df = DateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'");
  String date = df.format(DateTime.now());
  final String signatureOrigin = 'host: $hostndate: $datenGET /v2/tts HTTP/1.1';
  final String signature = _hmacSha256Base64(signatureOrigin, apiSecret);
  final String authorizationOrigin =
      'api_key="$apiKey", algorithm="hmac-sha256", headers="host date request-line", signature="$signature"';
  final String authorization = base64.encode(authorizationOrigin.codeUnits);
  //对日期进行uri编码
  date = Uri.encodeComponent(date);
  final String url =
      'wss://$host/v2/tts?authorization=$authorization&date=$date&host=$host';
  print("鉴权 = $url");
  return url;
}
///hmacSha256加密后再进行base64编码
String _hmacSha256Base64(String message, String secret) {
  List<int> messageBytes = utf8.encode(message);
  List<int> secretBytes = utf8.encode(secret);
  Hmac hmac = Hmac(sha256, secretBytes);
  Digest digest = hmac.convert(messageBytes);
  return base64.encode(digest.bytes);
}

有了创建连接的url,接下来创建WebSocket连接application,需要引入以下插appearance件:

# webSocket 连接
web_socket_channel: ^2.2.0

核心代码:

/// 讯飞Socket
class XfSocket {
  IOWebSocketChannel? _channel;
  ValueChanged<String>? onFilePath;
  /// 创建连接
  /// listen 转化完成后的回调
  XfSocket.connect(String text, {this.onFilePath}) {
    String u = XfUtil.getAuthUrl(_host, _apiKey, _apiSecret);
    debugPrint('创建连接url = $u');
    //关闭上一个连接
    close();
    _channel = IOWebSocketChannel.connect(u);
    //创建监听
    _channel?.stream.listen((message) {
      _onMessage(message);
    }, onError: (e) {
      debugPrint(" init error $e");
    });
    /// 发送文本请求转换语音
    sendText(text);
  }
void _onMessage(dynamic data) {
  debugPrint("result = $data");
  var xfRes = XfRes.fromJson(jsonDecode(data));
  var decode = base64.decode(xfRes.data!.audio!);
  debugPrint("result222 = $decode");
  _writeCounter(decode);
}
  ///关闭连接
  close([int status = status.goingAway]) {
    debugPrint('关闭连接');
    _channel?.sink.close(status);
  }
}

发送文本数据

发送消息时,我们可以设置发音人、音频格式、音调、音量、数字读法等等,这些属github开放私库性一http代理定不能有错,有application1个错就可能出现莫名其妙的错误,具体说明官方文档说的很清楚,我这边封装了实体类,直接设置即可,发音人可以通过讯飞控制人进行添加。PS:github中文社区好听的声音都是收费的。价格不appointment一,自行体验。☺

Flutter实现讯飞在线语音合成(WebSocket流式版)
示例代码:

sendText(String text) {
  XfTextReq xfTextReq = XfTextReq();
  ///设置appId
  Common common = Common();
  common.appId = _appId;
  xfTextReq.common = common;
  Business business = Business();
  business.aue = "lame"; // 音频编码 mp3格式
  business.tte = "UTF8";
  business.pitch = 50;
  business.speed = 40;
  business.vcn = "x3_doudou";// 发音人
  business.volume = 50; //音量
  business.sfl = 0;// 是否开启流式传输 =1会将一段文字音频分成多段返回
  business.auf = "audio/L16;rate=16000";
  business.bgs = 0;
  business.reg = "0";
  business.rdn = "0";
  xfTextReq.business = business;
  DataReq dataReq = DataReq();
  dataReq.status = 2; //固定 = 2
  dataReq.text = base64.encode(utf8.encode(text));
  xfTextReq.data = dataReq;
  debugPrint("input == ${jsonEncode(xfTextReq)}");
  /// 这里一定要jsonEncode转化成字符串json  formJson对象不可以
  _channel?.sink.add(jsonEncode(xfTextReq));
}

这里通过listen方法监听回调收到ios15以下信息,其中data中的audio就是我们需要声音数据,通过base64进行解码就可以得到我们需要的声音字节码,然后将字节码写入文appstore件即可。

Flutter实现讯飞在线语音合成(WebSocket流式版)
比如我这里appear以mp3为例,MP3的音频编码为lame,只需设置aue属性即可,同时要注意文件github汤姆名后缀,其他格式有兴趣的小伙伴参照官网直接修改参数即可。

保存音频

保存音频就很简单了,通过以下插件将音频保存至存储卡内,这样我们下次播放就无须再次转换了。

# 文件路径获取
path_provider: ^2.0.1

核心代码:

/// 获取文件路径
Future<String> _getLocalFileDir() async {
  Directory? tempDir = await getExternalStorageDirectory();
  return tempDir!.path;
}
/// 获取文件
Future<File> _getLocalFile() async {
  String dir = await _getLocalFileDir();
  return File("$dir/demo.mp3");
}
/// 写入内容
void _writeCounter(Uint8List decode) async {
  File file = await _getLocalFile();
  file.writeAsBytes(decode, mode: FileMode.write).then((value) {
    debugPrint("result写入文件 $value");  
   //保存成功通知客户端可以播放。
    onFilePath?.call(value.path);
  });
}

具体用法

非常的easy。
播放音频插件:目http://www.baidu.com前只支持Androgithub中文社区id, 以后可以换个同时支持iOS的。

# 音频播放器
audioplayers: ^0.20.1
// text 转换文本
XfSocket.connect(text, onFilePath: (path) {
  _playAudio(path);
});
void _playAudio(String filePath) async {
  var i = await audioPlayer.play(filePath, isLocal: true);
  debugPrint("lxp $i");
}

HTTP要demo小伙伴的可以留言。稍后我会上传到github。

总结

需要注意的点,发送数据参数一定不能错,要认真对照官网,一般出问题大部分都是这里造成的,总体体验来说,讯飞的发音github是什么机器人已经非常接近人声了,甚至有的发音人阅读文章根本分辨不出来是是机器人github永久回家地址还是真人,但是是收费的~。,当然每天都有免费调用接口的额度,比如我们有时候看一本书http 302,不想看,想听,但是网上又没有人声版appstore本,那么用这个工具进行生成音频还是不错的,有时间再研究下ios应用商店语音实体类的定义识别,看了下文档,大同小异,有时间再分享了,那本篇文章就到这http 500里了,希望对你有所帮助~