基于微信JS-SDK的录音与语音评价功能实现[转]

最近接受了一个新的需求,希望制作一个基于微信的英语语音评价页面。即点击录音按钮,用户录音说出预设的英文,根据用户的发音给出对应的评价。

以下是示例二维码,使用微信扫一扫即可查看:

基于微信JS-SDK的录音与语音评价功能实现[转]_第1张图片

功能列表

录音

录音动画

录音播放

英语语音评价(部分实现)

只允许微信客户端打开

零 技术选型

录音方面

可供选择的方案有两个:

使用HTML5接口 -getUserMedia();

微信开放平台-微信JS-SDK;.

通过http://caniuse.com查询getUserMedia()的兼容性。

基于微信JS-SDK的录音与语音评价功能实现[转]_第2张图片

getUserMedia()  API兼容性

由于主要应用场景是在移动端,此API只能在iOS 11+与Android 5-6.X及以上可用,兼容性感人,故舍弃此方案。所以此次录音实现基于微信开放平台提供的微信JS-SDK。

英语语音识别

因为主要是基于微信平台,所以要求语音识别平台需要提供Web Api。

语音识别方面功能,主要有两种技术路线。

专门着力于语音识别及相关产业的技术平台,例如讯飞以及调研中发现的驰声。

优势:专业并且提供语音评测相关功能;

劣势:花费昂贵;

AI开放平台,因为各大厂商布局AI,免费提供语音识别相关的接口。

优势:免费,API清晰;

劣势:并非为专门为教育定制,无语音评测功能;

结合项目的实际情况,决定使用第二种方案。(主要是因为经费有限……)

大厂提供的免费方案主要有:

百度AI

限制:50000次/天免费

格式支持:pcm(不压缩)、wav(不压缩,pcm编码)、amr(压缩格式);固定16k 采样率;

腾讯AI开发平台

语音参数:必须符合16k或8K采样率、16bit采样位数、单声道

语音格式:PCM、WAV、AMR、 SILK

其他:目前只支持汉语

腾讯云智能语音服务

语音参数:必须符合16k或8K采样率、16bit采样位数、单声道

语音格式:通用标准格式,例如 mp3,wma,wav 等

微信公众平台AI开放接口

语音参数:16k,单声道,最大1M

语音格式:mp3

微信公众平台JS-SDK智能接口

其他:目前只支持汉语

大厂竞争果然系列,大鹅厂光语音服务肉眼可见的就折腾了这么多。(大雾)

经过一番折腾,最终可以形成两种方案:

微信JS-SDK音频接口录音->上传到微信临时素材->下载到服务器->转换录音文件格式->百度AI语音识别返回结果->与预置的文件比对->返回比对结果

优势:识别结果准

劣势:慢(因为无法直接获取用户的录音,需要从微信公众平台的临时素材中转,且录音文件格式与百度AI可识别格式不一致,所以整个流程下来太慢);微信公众号需要企业认证

其他:至于为什么不选用腾讯系列,因为腾讯系列语音服务没有调通。。。

微信JS-SDK音频接口录音->调用JS-SDK智能接口返回识别结果->结果转为拼音->与预置的文件比对->返回比对结果

优势:返回结果迅速、方法简单

劣势:识别结果不太准确(因为JS-SDK智能接口不只是单单根据语音直接转换,还会在结果的基础上进行一定程度的联想,话说为啥不能增加个语言选择参数。)

本次整个方案使用方案2

一 微信JS-SDK环境准备

写在前边:此处的开发环境不是指本地的开发环境,单指使用微信JS-SDK所需完成的一系列的获取AccessToken、jsapi_ticket等前置条件。

开发环境

云服务器:腾讯云 · 小程序(特价,买了个折腾)

后台语音:PHP · CodeIgniter(小程序PHP样例使用CI框架)

1)公众号配置

前置的公众号申请等就不再赘述,如果要正常使用微信JS-SDK的功能,需要在公众号配置一些内容。

配置IP白名单

通过微信公众平台开发->基本配置->IP白名单进行设置,将开发环境的IP配置到IP白名单。

注1. 如果不配置白名单将无法获取access_token,并在返回结果中返回40164错误;

注2. 因为是在腾讯云 · 小程序主机开发环境下折腾的,该环境如果一周不更新新的代码会暂时关闭,IP也会发生变化,所以建议每周更新一下代码;

配置JS接口安全域名

通过微信公众平台设置->公众号设置->功能设置->JS接口安全域名进行设置,将JS接口安全域名写入。

