持续创作,加速成长!这是我参与「日新计划 6 月更文挑战」的第4天,点击查看活动详情
前言
哈喽,的小伙伴好呀,之前端午节画粽子的时候【端午】会说话的粽子你见过吗?,实现了语音合成将文字转化为声音播报,那么今天这篇文章我们就反过来在Flutter中将声音即时转换为文字。无需集成SDK。
准备工作
1、讯飞准备工作
讯飞准备工作参考上一篇Flutter实现讯飞在线语音合成(WebSocket流式版),这里就不过多介绍了。
需要注意的点语音听写的API与合成的API是不同的,APPID,APIKey,APISecret等则是相同的。
2、录音
对于语音听写来说,首先我们需要获取到我们的声音数据,所以,这里我们需要一个集成
一个录音插件,flutter_sound:^9.2.3
。
一个音频硬件处理插件 audio_session 0.1.7
。例如:当音乐播放时,录音开启应当静音。
Android端配置:
清单文件增加录音权限。
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
iOS端配置:
info.plist
文件增加:
<key>NSMicrophoneUsageDescription</key>
<string>描述你使用麦克风用来干嘛</string>
Podfile
文件中增加:
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=1',
]
end
end
end
最后还需要在iOS工程中增加系统库libc++.tbd
至此录音准备工作完毕。
录制音频到流
因为我们的核心目的是实时语音听写,所以我们不希望我们的录制声音保存下来,我们需要将声音以流stream
的形式接收,然后通过讯飞引擎进行输出文本。
1、初始化录音
在录制之前,我们需要初始化录音机。
核心代码:
/// 初始化录音
Future<void> initRecorder() async {
// 权限申请
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('Microphone permission not granted');
}
// 初始化录音
await _mRecorder.openRecorder();
// 音频处理初始化
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy:
AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
}
2、开始录制
这里我们要将声音录制到流,所以要使用toStream
来接受,如果你想保存文件使用toFile
,两者不同时使用,根据业务进行选择。
这里简单的介绍下这个插件,它不仅能录制保存不同格式音频,同时还支持播放保存的任何格式音频,还支持流形式的播放,这里我们为了讯飞语音要求,这里我们采用pcm16k采样率。 录制视频完成记得关闭录音,释放资源。
核心代码:
// 开始录制
Future<void> startRecord() async {
var recordingDataController = StreamController<FoodData>();
_mRecordingDataSubscription =
recordingDataController.stream.listen((buffer) {
debugPrint("stream result ${buffer.data}");
});
await _mRecorder.startRecorder(
// toFile: "filePath",
toStream: recordingDataController.sink,
codec: Codec.pcm16,
numChannels: 1,
sampleRate: 16000,
);
}
// 关闭录制
Future<void> stopRecorder() async {
await _mRecorder.stopRecorder();
if (_mRecordingDataSubscription != null) {
await _mRecordingDataSubscription!.cancel();
_mRecordingDataSubscription = null;
}
}
当我们开始录制时,在stream
的回调中就会不断收到以下音频字节码信息,出现这些信息,说明我们的声音已经开始录制了。
有了这些音频字节码信息,我们就可以通过讯飞引擎来实时进行转换文字了。
备注: 这里每次接受的字节是640个字节
,这里当时我想控制接受字节数量和接受频率,但是没有找到好的方法。讯飞的建议是间隔发送时间为40ms
,每次发送的直接数是一帧音频,pcm格式也就是1280个字节
。但是我尝试每次发送640个字节
好像也可以识别,所有讯飞这里是建议,并不是强制,所以,发送640个字节
也是可以识别的。这里以后我找到控制流的反馈频率以及每次反馈的大小再做调整,算是一个小坑吧,不过测试结果好像影响不大。
至此,我们通过录音等到流的信息完毕。
向讯飞发送流信息
接下来我们就需要将这些流信息通过WebSocket连接形式发送给讯飞。
1、创建连接
如何初始化WevSocket以及创建连接参考之前的文章语音合成,这里就不多介绍了。
这里说一个注意点,语音听写合成的Api是有区别的,所以在鉴权时一定要区分开,其他加密规则一样。
2、发送信息
发送信息和语音合成的方式一样,只是发送的参数发生了改变,具体参数我这里封装了实体类,对应属性可以参考官网字段说明,比我注释的详细,比如支持个性化词汇的设置、动态修正、方言识别设置等。
发送数据核心代码:
void sendVoice(Uint8List voice, {int state = 1}) {
XfVoiceReq xfVoiceReq = XfVoiceReq();
///设置appId
CommonV common = CommonV();
common.appId = _appId;
xfVoiceReq.common = common;
BusinessV business = BusinessV();
// 语种 zh_cn:中文
business.language = "zh_cn";
//应用领域
// iat:日常用语
// medical:医疗
business.domain = "iat";
// 方言
business.accent = "mandarin";
// 静默时间
business.vadEos = 3000;
// 动态修正
business.dwa = "wpgs";
// 使用场景
business.pd = "game";
// 是否开启标点符号
business.ptt = 1;
// 字体 简体繁体
business.rlang = "zh-cn";
business.vinfo = 1;
//数字以阿拉伯数字返回
business.nunum = 1;
business.speexSize = 70;
business.nbest = 3;
business.wbest = 5;
xfVoiceReq.business = business;
DataV dataReq = DataV();
//0 :第一帧音频
// 1 :中间的音频
// 2 :最后一帧音频,最后一帧必须要发送
dataReq.status = state;
dataReq.format = "audio/L16;rate=16000";
dataReq.encoding = "raw";
dataReq.audio = base64.encode(voice);
xfVoiceReq.data = dataReq;
String req = jsonEncode(xfVoiceReq);
_channel?.sink.add(req);
// debugPrint("inputV == $req}");
if (state == 2) {
close();
}
}
如果发送成功,讯飞会给我们返回以下数据,说明解析成功。需要注意的是这里data中的status = 2代表最后一帧音频传输完毕,需要关闭连接。 这里解析数据我也分装了实体类,这里我们只需关系ws中cw数组中首个对象的w数据即可,开启动态修正会不断的校验修改,会帮助我们获得更为接近真实想法的文本,比如上面我说哈喽大家好,一开始返回的是下方的哈-喽,开启动态修正后,最后返回了上面的Hello,大家好。 核心代码:
var xfVoiceRes = XfVoiceRes.fromJson(jsonDecode(data));
var code = xfVoiceRes.code;
var status = xfVoiceRes.data?.status;
if (code == 0 && status != 2) {
var result = xfVoiceRes.data?.result;
var ws = result?.ws;
StringBuffer resultText = StringBuffer();
if (ws != null) {
for (int i = 0; i < ws.length; i++) {
var cw = ws[i].cw;
if (cw != null) {
resultText.write(cw[0].w);
}
}
onTextResult?.call(resultText.toString());
}
} else if (status == 2) {
// 识别结束
close();
} else {
onTextResult?.call("数据有误:${xfVoiceRes.message}");
}
3、自定义热词
有些时候我们需要输出一些特殊文本,比如人的名字,这时候可以在讯飞控制台这里进行添加,添加之后再说这个名字就会直接输出我们设置的热词。 至此,声音转文本基本结束。
加点交互
语音识别的的使用场景一般是比如我们的智能音箱,手机端语音搜索、手游中的文字交流等等,在说话同时如果我们可以监听到声音的音量有一个声波的振幅可以给用户一个更好的体验,那这里我们就需要获取声音音量的回调,其实我们只需在初始化的时候实现下方代码就能接受声音的回调了,可以设置音量接受的频率。
// 设置音频音量获取频率
_mRecorder.setSubscriptionDuration(
const Duration(milliseconds: 100),
);
_mRecorder.onProgress!.listen((event) {
// 0 - 120
print("音量 : ${event.decibels}");
});
这里我简单的写了一个音量的交互,通过这样的交互,可以使用户体验会更好,效果只是展示回调音量返回效果,这里我设置每100毫秒返回当前音量。看下最终效果:
正常文本:
自定义热词文本:
注:示例为1分钟的即时语音听写,如果有超过1分钟需求,请参考官方的语音转写。
小结:
21世纪是Ai的时代,语音识别技术的使用场景遍布各个角落,目前国内在这方面做的比较好的公司,一个讯飞,一个百度,讯飞算是起步早的,目前推出的解决方案也是覆盖生活的各个角落,比如支持医疗、金融、政务中的专业语音识别场景,当然讯飞的这些服务都是收费的,但是就使用体验来说,无论语音合成还是语音识别体验都还是不错的,目前国内的智能音箱,比如小爱同学也都是使用的讯飞语音识别,有兴趣的小伙伴可以去讯飞官网上了解,这篇文章就到这里了,希望对你有所帮助,如果有问题,欢迎交流~