语音识别动手玩

语音识别哪家强,腾讯百度讯飞谁称王?如果想自己动手体验一下各家的语音识别服务却又不只从何入手,那么相信这篇文章就是你需要的。废话不多说,我们直接开动吧!


更新历史

  • 2017.07.10: 完成初稿

写在前面

上周因为工作需要,用 HTML5 + Go 搭了一个简单的语音识别评测的 demo,用户可以直接打开网页 -> 说一句话 -> 查看来自不同服务提供商的语音识别结果(目前是科大讯飞、腾讯 AI 加速器和百度语音三家)。因为之前的项目很少直接跟硬件打交道,所以这次依然是摸着石头过河。好在之前有做过语音识别的项目,所以基本的概念还是懂的(因为涉及到数据格式,不懂的话即使看代码也容易云里雾里)。

俗话说的好,授人以鱼不如授人以渔,这个小项目的代码固然不多,但如何一个人把项目做好可能比源代码本身更重要,所以更多会记录整个项目是如何开展的过程(以及我的思考和选择)。

准备工作

编码之前的准备工作有很多,从需求分析、技术调研到技术选型,每一步都走好了,项目才能顺利。

需求分析

在领到一个新任务之后,最重要的工作不是动手,而是弄清楚到底需要做什么。为什么这么说?因为很多时候老大们只是有一个想法想要验证,并不会有一个很清晰的图景,这就需要我们主动沟通,去和老大们一起弄清楚到底需要做一个什么东西,然后才是具体技术方案的选择。

经过和老大的沟通,确定了是要做一个大家都方便访问的评测不同语音识别服务提供商的 demo,老大倾向于做一个页面,只要有浏览器和麦克风就可以体验,至于交互这些只要达到能用的平均线即可。

技术调研

因为需要依赖第三方的服务,所以首先要弄清楚第三方提供了什么服务,尤其是具体的接入方式需要特别注意。经过一番搜索和阅读,我得到了下面的表格:

提供商\SDK平台 iOS android Linux(c) Java REST API Windows
百度 yes yes x x yes x
科大 yes yes yes yes x yes
腾讯 x x x x yes x

对应的文档在

支持的格式

  • 百度 Rest API: 原始 PCM 的录音参数必须符合 8k/16k 采样率、16bit 位深、单声道,支持的压缩格式有:pcm(不压缩)、wav(不压缩,pcm编码)、amr(压缩格式)
  • 腾讯 Rest API: 必须符合16k采样率、16bit采样位数、单声道,语音格式 PCM、WAV、AMR、 SILK
  • 讯飞 SDK: 采样率16KHZ或者8KHZ,单声道,采样精度16bit的PCM或者WAV格式的音频

在调研到这些信息之后,我们就可以轻松做出技术选型了。

技术选型

首先的考虑因素是快速出成果,所以能用 Restful API 就尽量用(毕竟直接对好接口即可)。但是讯飞在这个方面非常不友好,只提供了 Linux C 和 Java 的 SDK,所以得另外想办法处理讯飞这个问题。

注:最快的方法其实是用 android 或 iOS 的 SDK,集成到项目中直接用即可,但因为前面老大已经要求要用 html 方案,因此这些不予考虑(除非 html 做不出来,才弄其他方案)

因为讯飞只提供了 Linux C 的 SDK(Java 我是不太想用的),所以得想个办法集成到后端(Go),正当我一筹莫展之际,一个名为 imroc/ontts 的项目进入了我的眼帘(科大讯飞语音linux在线语音合成后台服务),虽然并不是使用讯飞的语音识别而是语音合成功能,但证明了用 Go 封装讯飞的 C SDK 是可能的。于是我赶紧看了一波代码,并很快完成了用 Go 封装的讯飞语音识别 SDK(也算是给讯飞做了一点微小的工作)。

好,那么现在问题解决,前端 html5 + js,后端 Go + Go 封装的 C SDK,直接在之前为公司开发的深度学习平台上开一个接口即可。

创建应用

准备工作的最后一步就是到各家的开放平台上创建应用,得到我们调用 API 或 SDK 的凭证。百度语音的最好申请,腾讯 AI 加速器的需要申请内测(我们公司有合作所以不用申请),讯飞的需要审核(不然每天只有 500 次)。