注1. 一个公众号最多可以配置3个安全域名,需使用字母、数字及“-”的组合,不支持IP地址、端口号及短链域名,且域名必须经过备案;

注2. 需要将MP_verify_qEwAJiPuWerKftkO.txt(可在配置JS接口安全域名处自行下载)放到配置域名的根目录,并确保可以访问到。腾讯云 · 小程序默认样例使用的CI框架,需要放到\server下;

注3. 如不配置JS接口安全域名,则无法成功调用JS-SDK;

2)获取access_token

access_token是公众号的全局唯一接口调用凭据,调用公众号的各个接口时都需要使用。获取access_token需要appid与appsecret。微信公众平台的access_token有效期为7200s (2小时),每天最高可调用上限为2000次。因此获取access_token需要做到:

定时刷新(刷新间隔大于1分钟,小于120分);

全局缓存access_token;

Show me the code

classclassNameextendsCI_Controller{var$appId ="appId";var$appSecret ="appSecret";var$accessTokenFile ="wxtoken.txt";// var $jsapiTicketFile = "wxjsapiTicket.txt";publicfunctionindex(){$this- > build_access_token();//获取access_token// $this - > get_jsapi_ticket(); //获取jsapic_ticket}publicfunctionbuild_access_token(){    $ch = curl_init();//初始化一个CURL对象curl_setopt($ch, CURLOPT_URL,"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$this->appId}&secret={$this->appSecret}");    curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);    $data = json_decode(curl_exec($ch));if($data - > access_token) {      $token_file = fopen($this- > accessTokenFile,"w")ordie("Unable to open file!");//打开wxtoken.txt文件,没有会新建fwrite($token_file, $data - > access_token);//重写wxtken.txt全部内容fclose($token_file);//关闭文件流}else{echo$data - > errmsg;    }    curl_close($ch);  }publicfunctionread_token(){    $token_file = fopen($this- > accessTokenFile,"r")ordie("Unable to open file!");    $rs = fgets($token_file);    fclose($token_file);return$rs;  }}

Talk is cheap

因为使用的是CI框架,将文件写到server\application\controllers\下可直接通过域名+文件名访问到该接口,默认执行文件中的index中的方法;

代码中的基本逻辑通过build_access_token()方法获取access_token,并存储到wxtken.txt,通过read_token()方法读取access_token;

获取access_token的详细情况见官方API。

3)获取jsapi_ticket

jsapi_ticket是公众号用于调用微信JS接口的临时票据,通过access_token来获取。微信公众平台的jsapi_ticket有效期为7200s (2小时),每天最高可调用上限为1000000次。因此同样在全局缓存。

Show me the code

publicfunctionget_jsapi_ticket(){  $access_token =$this- > read_token();  $ch = curl_init();//初始化一个CURL对象curl_setopt($ch, CURLOPT_URL,"https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={$access_token}&type=jsapi");  curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);  $data = json_decode(curl_exec($ch));if($data - > ticket) {    $token_file = fopen($this- > jsapiTicketFile,"w")ordie("Unable to open file!");    fwrite($token_file, $data - > ticket);    fclose($token_file);//关闭文件流}else{echo$data - > errmsg;  }  curl_close($ch);}publicfunctionread_jsapi_ticket(){  $jsapi_ticket_file = fopen($this- > jsapiTicketFile,"r")ordie("Unable to open file!");  $rs = fgets($jsapi_ticket_file);  fclose($jsapi_ticket_file);return$rs;}

Talk is cheap

写到跟获取access_token同一文件中,以便同时刷新;

同之前的代码中逻辑类似,通过get_jsapi_ticket()方法获取jsapi_ticket,并存储到wxjsapiTicket.txt,通过read_jsapi_ticket()方法读取jsapi_ticket;

获取access_token的详细情况见官方API。

4)刷新access_token及jsapi_ticket

由于微信公众平台的access_token与jsapi_ticket有两个小时有效期,故需要定期刷新。基本思路有如下三个:

PHP定时执行任务;

服务器定时任务;

定时访问URL;

1.PHP定时执行任务

主要使用死循环,执行一次时间,使用sleep()函数休眠一段时间,如下代码:

ignore_user_abort();//即使Client断开(如关掉浏览器),PHP脚本也可以继续执行.set_time_limit(0);//执行时间为无限制,php默认的执行时间是30秒,通过set_time_limit(0)可以让程序无限制的执行下去$interval=60*100;//每隔100分钟运行do{//do sthsleep($interval);//按设置的时间等待100分钟循环执行}while(true);

缺点:缺点严重,启动之后,无法控制。而且一直消耗服务器资源,容易被杀死;

2.服务器定时任务

windows平台的计划任务或者是Unix平台的Crontab都有定时执行php脚本或者访问URL的方法,但是由于使用的腾讯云 · 小程序使用的是Wafer一体化解决方案,无法直接访问远端服务器,故此方法放弃。

3. 定时访问URL

我们这次定时刷新access_token及jsapi_ticket采用的就是此方法,腾讯云平台,有个免费的功能云拨测可定时访问某个URL,并且在无法访问时,将预警信息发送给某个设定好的用户组。

将我们之前写好的获取access_token及jsapi_ticket方法,写到index()方法下,将URL填到拨测地址中,定时刷新,搞定。

注1. 云拨测最长的周期为半个小时,而且每次拨测可能访问地址5-6次,其实更稳妥的方法是在数据库中设置标志位,防治过度刷新,但是每天2000次的限额完全够用,就暂时未做此功能。

5)生成JS-SDK配置信息

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用,配置信息需要的参数如下:

wx.config({debug:true,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId:'',// 必填,公众号的唯一标识timestamp: ,// 必填,生成签名的时间戳nonceStr:'',// 必填,生成签名的随机串signature:'',// 必填,签名jsApiList: []// 必填,需要使用的JS接口列表});

其中的appId、jsApiList已知,timestamp、nonceStr动态生成,signature由算法生产。其中关于signature的算法官方API描述如下:

签名算法

签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。

即signature=sha1(string1)。 示例:

noncestr=Wm3WZYTPz0wzccnW

jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg

timestamp=1414587457

url=http://mp.weixin.qq.com?params=value

步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:

jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW×tamp=1414587457&url=http://mp.weixin.qq.com?params=value

步骤2. 对string1进行sha1签名,得到signature:

0f9de62fce790f9a083d5c99e95740ceb90c27ed

注意事项

签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。

签名用的url必须是调用JS接口页面的完整URL。

出于安全考虑,开发者必须在服务器端实现签名的逻辑。

Show me the code

publicfunctionget_signpackage(){  $jsapi_ticket =$this->read_jsapi_ticket();    $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !=='off'|| $_SERVER['SERVER_PORT'] ==443) ?"https://":"http://";  $url ="$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";// 注意 URL 一定要动态获取,不能 hardcode.$noncestr =$this->createNonceStr();  $timestamp = time();  $trs_url =$this->input->post('trs_url');  $url =isset($trs_url)?$trs_url:$url;  $string1 ="jsapi_ticket={$jsapi_ticket}&noncestr={$noncestr}×tamp={$timestamp}&url={$url}";  $signature = sha1($string1);$this->json(['appId'=>$this->appId,'nonceStr'=> $noncestr,'timestamp'=> $timestamp,'signature'=> $signature,'url'=> $url  ]);// return $signPackage;}privatefunctioncreateNonceStr($length =16){  $chars ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";  $str ="";for($i =0; $i < $length; $i++) {    $str .= substr($chars, mt_rand(0, strlen($chars) -1),1);  }return$str;}

Talk is cheap

一定要注意,签名用的url必须是调用JS接口页面的完整URL,这里通过前端POST获取调用页的URL;

返回值为json格式,前端通过ajax获取;

因为采用了CI框架,前端ajax请求地址为域名/weapp/此段代码的文件名/get_signpackage。

至此,使用微信公众平台JS-SDK的前置条件均已准备完毕。

二 实现录音按钮动画

基本的交互逻辑如下图演示:

基于微信JS-SDK的录音与语音评价功能实现[转]_第3张图片

此处来实现长按录音按钮的动画。基本思路是:

通过CSS3的transition属性实现record突变的平滑变小、平滑变大;

通过CSS3的keyframes动画与伪类配合完成环形进度动画;

Show me the code

.voice-remote{border-radius:50%;width:4rem;height:4rem;overflow: hidden;position: absolute;background:#f6f6f6;bottom:1.5rem;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);transition: all .2s;-webkit-transition: all .2s;}.voice-remote:active{width:4.5rem;height:4.5rem;bottom:1rem;border:1pxsolid#e7e7e7;}.voice-remote:before{content:"";width:100%;height:100%;position: absolute;z-index:2;top:0;left:0;border-radius:50%;background-image:linear-gradient(-90deg, transparent 50%, #1dc61c 50%);}.voice-remote:after{content:"";width:100%;height:100%;position: absolute;z-index:3;bottom:0;left:0;border-radius:50%;background-image:linear-gradient(-90deg, transparent 50%, #1dc61c 50%);}.voice-remote.cover{position: absolute;border-radius:50%;width:100%;height:100%;z-index:4;top:0;left:0;background-image:linear-gradient(-90deg, transparent 50%, #f6f6f6 50%);}.voice-remote.icon{position: absolute;width:100%;height:100%;top:0;left:0;background:#f6f6f6url(../../images/voice.png) no-repeat center center;background-size:100%;border-radius:50%;z-index:5;}.voice-remote.icon:active{width:80%;height:80%;top:10%;left:10%;background-size:100%;}.voice-remote:active:before{-webkit-animation: scoll linear30s;animation: scoll linear30s;-webkit-animation-fill-mode: forwards;animation-fill-mode: forwards;}.voice-remote:active:after{-webkit-animation: xscoll linear60s;animation: xscoll linear60s;-webkit-animation-fill-mode: forwards;animation-fill-mode: forwards;}.voice-remote:active.cover{-webkit-animation: hide linear60s;animation: hide linear60s;-webkit-animation-fill-mode: forwards;animation-fill-mode: forwards;}@-webkit-keyframesscoll {    0% {-webkit-transform:rotate(0deg);    }    100% {-webkit-transform:rotate(180deg);    }}@keyframesscoll {    0% {transform:rotate(0deg);    }    100% {transform:rotate(180deg);    }}@-webkit-keyframesxscoll {    0% {-webkit-transform:rotate(0deg);    }    100% {-webkit-transform:rotate(360deg);    }}@keyframesxscoll {    0% {transform:rotate(0deg);    }    100% {transform:rotate(360deg);    }}@-webkit-keyframeshide {    0% {opacity:1}    49.9% {opacity:1;    }    50% {opacity:0;    }    100% {opacity:0;    }}@keyframeshide {    0% {opacity:1}    49.9% {opacity:1;    }    50% {opacity:0;    }    100% {opacity:0;    }}

Talk is cheap

基于微信JS-SDK的录音与语音评价功能实现[转]_第4张图片

录音按钮原理图

录音按钮动画原理如上图分层,其中:before层添加动画为旋转180度,时间为30s,与此同时:after层添加动画为旋转360度,时间为60s,即前30s两个图层同时旋转,当30s后:after层继续旋转,:before层保持位置不变,使整个右侧环形显示。.cover层添加动画为前30s覆盖整个左侧,后30s隐藏。 整个动画由最顶部.icon覆盖,使整个动画过程显示为一个环形。

三 实现录音及录音播放功能

开始是实现录音及播放的相关功能。主要流程是引入JS文件、通过config接口注入权限验证配置、通过ready接口处理成功验证、撰写录音代码逻辑、撰写录音播放代码逻辑。

1)引入JS文件

在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.2.0.js

Show me the code

requirejs.config({baseUrl:'./lib/js',paths: {'jquery':'jquery','jweixin':'jweixin','util':'util','post_data':'data','pinyin_dict_notone':'pinyin_dict_notone','pinyinUtil':'pinyinUtil',    }});define(['jquery','jweixin','post_data','util','pinyin_dict_notone','pinyinUtil'],function($, wx){}

Talk is cheap

此次使用AMD模式requirejs引入相关文件;

这里引入多个文件,之后的代码需要使用;

注1. 支持使用 AMD/CMD 标准模块加载方法加载,也支持直接使用直接引用;

注2. 调用之前需要完成配置JS接口安全域名。

2)通过config接口注入权限验证配置

通过ajax请求之前完成的生成JS-SDK配置信息接口,获取到相关的配置内容,另外jsApiList接口列表需要根据业务需求自行添加。

Show me the code

$.ajax({url:"your js-sdk interface",dataType:"json",contentType:"application/x-www-form-urlencoded; charset=utf-8",data:{"trs_url":window.location.href.split("#")[0]},type:"POST",success:function(data){varbaseWxData = data;        wx.config({debug:false,// 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。appId: baseWxData['appId'],// 必填,公众号的唯一标识timestamp: baseWxData['timestamp'],// 必填,生成签名的时间戳nonceStr: baseWxData['nonceStr'],// 必填,生成签名的随机串signature: baseWxData['signature'],// 必填,签名,见附录1jsApiList: ['startRecord',// 录音开始api'stopRecord',// 录音结束api'onVoiceRecordEnd',// 超过一分钟自动停止api'playVoice',// 播放录音api'pauseVoice',// 暂停录音api'stopVoice',// 停止播放api'onVoicePlayEnd',// 监听语音播放完毕api'translateVoice']        });    }});

Talk is cheap

用post传入当前页面URL,因为签名算法必须是使用调用页的地址;

此次功能只用到如代码中的几个API,更多API详见官方API;

3)通过ready接口处理成功验证

wx.ready(function(){// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。});

4)撰写录音代码逻辑

创建一个对象R,封装录音、播放以及翻译等过程。监听录音按钮的touchstart事件启动录音,监听touchend时间结束录音。

Show me the code

/* Javascript Code*/varR = {options: {spoint:0,//记录recordstart时间tpoint:0,//记录touchstart时间epoint:0,//记录touchend时间timer:0,//setIntervaliOrder:0//记录录音序列order},feedback: {great: ["Excellent!","Well done!","口语不错嘛!","非常棒!","Great"],good: ["Good job!","Not bad!","还不错哦!","Good! Keep going!","干得不错!加油"],normal: ["Please try again!","Oh,you can do better than that!","分数有点儿低哦!","再来一次试试!","Have another try,please!"]    },recode:function(){//定时最长60s后结束录音R.options.timer = setInterval(function(){vartime = +newDate() - R.options.spoint;if(time >=60000) {                alert("时间超过60秒,请再次录制!");                setTimeout(function(){                    R.translate();                },100);                clearInterval(R.options.timer);            }        },1000);    },translate:function(){//结束录音并识别语音wx.stopRecord({success:function(res){                localId = res.localId;                $(".voice-remote").addClass("vrPause");                wx.translateVoice({localId: localId,complete:function(res){}                });            },fail:function(res){                alert(JSON.stringify(res));            }        });    },insertContent:function(obj){var_str ="";switch(obj.iType) {case1:                _str ='

'+ obj.iContent +'
';break;case2:                _str ='
'break;case3:break;case4:break;default:console.log('Undefined element of iType :'+ iType);        }        $("#dialogBox").append(_str).scrollTop($("#dialogBox")[0].scrollHeight);    },init:function(){        R.insertContent({iType:1,iContent: word.keyword[R.options.iOrder].content,        });// $.ajax();wx.ready(function(){            $('.voice-remote').on('touchstart',function(e){                $(".playing").each(function(){                    _stoplocalId = $(this).data("localid");                    $(this).removeClass("playing");                    wx.stopVoice({localId: _stoplocalId                    });                });                R.options.tpoint = +newDate();//记录touchstart时间点wx.startRecord({success:function(){                        $('.voice-remote').addClass('active');                        R.options.spoint = +newDate();//记录开始录音成功时间点R.recode();//启用定时结束录音定时器if(R.options.spoint > R.options.epoint && R.options.epoint > R.options.tpoint) {//处理因为短按,startRecord还未初始成功,导致无法正常停止录音clearInterval(R.options.timer);                            $('.voice-remote').removeClass('active');                        }                    },fail:function(res){                        alert(JSON.stringify(res));                    },cancel:function(){                        alert('您拒绝了授权录音');                    }                });            });document.oncontextmenu =function(e){// 阻止部分手机长按会产生弹出框的问题e.preventDefault();            };            $('.voice-remote').on('touchend',function(){                R.options.epoint = +newDate();//记录touchend时间点$(this).removeClass('active');vartime = +newDate() - R.options.spoint;if(time <60000) {//当录音间隔时间小于60s,touchend后清除定时结束录音定时器,并调用结束录音方法setTimeout(function(){                        R.translate();                    },200);                    clearInterval(R.options.timer);                }            });            $(document).on('touchstart','.iPlayVoice',function(){// do sth});            wx.onVoicePlayEnd({complete:function(res){// do sth}            });        });    }}R.init();

/* CSS Code*/.setHide{display: none;}.content{background:#ebebeb;width:100%;height:100%;overflow: hidden;font-family: Microsoft YaHei;}.dialogBox{margin:3%;width:94%;height:81%;overflow-y: scroll;}.dialogItem{margin:3%0;overflow: hidden;text-align: left;}.avatarBox{display: inline-block;}.contentBox{display: inline-block;max-width:68%;margin-left:12px;}.wordBox:before{content:"";width:12px;height:25px;background:url(../../images/sharpOther.png)00no-repeat;position: absolute;top:50%;margin-top: -12px;left: -12px;}.wordBox{border:1pxsolid#d4d4d4;background-color:#fff;padding:5px10px;display: inline-block;vertical-align: middle;-webkit-border-radius:5px;border-radius:5px;position: relative;min-height:40px;line-height:40px;vertical-align: middle;text-align: left;}.wordBox>span{line-height:1.5em;display: inline-block;vertical-align: middle;text-align: justify;}.avatar{width:40px;vertical-align: middle;}.sharpStyle{width:17px;height:35px;background:url(../../images/sharpOther.png)00no-repeat;display: inline-block;margin-left:6px;vertical-align: middle;}.sharpMe{background-image:url(../../images/sharpMe.png);margin-left:0;margin-right:6px;}.sound{display: inline-block;width:18px;height:25px;background:url(../../images/sound.png)00no-repeat;background-size:100%100%;}.playing.sound{background-image:url(../../images/sound.gif);}.p2{text-align: right;}.p2.contentBox{margin-left:0;margin-right:12px;}.p2.wordBox{border:1pxsolid#86b850;background-color:#a1e75b;}.p2.wordBox:before{background:url(../../images/sharpMe.png)00no-repeat;left: auto;right: -12px;}.p2.sound{background-image:url(../../images/soundMe.png);}.p2.playing.sound{background-image:url(../../images/soundMe.gif);}.dialogItem.contentBox:after{color:#969696;margin-left:3px;}.dialogItem.contentBox:before{color:#969696;margin-right:3px;}.isSound.contentBox{width:68%;}.p2.isSound.wordBox{text-align: right;}.soundOt1.wordBox{width:15%;}.soundOt2.wordBox{width:16%;}/*……*/.soundOt1.contentBox:after{content:"1 ''";}.soundOt2.contentBox:after{content:"2 ''";}/*……*/.soundMe1.contentBox:before{content:"1 ''";}.soundMe2.contentBox:before{content:"2 ''";}/*……*/.soundMe1.wordBox{width:15%;}.soundMe2.wordBox{width:16%;}/*……*/

Talk is cheap

构建了insertContent()方法构建页面,使用scrollTop()方法使填充的新的对话框出现再最下边;

构建了spoint与epoint两个参数,判断录音时间;

构建recode()方法,使用setInterval()方法,限制录音超过60s后停止(因为微信JS-SDK限制录音时长最多为60s);

构建feedback参数,为之后翻译提供反馈;

使用伪类实现对话前后的音频时长;

已知兼容性问题

部分华为手机,长按后弹出弹出菜单,检测document的oncontextmenu事件,阻止默认事件e.preventDefault();

微信开发者工具调试时,超过60s后会因为alert()会触发一次touchend事件,真正抬手后又会触发一次touchend,真机运行时无此问题;

5)撰写录音播放代码逻辑

在构建页面时将localid写到对应对话语句中,通过该localid对应相应的录音。

Show me the code

$(document).on('touchstart','.iPlayVoice',function(){var$this= $(this),        _localId = $this.data("localid");if($this.hasClass("playing")) {        wx.stopVoice({localId: _localId        });        $this.removeClass("playing");    }else{        $(".playing").not($this).each(function(){            _stoplocalId = $(this).data("localid");            $(this).removeClass("playing");            wx.stopVoice({localId: _stoplocalId            });        });        wx.playVoice({localId: _localId        });        $this.addClass("playing");    }});wx.onVoicePlayEnd({complete:function(res){        $(".playing").removeClass("playing");    }});

Talk is cheap

使用$(document).on('touchstart', '.iPlayVoice', function() {})为.iPlayVoice动态绑定事件;

使用playing类名,控制播放时的状态;

四 实现语音评价功能

开篇的技术选型时已经将前因后果说明了。现在就写借助微信JS-SDK中的wx.translateVoice()方法实现语音评价功能的具体实现。具体流程为引入示例json、获取语音翻译结果、语音结果转为拼音、结果比对、反馈评价。

1)引入示例json

将示例的数据写成json,用requirejs引入。

Show me the code

varword = {keyword: [{order:1,content:"请说:
What's your name.",matched:"我次要儿内幕,我想那,我次有那么",localId:"-1"}, {order:2,content:"请说:
How are you.",matched:"好啊有",localId:"-1"}, {order:3,content:"请说:
Nice to meet you.",matched:"挨次图密特油",localId:"-1"}],}

Talk is cheap

content数据项,标识的是引导语;

matched项标识的是匹配内容,通过“,”分隔多个匹配内容,以提高匹配度;

2)获取语音翻译结果

Show me the code

wx.translateVoice({localId:'',// 需要识别的音频的本地Id,由录音相关接口获得isShowProgressTips:1,// 默认为1,显示进度提示success:function(res){        alert(res.translateResult);// 语音识别的结果}    fail:function(res){        alert(JSON.stringify(res));    }});

Talk is cheap

翻译接口主要依靠localId来完成一系列的工作,成功后返回一段json格式的数据。

3)语音结果转为拼音

此步骤主要将返回的内容转换成拼音。借助的是@sxei(小茗同学)的一个库,地址为github。

因为只需要转换成无声掉的拼音,那么只需要引入pinyin_dict_notone.js与pinyinUtil.js两个文件,使用pinyinUtil.getPinyin('')方法将汉字转化成拼音。

4)结果比对

比对语音翻译的拼音与预置的信息的拼音进行比对,返回匹配程度。因为预置的结果有多个,取其中匹配程度最高的的一项。

Show me the code

varstr_User = pinyinUtil.getPinyin(res.translateResult.split("。")[0]),    str_Ans = word.keyword[R.options.iOrder].matched.split(","),    matchedArray =newArray(),    matchedNum =0;for(vari =0; i < str_Ans.length; i++) {    matchedArray[i] = strSimilarity2Percent(Trim(str_User), Trim(pinyinUtil.getPinyin(str_Ans[i])));}matchedNum = arrayMax(matchedArray);

Talk is cheap

返回的json数据,返回结果的key为translateResult;

返回的结果有“。”,故需要使用res.translateResult.split("。")[0]将“。”排除;

使用了三个自定义方法,strSimilarity2Percent()返回匹配程度、Trim()排除字符串中的空格、arrayMax()返回数组中的最大值。相关方法存放在unit.js中;

/**

* 两个字符串的相似程度,并返回相差字符个数

*

*

* @param    {string}  s  字符串1

* @param    {string}  t  字符串2

* @returns  {number}  d[n][m]  字符串差异个数

*

* @date    2018-03-02

* @author  ReeCode

*/functionstrSimilarity2Number(s, t){varn = s.length,    m = t.length,    d = [];vari, j, s_i, t_j, cost;if(n ==0)returnm;if(m ==0)returnn;for(i =0; i <= n; i++) {    d[i] = [];    d[i][0] = i;  }for(j =0; j <= m; j++) {    d[0][j] = j;  }for(i =1; i <= n; i++) {    s_i = s.charAt(i -1);for(j =1; j <= m; j++) {      t_j = t.charAt(j -1);if(s_i == t_j) {        cost =0;      }else{        cost =1;      }      d[i][j] = Minimum(d[i -1][j] +1, d[i][j -1] +1, d[i -1][j -1] + cost);    }  }returnd[n][m];}/**

* 两个字符串的相似程度,并返回相似度百分比

*

*

* @param    {string}  s  字符串1

* @param    {string}  t  字符串2

* @returns  {number}    字符串差异百分比

*

* @date    2018-03-02

* @author  ReeCode

*/functionstrSimilarity2Percent(s, t){varl = s.length > t.length ? s.length : t.length;vard = strSimilarity2Number(s, t);return(1- d / l).toFixed(4);}functionMinimum(a, b, c){returna < b ? (a < c ? a : c) : (b < c ? b : c);}/**

* 去除字符串中的空格

*

* 去除字符串中的空格,

* 如果不加参数"g",只去除字符串前后空格;

* 如果加参数"g",去除字符串全部空格;

*

* @param    {string}  str        目标字符串

* @param    {string}  is_global  是否检测整个字符串,如果是,输入为 "g",其他情况无视该参数

* @returns  {string}

*

* @date    2018-03-02

* @author  ReeCode

*/functionTrim(str, is_global){varresult,    _is_global = (typeof(is_global) !=="undefined") ? is_global :"n";  result = str.replace(/(^\s+)|(\s+$)/g,"");if(_is_global.toLowerCase() =="g") {    result = result.replace(/\s/g,"");  }returnresult;}/**

* 获取字符串的长度

*

* 获取字符串的长度,

* 汉字为两个字符长度,英语级其他符号为1个长度;

*

* @param    {string}  val        目标字符串

* @returns  {number}

*

* @date    2018-03-05

* @author  ReeCode

*/functiongetByteLen(val){varlen =0;for(vari =0; i < val.length; i++) {vara = val.charAt(i);if(a.match(/[^\x00-\xff]/ig) !=null) {      len +=2;    }else{      len +=1;    }  }returnlen;}/**

* 移除数组中的某個元素  (改变数组长度)

*

*

* @param    {array}  arr        目标数组

* @param    {any}    item      要从数组中移除的元素

* @returns  {array}

*

* @date    2018-03-06

* @author  ReeCode

*/functionremoveWithoutCopy(arr, item){for(vari =0; i < arr.length; i++) {if(arr[i] == item) {      arr.splice(i,1);      i--;    }  }returnarr;}/**

* 找出数组中的最小值

*

*

* @param    {array}  arr      目标数组

* @returns  {number} min      数组最小值

*

* @date    2018-04-19

* @author  ReeCode

*/functionarrayMin(arr){varmin = arr[0],    len = arr.length;for(vari =1; i < len; i++) {if(arr[i] < min) {      min = arr[i];    }  }returnmin;}/**

* 找出数组中的最大值

*

*

* @param    {array}  arr      目标数组

* @returns  {number} max      数组最小值

*

* @date    2018-04-19

* @author  ReeCode

*/functionarrayMax(arr){varmax = arr[0],    len = arr.length;for(vari =1; i < len; i++) {if(arr[i] > max) {      max = arr[i];    }  }returnmax;}

5)反馈评价

根据评价结果的情况,分为三档:

matchedNum >= 0.8 ----------great

0.8 > matchedNum >= 0.6 --good

matchedNum < 0.6  -----------normal

同时在此时对小于0.5s的录音予以忽略。

Show me the code

translate:function(){//结束录音并识别语音wx.stopRecord({success:function(res){            localId = res.localId;            $(".voice-remote").addClass("vrPause");            wx.translateVoice({localId: localId,complete:function(res){varvoice_time =Math.abs(R.options.epoint - R.options.point),                        _iClass ="soundMe"+Math.round(voice_time /1000);if(res.hasOwnProperty('translateResult') && voice_time >500) {varstr_User = pinyinUtil.getPinyin(res.translateResult.split("。")[0]),                            str_Ans = word.keyword[R.options.iOrder].matched.split(","),                            matchedArray =newArray(),                            matchedNum =0;for(vari =0; i < str_Ans.length; i++) {                            matchedArray[i] = strSimilarity2Percent(Trim(str_User), Trim(pinyinUtil.getPinyin(str_Ans[i])));                        }                        matchedNum = arrayMax(matchedArray);                        R.insertContent({iType:2,iClass: _iClass,iContent: localId,                        });if(matchedNum >=0.8) {                            R.options.iOrder++;                            alert(R.feedback.great[parseInt(Math.random() *5)] +"\r\n 您本次录音匹配程度为:"+ (matchedNum *100).toFixed(2) +"% 。");if(R.options.iOrder < word.keyword.length) {                                R.insertContent({iType:1,iContent: word.keyword[R.options.iOrder].content,                                });                            }else{                                alert("恭喜,本次测试完成!");                            }                        }elseif(matchedNum >=0.6) {                            alert(R.feedback.good[parseInt(Math.random() *5)] +"\r\n 您本次录音匹配程度为:"+ (matchedNum *100).toFixed(2) +"%!");                        }else{                            alert(R.feedback.normal[parseInt(Math.random() *5)] +"\r\n 您本次录音匹配程度为:"+ (matchedNum *100).toFixed(2) +"%!");                        }                    }elseif(voice_time >500) {                        alert('无法识别');                    }elseif(voice_time <=500) {                        alert("录音过短,请重新录音!");                    }                }            });        },fail:function(res){            alert(JSON.stringify(res));        }    });},

Talk is cheap

使用parseInt(Math.random() * 5)生成随机数,使反馈语随机生成;

五 限制只允许微信客户端打开

检测客户端版本的micromessenger值,微信用的是浏览器内核是这个。

Show me the code

/**

* 判断是否是微信

*

* @returns  {boolen}      true 是微信  false  不是微信

*

* @date    2018-05-29

* @author  ReeCode

*/functioniswx(){varua = navigator.userAgent.toLowerCase();returnua.indexOf('micromessenger') !=-1?true:false;}if(!iswx()) {document.head.innerHTML ='抱歉,出错了';document.body.innerHTML ='

请在微信客户端打开链接

';    }else{        R.init();    }

Talk is cheap

判断如果是微信浏览器,对对象R进行初始化,如果不是,返回请在微信客户端打开;

总结

絮絮叨叨终于总结好了。过段时间用小程序对该功能进行重写。

小礼物走一走,来关注我

作者:ReeCode

链接:https://www.jianshu.com/p/e317c74cdd8c

來源:

著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的:(基于微信JS-SDK的录音与语音评价功能实现[转])