创建完成之后把各家的 App ID/APP Key 之类的信息记录下来,后面需要用。

开始编码

之所以要后端,是因为前端 JS 发请求会遇到跨域的问题,另外后端处理我也更加熟悉(事实证明离开了后端还真不行)

前端

前端部分的难点在于如何通过浏览器调用麦克风,尤其是在 Chrome 上,要求全程 HTTPS 加密(当然也有另外的办法就是本地打开)。那用其他浏览器可不可以呢?可以!比如 Firefox,但是感觉 Firefox 对 Mac 的麦克风支持的一般,经常出现问题,所以我一直都是用 Chrome 在测试的。

因为毕竟前端写得少,大部分代码都是参照 HTML5网页录音和压缩,边猜边做..(附源码) 这篇博客写的。主要是两个文件 index.htmlrecorder.js,接下来分别说明一下。

先说 index.html 的部分,代码不长,主要做的事情就是申请麦克风访问,然后给按钮添加动作,最后就是用 audio 控件来播放音频了。audio 控件还是比较好用的,给出音源地址即可,播放和音量都自带,很省心。我还加了个日志区域方便调试,其他的没什么难度,在此略去不表。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>语音识别评测 Demo</title>
</head>
<body>
<h1>追一语音识别评测 Demo</h1>
<h2>评测科大讯飞、百度语音与腾讯 AI 加速器的语音识别接口</h2>
<p>由于 Chrome47 以上以及 QQ 浏览器需要 HTTPS 的支持,请更换至 360、FireFox、Edge 进行体验</p>
<p>另:IE 和 Safari 全版本不支持录音功能</p>
<button id="start" class="ui-btn ui-btn-primary">录音</button>
<button id="stop" class="ui-btn ui-btn-primary" disabled>停止</button>
<!--<button id="upload" class="ui-btn ui-btn-primary" disabled>上传</button>-->
<div id="audio-container"></div>
<h2>操作日志</h2>
<pre id="log"></pre>
<script type="text/javascript" src="js/recorder.js"></script>
<script>
function __log(e, data) {
log.innerHTML += "\n" + getNowFormatDate() + " " + e + " " + (data || '');
};
function getNowFormatDate() {
var date = new Date();
var seperator1 = "/";
var seperator2 = ":";
var month = date.getMonth() + 1;
var strDate = date.getDate();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate;
}
var hours = date.getHours()
if (hours < 10) {
hours = "0" + hours
}
var minutes = date.getMinutes()
if (minutes < 10) {
minutes = "0" + minutes
}
var seconds = date.getSeconds()
if (seconds < 10) {
seconds = "0" + seconds
}
var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate +
"-" + hours + seperator2 + minutes + seperator2 + seconds;
return currentdate;
}
window.onload = function() {
var start = document.querySelector('#start');
var stop = document.querySelector('#stop');
//var upload = document.querySelector('#upload');
var download = document.querySelector('#download');
var container = document.querySelector('#audio-container');
var recorder;
HZRecorder.get(function(rec) {
recorder = rec;
})
start.addEventListener('click', function() {
__log("开始录音")
this.disabled = true;
stop.disabled = false;
//upload.disabled = true;
var audio = document.querySelectorAll('audio');
for (var i = 0; i < audio.length; i++) {
if (!audio[i].paused) {
audio[i].pause();
}
container.removeChild(audio[i]) // 移除之前的录音
}
recorder.start();
});
stop.addEventListener('click', function() {
this.disabled = true;
start.disabled = false;
//upload.disabled = false;
recorder.stop();
var audio = document.createElement('audio');
recorder.play(audio)
container.appendChild(audio);
// 下载音频文件
// var link = window.document.createElement('a');
// var link = document.createElement('a');
// link.href = audio.src;
// link.download = 'output.wav';
// link.click();
recorder.upload("http://127.0.0.1:8778/v1/api/qq/voice_recog", function(state, e) {
switch (state) {
case 'uploading':
__log("上传中,会在后端进行转码与识别");
break;
case 'ok':
//__log("上传成功");
break;
case 'error':
__log("上传失败");
break;
case 'cancel':
__log("上传被取消");
break;
}
})
});
};
</script>
</body>
</html>

然后是 recorder.js(基本是照抄上面提到的原博客),主要做的工作就是处理浏览器采集到的音频信号,编码成 wav。注意这里没有更改采样率(采用默认的 44100),因为直接截取的话音调会有变化,导致识别结果较差。但其他的部分都尽量按照前面提到的标准来弄,即 16 位采样。

(function(window) {
//兼容
window.URL = window.URL || window.webkitURL;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
var HZRecorder = function(stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 16; //采样数位 8, 16
config.sampleRate = config.sampleRate || 44100; //采样率 16000(会变慢)
var context = new(window.webkitAudioContext || window.AudioContext)();
var audioInput = context.createMediaStreamSource(stream);
var createScript = context.createScriptProcessor || context.createJavaScriptNode;
var recorder = createScript.apply(context, [4096, 1, 1]);
var audioData = {
size: 0, //录音文件长度
buffer: [], //录音缓存
inputSampleRate: context.sampleRate, //输入采样率
inputSampleBits: 16, //输入采样数位 8, 16
outputSampleRate: config.sampleRate, //输出采样率
oututSampleBits: config.sampleBits, //输出采样数位 8, 16
input: function(data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
},
compress: function() { //合并压缩
//合并
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//压缩
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0,
j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
},
encodeWAV: function() {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1; //单声道
var offset = 0;
var writeString = function(str) {
for (var i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
}
// 资源交换文件标识符
writeString('RIFF');
offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + dataLength, true);
offset += 4;
// WAV文件标志
writeString('WAVE');
offset += 4;
// 波形格式标志
writeString('fmt ');
offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true);
offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true);
offset += 2;
// 通道数
data.setUint16(offset, channelCount, true);
offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true);
offset += 4;
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
offset += 4;
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
data.setUint16(offset, channelCount * (sampleBits / 8), true);
offset += 2;
// 每样本数据位数
data.setUint16(offset, sampleBits, true);
offset += 2;
// 数据标识符
writeString('data');
offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, dataLength, true);
offset += 4;
// 写入采样数据
if (sampleBits === 8) {
for (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
};
//开始录音
this.start = function() {
// 需要先清理 buffer
audioData.size = 0;
audioData.buffer = [];
audioInput.connect(recorder);
recorder.connect(context.destination);
}
//停止
this.stop = function() {
recorder.disconnect();
}
//获取音频文件
this.getBlob = function() {
this.stop();
return audioData.encodeWAV();
}
//回放
this.play = function(audio) {
var blob = this.getBlob()
audio.src = window.URL.createObjectURL(blob);
__log("录音结束,Wav 大小: " + blob.size + "B Wav 地址: " + audio.src)
audio.controls = true;
}
//上传
this.upload = function(url, callback) {
var fd = new FormData();
fd.append("audioData", this.getBlob());
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
__log(xhr.responseText)
}
}
if (callback) {
xhr.upload.addEventListener("progress", function(e) {
callback('uploading', e);
}, false);
xhr.addEventListener("load", function(e) {
callback('ok', e);
}, false);
xhr.addEventListener("error", function() {
callback('error', e);
}, false);
xhr.addEventListener("abort", function(e) {
callback('cancel', e);
}, false);
}
xhr.open("POST", url);
xhr.send(fd);
}
//音频采集
recorder.onaudioprocess = function(e) {
audioData.input(e.inputBuffer.getChannelData(0));
//record(e.inputBuffer.getChannelData(0));
}
};
//抛出异常
HZRecorder.throwError = function(message) {
alert(message);
throw new function() { this.toString = function() { return message; } }
}
//是否支持录音
HZRecorder.canRecording = (navigator.getUserMedia != null);
//获取录音机
HZRecorder.get = function(callback, config) {
if (callback) {
if (navigator.getUserMedia) {
navigator.getUserMedia({ audio: true } //只启用音频
,
function(stream) {
var rec = new HZRecorder(stream, config);
callback(rec);
},
function(error) {
switch (error.code || error.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
HZRecorder.throwError('用户拒绝提供信息。');
break;
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
HZRecorder.throwError('浏览器不支持硬件设备。');
break;
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
HZRecorder.throwError('无法发现指定的硬件设备。');
break;
default:
HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name));
break;
}
});
} else {
HZRecorder.throwErr('当前浏览器不支持录音功能。');
return;
}
}
}
window.HZRecorder = HZRecorder;
})(window);

至此,前端的工作就告一段落,主要工作就是录音,并把 44100Hz 16bit 的 wav 文件上传到后端,并等待后端返回语音识别结果。

后端

虽然前端做的工作不多,但其实后端要做的也不多,毕竟是调用别人的接口嘛,要再这么麻烦就没有人用啦。所以后端要做的工作主要分两个部分,对于有 Restful API 的服务来说,就是准备数据,对于只有 Linux C SDK 的服务来说,就是用 Go 去调用 C SDK,接下来我们先讲讲共用的部分,然后分别说说不同服务需要注意的地方。

共用的部分就是改变录音的采样率,前面提到浏览器默认的采样率是 44100Hz,我们需要转变成 16000Hz 的,考虑到 44100 并不能整除 16000,所以简单的采样一定会导致频率的变化,但是不用紧张,我们还有两大法宝 ffmpegsox。转码的命令也很简单,以下两个任选一个即可:

ffmpeg -i input.wav -ar 16000 output.wav
# or
sox input.wav -r 16000 output.wav

然后我们就要以转码后的 output.wav 为基础,进行下面的操作了。使用 Restful API 需要注意的就是不同平台有不同的加密、验证方式和不同的参数,但无论如何音频文件需要用 Base64 编码,然后只要写一个通用的工具方法即可,这里不多说,主要还是说一下如何去用 Go 调用 C SDK(主要参考的是 imroc/ontts - 科大讯飞语音linux在线语音合成后台服务 的代码)。

首先就是从讯飞的语音云平台上下载 SDK,然后把 SDK 内的文件放到一个名为 xf 的文件夹中,就叫做 package xf 好了。目录大概是这样的:

├── README.md
├── include
│   ├── convert.h
│   ├── msp_cmn.h
│   ├── msp_errors.h
│   ├── msp_types.h
│   ├── qise.h
│   ├── qisr.h
│   └── qtts.h
├── libs
│   ├── x64
│   │   └── libmsc.so
│   └── x86
│   └── libmsc.so
└── xf.go

着重讲两个文件,一个是 xf.go(相当于是 C SDK 的 Wrapper),另一个是 convert.h(是参照讯飞的官方例子改的)。我们先来看看 convert.h,实际上就是用 C 来完成语音识别的调用(代码略长,感兴趣的同学估计得慢慢理解)。留意一下 run_iat 这个核心函数的返回值 char *,后面在 xf.go 中有用:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include "qisr.h"
#include "msp_cmn.h"
#include "msp_errors.h"
#define BUFFER_SIZE 4096
#define FRAME_LEN 640
#define HINTS_SIZE 100
char* run_iat(const char* audio_file)
{
const char* session_begin_params = "sub = iat, domain = iat, language = zh_cn, accent = mandarin, sample_rate = 16000, result_type = plain, result_encoding = utf8";
char *retstr = NULL;
const char* session_id = NULL;
char rec_result[BUFFER_SIZE] = {'\0'};
char hints[HINTS_SIZE] = {'\0'}; //hints为结束本次会话的原因描述,由用户自定义
unsigned int total_len = 0;
int aud_stat = MSP_AUDIO_SAMPLE_CONTINUE ; //音频状态
int ep_stat = MSP_EP_LOOKING_FOR_SPEECH; //端点检测
int rec_stat = MSP_REC_STATUS_SUCCESS ; //识别状态
int errcode = MSP_SUCCESS ;
FILE* f_pcm = NULL;
char* p_pcm = NULL;
long pcm_count = 0;
long pcm_size = 0;
long read_size = 0;
if (NULL == audio_file)
{
retstr = "[Error] 文件名为空";
goto iat_exit;
}
f_pcm = fopen(audio_file, "rb");
if (NULL == f_pcm)
{
retstr = "[Error] 打开音频文件失败";
printf("\nopen [%s] failed! \n", audio_file);
goto iat_exit;
}
fseek(f_pcm, 0, SEEK_END);
pcm_size = ftell(f_pcm); //获取音频文件大小
fseek(f_pcm, 0, SEEK_SET);
p_pcm = (char *)malloc(pcm_size);
if (NULL == p_pcm)
{
retstr = "[Error] 无法分配内存";
printf("\nout of memory! \n");
goto iat_exit;
}
read_size = fread((void *)p_pcm, 1, pcm_size, f_pcm); //读取音频文件内容
if (read_size != pcm_size)
{
retstr = "[Error] 读取音频文件失败";
printf("\nread [%s] error!\n", audio_file);
goto iat_exit;
}
printf("\n开始语音听写 ...\n");
session_id = QISRSessionBegin(NULL, session_begin_params, &errcode); //听写不需要语法,第一个参数为NULL
if (MSP_SUCCESS != errcode)
{
retstr = "[Error] QISRSessionBegin 失败";
printf("\nQISRSessionBegin failed! error code:%d\n", errcode);
goto iat_exit;
}
while (1)
{
unsigned int len = 10 * FRAME_LEN; // 每次写入200ms音频(16k,16bit):1帧音频20ms,10帧=200ms。16k采样率的16位音频,一帧的大小为640Byte
int ret = 0;
if (pcm_size < 2 * len)
len = pcm_size;
if (len <= 0)
break;
aud_stat = MSP_AUDIO_SAMPLE_CONTINUE;
if (0 == pcm_count)
aud_stat = MSP_AUDIO_SAMPLE_FIRST;
printf(">");
ret = QISRAudioWrite(session_id, (const void *)&p_pcm[pcm_count], len, aud_stat, &ep_stat, &rec_stat);
if (MSP_SUCCESS != ret)
{
retstr = "[Error] QISRAudioWrite 失败";
printf("\nQISRAudioWrite failed! error code:%d\n", ret);
goto iat_exit;
}
pcm_count += (long)len;
pcm_size -= (long)len;
if (MSP_REC_STATUS_SUCCESS == rec_stat) //已经有部分听写结果
{
const char *rslt = QISRGetResult(session_id, &rec_stat, 0, &errcode);
if (MSP_SUCCESS != errcode)
{
retstr = "QISRGetResult failed";
printf("\nQISRGetResult failed! error code: %d\n", errcode);
goto iat_exit;
}
if (NULL != rslt)
{
unsigned int rslt_len = strlen(rslt);
total_len += rslt_len;
if (total_len >= BUFFER_SIZE)
{
printf("\nno enough buffer for rec_result !\n");
retstr = "[Error] Buffer 太小";
goto iat_exit;
}
strncat(rec_result, rslt, rslt_len);
}
}
if (MSP_EP_AFTER_SPEECH == ep_stat)
break;
usleep(200*1000); //模拟人说话时间间隙。200ms对应10帧的音频
}
errcode = QISRAudioWrite(session_id, NULL, 0, MSP_AUDIO_SAMPLE_LAST, &ep_stat, &rec_stat);
if (MSP_SUCCESS != errcode)
{
printf("\nQISRAudioWrite failed! error code:%d \n", errcode);
goto iat_exit;
}
while (MSP_REC_STATUS_COMPLETE != rec_stat)
{
const char *rslt = QISRGetResult(session_id, &rec_stat, 0, &errcode);
if (MSP_SUCCESS != errcode)
{
printf("\nQISRGetResult failed, error code: %d\n", errcode);
goto iat_exit;
}
if (NULL != rslt)
{
unsigned int rslt_len = strlen(rslt);
total_len += rslt_len;
if (total_len >= BUFFER_SIZE)
{
printf("\nno enough buffer for rec_result !\n");
goto iat_exit;
}
strncat(rec_result, rslt, rslt_len);
}
usleep(150*1000); //防止频繁占用CPU
}
printf("\n语音听写结束\n");
printf("=============================================================\n");
printf("%s\n",rec_result);
printf("=============================================================\n");
retstr = rec_result;
iat_exit:
if (NULL != f_pcm)
{
fclose(f_pcm);
f_pcm = NULL;
}
if (NULL != p_pcm)
{ free(p_pcm);
p_pcm = NULL;
}
QISRSessionEnd(session_id, hints);
return retstr;
}

然后我们来看看对 C SDK 的封装,这里采用了 cgo 的方案,具体的不展开,我们直接上 xf.go 的代码(很短,没想到吧,另外 import "C" 这句和上面的注释之间不能有空行):

package xf
/*
#cgo CFLAGS:-g -Wall -I ./include
#cgo LDFLAGS:-L./lib -lmsc -lrt -ldl -lpthread
#include "convert.h"
*/
import "C"
import "fmt"
func Login() error {
loginParams := "appid = your_app_id, work_dir = ."
ret := C.MSPLogin(nil, nil, C.CString(loginParams))
if ret != C.MSP_SUCCESS {
fmt.Println("登录失败,错误码: %d", int(ret))
return fmt.Errorf("登录失败,错误码: %d", int(ret))
}
fmt.Println("登录成功")
return nil
}
func Logout() error {
ret := C.MSPLogout()
if ret != C.MSP_SUCCESS {
fmt.Println("注销失败,错误码: %d", int(ret))
return fmt.Errorf("注销失败,错误码: %d", int(ret))
}
fmt.Println("注销成功")
return nil
}
func SpeechToText(filename string) (string, error) {
// https://golang.org/cmd/cgo/
// 需要在执行前 export LD_LIBRARY_PATH=/usr/local/lib
// 如果改动了 c++ 文件,需要改动这个调用 C 的文件,才会进行重新编译,不然一直是老的
// 官方例子中是支持 wav 的(不需要转换了)
// ffmpeg -i output.wav -f s16be -acodec pcm_s16be output.pcm
fmt.Println("004")
retstr := C.run_iat(C.CString(filename))
return C.GoString(retstr), nil
}

从 Go 的 string 到 C 的 char* 可以使用 C.CString(),反过来可以使用 C.GoString() 来转换。转换完成之后,把结果返回给前端即可。

总结

像游戏评测一样,来个打分,满分十分

  • 接入方便程度
    • 腾讯: 9.0
    • 百度: 8.5
    • 讯飞: 5.0
  • 用户体验
    • 百度: 9.0
    • 腾讯: 8.0
    • 讯飞: 6.0
  • 识别效果(基于小样本的主观判断)
    • 讯飞: 8.0
    • 百度: 7.5
    • 腾讯: 6.0

在这里希望讯飞能够更加互联网/接地气一些,不然用起来真的挺麻烦的(不知道大客户是不是有其他待遇)

附录: 音频格式知识

WAV:wav是一种无损的音频文件格式,WAV符合 PIFF(Resource Interchange File Format)规范。所有的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。

PCM:PCM(Pulse Code Modulation—-脉码调制录音)。所谓PCM录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由[1]、[0]等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。

简单来说:wav是一种无损的音频文件格式,pcm是没有压缩的编码方式。

WAV可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中,和AVI一样,只要安装好了相应的Decode,就可以欣赏这些WAV了。在Windows平台下,基于PCM编码的WAV是被支持得最好的音频格式,所有音频软件都能完美支持,由于本身可以达到较高的音质的要求,因此,WAV也是音乐编辑创作的首选格式,适合保存音乐素材。因此,基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互转换之中,例如MP3转换成WMA。来源

采样的位数指的是描述数字信号所使用的位数。8 位(8 bit)代表 2 的 8 次方即 256,16 位(16 bit)则代表 2 的 16 次方即 65536 / 1024 = 64K

采样率是一秒钟内对声音信号的采样次数

网络接收一个音频的时长是 20ms, 已知音频采样率是 8kHz,采样的位数是 16bit。[时长]20ms * [采样率]8kHz * [采样的位数]16bit = 320 byte

例如,CD 采用16位的采样精度,44.1KHz 的采样频率,为双声道,它每秒所需要的数据量为 16×44100×2÷8=176400 字节。这样算下来,比特率应该是 1400 多 Kbps,如果采用 MP3、WMA 编码格式,比特率能够更小。来源

参考链接

捧个钱场?