您的位置:首页 - 文章 -  - 正文

5.语音板使用科大讯飞语音唤醒

0x00 科大讯飞唤醒引擎介绍

语音唤醒是设备(手机、玩具、家电等)在休眠或锁屏状态下也能检测到用户的声音(设定的语音指令,即唤醒词),让处于休眠状态下的设备直接进入到等待指令状态,开启语音交互第一步。科大讯飞的唤醒引擎有什么优势呢?可以应用在哪些场景中呢?以下是从科大讯飞官网上获取到的信息,供大家参考:

科大讯飞唤醒引擎优势及应用场景

0x01 下载语音唤醒SDK

我们在使用讯飞的唤醒引擎服务之前,需要前往讯飞开放平台注册一个账户。然后登录账户后就可以前往自己的控制台,创建一个应用。创建这个应用以后,我们就可以得到一个调用科大讯飞各项语音服务的AppID。这个AppID是很重要的,它会跟我们后面下载的各项语音服务是打包在一起使用的。也就是说你下载的语音SDK必须要搭配着你的AppID来使用,否则可能调用就会出错。那注册科大讯飞开放平台账户是比较简单的,这里不再做介绍,大家可以直接去下面的讯飞开放平台上注册即可:

https://www.xfyun.cn/

那现在注册账户的话,还是会有各种免费的资源服务可以使用,官网页面的可以免费使用的服务如下:

讯飞开放平台官网
个人账户免费套餐

那注册账户后,我们就可以登录自己的控制台。这里要做的第一件事就是“创建新应用”,然后就可以准备使用“语音唤醒”服务了:

创建应用

当点击“语音唤醒”进入服务后,这里我们就可以设置自己唤醒词和下载对应的语音唤醒SDK了,如下图所示:

设置唤醒词

这里我们在提交唤醒词前,需要先使用“唤醒词评估小工具”来评估下你想设置的唤醒词效果。当评估效果好以后,我们再提交。如下图所示:

各唤醒词唤醒效果评估

可以看出来这里的“阿里巴巴”唤醒词效果就不好,只有四颗星,我们最好找五颗星的唤醒词。这里特别需要注意唤醒词设置的规则:

1.音节覆盖尽量多,长度最少为四个音节,相邻音节要规避,字要发音清晰响度大;

2.尽可能选择日常不容易出现的短语,可以有效降低误唤醒率。例如:“凯越在线”就是一个高质量唤醒词,它的音节覆盖多,差异大,而且平时较为不常说。质量较差的唤醒词:“语音在线”,前两个音节相近,不是一个质量高的唤醒词。

3.英文唤醒词仅支持有限的词库,不可超出词典范围,点击下载英文唤醒词典

当我们设置好自己的唤醒词后,接下来就可以进入下载SDK的页面了,这里需要注意选择Linux平台,如下图所示:

语音唤醒SDK下载

0x02 测试语音唤醒效果

当我们下载好自己设置的唤醒词SDK后,我们就可以将该SDK发送到树莓派板上。然后就可以解压,查看源码了。我们可以在本地电脑上使用如下命令,将下载的SDK发送到树莓派上:

scp Linux_awaken1226_5d5b9efd.zip corvin@192.168.*.*:~/

这里需要注意的是后面的IP地址,大家需要根据自己树莓派的IP地址来修改这条命令就可以了。执行完这条命令就可以将文件传输到树莓派的home目录下,注意我这里的树莓派系统用户名是corvin。这里需要注意的就是解压zip文件的命令,完整解压命令如下:

unzip -q Linux_awaken1226_5d5b9efd.zip -d xf_awaken/

查看语音唤醒源码组成

接下来最重要的一件事就是来替换默认代码中提供的唤醒库了,因为默认提供的都是x86版本的,都是在我们的台式机这样电脑上使用的。因此这里需要使用树莓派版本的库才能正常的在树莓派上进行编译和使用语音唤醒,如果你已经向科大讯飞申请了树莓派版本的唤醒库,那这里就可以替换了。如果你没有的话,也可以下载我申请好的树莓派库,但是由于唤醒库和AppID绑定的。如果大家下载使用的话,会占用我的语音唤醒装机量的。所以大家要想下载我的版本树莓派库的话,是需要收费的,希望可以理解:

隐藏内容需要支付: ¥30
立即购买 升级VIP

这里的语音库只要下载好以后,以后都是永久使用的。当下载好该动态库后,我们就可以将其放到源码libs目录中备用了,具体操作如下:

放置树莓派版本动态库

当将动态库放置好以后,我们就可以来准备编译代码了,不过这里需要修改下才能开始编译。这里首先我们需要修改下32bit_make.sh这个脚本,修改也很简单就是将动态链接库的地址改成我们刚才修改的就行了,这里就是将export LD_LIBRARY_PATH=$(pwd)/../../libs/x86/最后的x86删除即可,最终的32bit_make.sh代码如下:

1#编译32位可执行文件
2make clean;make
3#设置libmsc.so库搜索路径
4export LD_LIBRARY_PATH=$(pwd)/../../libs/

接下来就是修改Makefile文件了,这里也是将链接的动态库地址改一下就可以了,主要就是修改LDFLAGS := -L$(DIR_LIB)/x86。将最后的x86删除就可以了。最终Makefile代码如下:

 1#common makefile header
2
3DIR_INC = ../../include
4DIR_BIN = ../../bin
5DIR_LIB = ../../libs
6
7TARGET    = awaken_offline_sample
8BIN_TARGET = $(DIR_BIN)/$(TARGET)
9
10CROSS_COMPILE = 
11CFLAGS = -g -Wall -I$(DIR_INC)
12
13ifdef LINUX64
14LDFLAGS := -L$(DIR_LIB)/x64
15else
16LDFLAGS := -L$(DIR_LIB)/
17endif
18LDFLAGS += -lmsc -lrt -ldl -lpthread -lstdc++
19
20OBJECTS := $(patsubst %.c,%.o,$(wildcard *.c))
21
22$(BIN_TARGET) : $(OBJECTS)
23    $(CROSS_COMPILE)gcc $(CFLAGS) $^ -o $@ $(LDFLAGS)
24
25%.o : %.c
26    $(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@
27clean:
28    @rm -f *.o $(BIN_TARGET)
29
30.PHONY:clean
31
32#common makefile foot

修改完这些代码,我们就可以来准备编译了。但是这里需要注意的是,科大讯飞提供的测试语音唤醒的demo程序比较低级。需要我们将录制好的语音文件放到指定的audio目录下,然后启动语音唤醒检测程序,这是程序会检测语音文件中是否包含有唤醒词。有的话就会有日志提示,整个操作过程如下所示,此时我们还没有录制语音文件:

编译后开始运行语音唤醒

这里我们可以从源码中找到我们需要录制的文件格式和文件名,这里打开awaken_offline_sample.c文件可以找到,如下图所示:

语音文件路径和名称

这里我们可以在audio目录中,使用如下命令来录制pcm格式的语料,然后我们就可以来运行程序来检测是否包含有唤醒词了,录制语料的命令如下:

arecord -d 3 -r 16000 -c 1 -t wav -f S16_LE awake.pcm

这里需要注意的是arecord命令后面的-d参数是表示录制3秒钟,所以我们执行完这条命令后需要立刻说出需要检测的唤醒词,到达3秒后,录音就自动结束了。

录制唤醒测试语料文件

当录制好测试语料后,我们就可以来运行唤醒测试程序了。当出现下图所示的日志,就说明唤醒程序已经检测到唤醒词了:

检测到唤醒词

这里需要注意的是唤醒结果中提示的各字段值,各字段值的意义如下图所示:

唤醒结构各字段参数解释

那什么样的日志打印是没有检测到唤醒词呢?如下图所示:

没有从语料中检测到唤醒词

那下面我通过视频给大家演示下整个过程,从录制唤醒语料开始,然后运行唤醒检测程序,这样大家会更清楚这个操作过程,视频如下:

测试语音唤醒

0x03 修改唤醒程序

通过上面的测试唤醒过程,大家就可以知道这个demo程序有点不够完善。它无法实现实时的检测唤醒词功能,每次都是要录制好测试语料,然后再运行唤醒检测程序。这样就很不方便了,那我们这里就来修改一下,使该demo程序可以像snowboy测试程序那样,可以实时的检测唤醒词。首先我们来看下原始的唤醒程序的源码:

  1#include <stdlib.h>
2#include <stdio.h>
3#include <unistd.h>
4#include <errno.h>
5
6#include "../../include/msp_cmn.h"
7#include "../../include/qivw.h"
8#include "../../include/msp_errors.h"
9
10#define IVW_AUDIO_FILE_NAME "audio/awake.pcm"
11#define FRAME_LEN    640 //16k采样率的16bit音频,一帧的大小为640B, 时长20ms
12
13
14void sleep_ms(int ms)
15
{
16    usleep(ms * 1000);
17}
18
19int cb_ivw_msg_procconst char *sessionID, int msg, int param1, int param2, const void *info, void *userData )
20
{
21    if (MSP_IVW_MSG_ERROR == msg) //唤醒出错消息
22    {
23        printf("\n\nMSP_IVW_MSG_ERROR errCode = %d\n\n", param1);
24    }
25    else if (MSP_IVW_MSG_WAKEUP == msg) //唤醒成功消息
26    {
27        printf("\n\nMSP_IVW_MSG_WAKEUP result = %s\n\n", info);
28    }
29    return 0;
30}
31
32void run_ivw(const char *grammar_list, const char* audio_filename ,  const char* session_begin_params)
33
{
34    const char *session_id = NULL;
35    int err_code = MSP_SUCCESS;
36    FILE *f_aud = NULL;
37    long audio_size = 0;
38    long real_read = 0;
39    long audio_count = 0;
40    int count = 0;
41    int audio_stat = MSP_AUDIO_SAMPLE_CONTINUE;
42    char *audio_buffer=NULL;
43    char sse_hints[128];
44    if (NULL == audio_filename)
45    {
46        printf("params error\n");
47        return;
48    }
49
50    f_aud=fopen(audio_filename, "rb");
51    if (NULL == f_aud)
52    {
53        printf("audio file open failed! \n");
54        return;
55    }
56    fseek(f_aud, 0, SEEK_END);
57    audio_size = ftell(f_aud);
58    fseek(f_aud, 0, SEEK_SET);
59    audio_buffer = (char *)malloc(audio_size);
60    if (NULL == audio_buffer)
61    {
62        printf("malloc failed! \n");
63        goto exit;
64    }
65    real_read = fread((void *)audio_buffer, 1, audio_size, f_aud);
66    if (real_read != audio_size)
67    {
68        printf("read audio file failed!\n");
69        goto exit;
70    }
71
72    session_id=QIVWSessionBegin(grammar_list, session_begin_params, &err_code);
73    if (err_code != MSP_SUCCESS)
74    {
75        printf("QIVWSessionBegin failed! error code:%d\n",err_code);
76        goto exit;
77    }
78
79    err_code = QIVWRegisterNotify(session_id, cb_ivw_msg_proc,NULL);
80    if (err_code != MSP_SUCCESS)
81    {
82        snprintf(sse_hints, sizeof(sse_hints), "QIVWRegisterNotify errorCode=%d", err_code);
83        printf("QIVWRegisterNotify failed! error code:%d\n",err_code);
84        goto exit;
85    }
86    while(1)
87    {
88        long len = 10*FRAME_LEN; //16k音频,10帧 (时长200ms)
89        audio_stat = MSP_AUDIO_SAMPLE_CONTINUE;
90        if(audio_size <= len)
91        {
92            len = audio_size;
93            audio_stat = MSP_AUDIO_SAMPLE_LAST; //最后一块
94        }
95        if (0 == audio_count)
96        {
97            audio_stat = MSP_AUDIO_SAMPLE_FIRST;
98        }
99
100        printf("csid=%s,count=%d,aus=%d\n",session_id, count++, audio_stat);
101        err_code = QIVWAudioWrite(session_id, (const void *)&audio_buffer[audio_count], len, audio_stat);
102        if (MSP_SUCCESS != err_code)
103        {
104            printf("QIVWAudioWrite failed! error code:%d\n",err_code);
105            snprintf(sse_hints, sizeof(sse_hints), "QIVWAudioWrite errorCode=%d", err_code);
106            goto exit;
107        }
108        if (MSP_AUDIO_SAMPLE_LAST == audio_stat)
109        {
110            break;
111        }
112        audio_count += len;
113        audio_size -= len;
114
115        sleep_ms(200); //模拟人说话时间间隙,10帧的音频时长为200ms
116    }
117    snprintf(sse_hints, sizeof(sse_hints), "success");
118
119exit:
120    if (NULL != session_id)
121    {
122        QIVWSessionEnd(session_id, sse_hints);
123    }
124    if (NULL != f_aud)
125    {
126        fclose(f_aud);
127    }
128    if (NULL != audio_buffer)
129    {
130        free(audio_buffer);
131    }
132}
133
134
135int main(int argc, char* argv[])
136
{
137    int         ret       = MSP_SUCCESS;
138    const char *lgi_param = "appid = 5d5b9efd,work_dir = .";
139    const char *ssb_param = "ivw_threshold=0:1450,sst=wakeup,ivw_res_path =fo|res/ivw/wakeupresource.jet";
140
141    ret = MSPLogin(NULLNULL, lgi_param);
142    if (MSP_SUCCESS != ret)
143    {
144        printf("MSPLogin failed, error code: %d.\n", ret);
145        goto exit ;//登录失败,退出登录
146    }
147    printf("\n###############################################################################################################\n");
148    printf("## 请注意,唤醒语音需要根据唤醒词内容自行录制并重命名为宏IVW_AUDIO_FILE_NAME所指定名称,存放在bin/audio文件里##\n");
149    printf("###############################################################################################################\n\n");
150    run_ivw(NULL, IVW_AUDIO_FILE_NAME, ssb_param); 
151
152    sleep_ms(2000);
153exit:
154    printf("按任意键退出 ...\n");
155    getchar();
156    MSPLogout(); //退出登录
157    return 0;
158}

上述语音唤醒主要API调用流程如下图所示,我们就可以比较清楚的了解整个唤醒流程是什么样的了:

唤醒检测API调用流程图

从流程图可以看出,要想实现不间断的检测唤醒词流程。那就需要不断的调用QIVWAudioWrite(),即将录音语料不断的写入进行检测。那我们这里使用的是linuxrec.c录音代码,然后再稍加修改这里的唤醒检测代码就可以不间断的检测唤醒词了。这里的linuxrec.c录音代码如下:

  1/*
2@file
3@brief  record demo for linux
4@author        taozhang9
5@date        2016/05/27
6*/

7#include <stdio.h>
8#include <stdlib.h>
9#include <fcntl.h>
10#include <alsa/asoundlib.h>
11#include <signal.h>
12#include <sys/stat.h>
13#include <pthread.h>
14#include "../../include/formats.h"
15#include "../../include/linuxrec.h"
16#define DBG_ON 1
17#if DBG_ON
18#define dbg  printf
19#else
20#define dbg
21#endif
22/* Do not change the sequence */
23enum {
24    RECORD_STATE_CREATED,   /* Init     */
25    RECORD_STATE_CLOSING,
26    RECORD_STATE_READY,     /* Opened   */
27    RECORD_STATE_STOPPING,  /* During Stop  */
28    RECORD_STATE_RECORDING, /* Started  */
29};
30#define SAMPLE_RATE  16000
31#define SAMPLE_BIT_SIZE 16
32#define FRAME_CNT   10
33//#define BUF_COUNT   1
34#define DEF_BUFF_TIME  500000
35#define DEF_PERIOD_TIME 100000
36static int show_xrun = 1;
37static int start_record_internal(snd_pcm_t *pcm)
38
{
39    return snd_pcm_start(pcm);
40}
41static int stop_record_internal(snd_pcm_t *pcm)
42
{
43    return snd_pcm_drop(pcm);
44}
45static int is_stopped_internal(struct recorder *rec)
46
{
47    snd_pcm_state_t state;
48    state =  snd_pcm_state((snd_pcm_t *)rec->wavein_hdl);
49    switch (state) {
50    case SND_PCM_STATE_RUNNING:
51    case SND_PCM_STATE_DRAINING:
52        return 0;
53    defaultbreak;
54    }
55    return 1;
56}
57static int format_ms_to_alsa(const WAVEFORMATEX * wavfmt,
58                        snd_pcm_format_t * format)

59
{
60    snd_pcm_format_t tmp;
61    tmp = snd_pcm_build_linear_format(wavfmt->wBitsPerSample,
62            wavfmt->wBitsPerSample, wavfmt->wBitsPerSample == 8 ? 1 : 00);
63    if ( tmp == SND_PCM_FORMAT_UNKNOWN )
64        return -EINVAL;
65    *format = tmp;
66    return 0;
67}
68/* set hardware and software params */
69static int set_hwparams(struct recorder * rec,  const WAVEFORMATEX *wavfmt,
70            unsigned int buffertime, unsigned int periodtime)

71
{
72    snd_pcm_hw_params_t *params;
73    int err;
74    unsigned int rate;
75    snd_pcm_format_t format;
76    snd_pcm_uframes_t size;
77    snd_pcm_t *handle = (snd_pcm_t *)rec->wavein_hdl;
78    rec->buffer_time = buffertime;
79    rec->period_time = periodtime;
80    snd_pcm_hw_params_alloca(&params);
81    err = snd_pcm_hw_params_any(handle, params);
82    if (err < 0) {
83        dbg("Broken configuration for this PCM");
84        return err;
85    }
86    err = snd_pcm_hw_params_set_access(handle, params,
87                       SND_PCM_ACCESS_RW_INTERLEAVED);
88    if (err < 0) {
89        dbg("Access type not available");
90        return err;
91    }
92    err = format_ms_to_alsa(wavfmt, &format);
93    if (err) {
94        dbg("Invalid format");
95        return - EINVAL;
96    }
97    err = snd_pcm_hw_params_set_format(handle, params, format);
98    if (err < 0) {
99        dbg("Sample format non available");
100        return err;
101    }
102    err = snd_pcm_hw_params_set_channels(handle, params, wavfmt->nChannels);
103    if (err < 0) {
104        dbg("Channels count non available");
105        return err;
106    }
107    rate = wavfmt->nSamplesPerSec;
108    err = snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0);
109    if (err < 0) {
110        dbg("Set rate failed");
111        return err;
112    }
113    if(rate != wavfmt->nSamplesPerSec) {
114        dbg("Rate mismatch");
115        return -EINVAL;
116    }
117    if (rec->buffer_time == 0 || rec->period_time == 0) {
118        err = snd_pcm_hw_params_get_buffer_time_max(params,
119                            &rec->buffer_time, 0);
120        assert(err >= 0);
121        if (rec->buffer_time > 500000)
122            rec->buffer_time = 500000;
123        rec->period_time = rec->buffer_time / 4;
124    }
125    err = snd_pcm_hw_params_set_period_time_near(handle, params,
126                         &rec->period_time, 0);
127    if (err < 0) {
128        dbg("set period time fail");
129        return err;
130    }
131    err = snd_pcm_hw_params_set_buffer_time_near(handle, params,
132                         &rec->buffer_time, 0);
133    if (err < 0) {
134        dbg("set buffer time failed");
135        return err;
136    }
137    err = snd_pcm_hw_params_get_period_size(params, &size, 0);
138    if (err < 0) {
139        dbg("get period size fail");
140        return err;
141    }
142    rec->period_frames = size;
143    err = snd_pcm_hw_params_get_buffer_size(params, &size);
144    if (size == rec->period_frames) {
145        dbg("Can't use period equal to buffer size (%lu == %lu)",
146                      size, rec->period_frames);
147        return -EINVAL;
148    }
149    rec->buffer_frames = size;
150    rec->bits_per_frame = wavfmt->wBitsPerSample;
151    /* set to driver */
152    err = snd_pcm_hw_params(handle, params);
153    if (err < 0) {
154        dbg("Unable to install hw params:");
155        return err;
156    }
157    return 0;
158}
159static int set_swparams(struct recorder * rec)
160
{
161    int err;
162    snd_pcm_sw_params_t *swparams;
163    snd_pcm_t * handle = (snd_pcm_t*)(rec->wavein_hdl);
164    /* sw para */
165    snd_pcm_sw_params_alloca(&swparams);
166    err = snd_pcm_sw_params_current(handle, swparams);
167    if (err < 0) {
168        dbg("get current sw para fail");
169        return err;
170    }
171    err = snd_pcm_sw_params_set_avail_min(handle, swparams,
172                        rec->period_frames);
173    if (err < 0) {
174        dbg("set avail min failed");
175        return err;
176    }
177    /* set a value bigger than the buffer frames to prevent the auto start.
178     * we use the snd_pcm_start to explicit start the pcm */

179    err = snd_pcm_sw_params_set_start_threshold(handle, swparams,
180            rec->buffer_frames * 2);
181    if (err < 0) {
182        dbg("set start threshold fail");
183        return err;
184    }
185    if ( (err = snd_pcm_sw_params(handle, swparams)) < 0) {
186        dbg("unable to install sw params:");
187        return err;
188    }
189    return 0;
190}
191static int set_params(struct recorder *rec, WAVEFORMATEX *fmt,
192        unsigned int buffertime, unsigned int periodtime)

193
{
194    int err;
195    WAVEFORMATEX defmt =
196{WAVE_FORMAT_PCM,    11600032000216sizeof(WAVEFORMATEX)};
197    if (fmt == NULL) {
198        fmt = &defmt;
199    }
200    err = set_hwparams(rec, fmt, buffertime, periodtime);
201    if (err)
202        return err;
203    err = set_swparams(rec);
204    if (err)
205        return err;
206    return 0;
207}
208/*
209 *   Underrun and suspend recovery
210 */

211static int xrun_recovery(snd_pcm_t *handle, int err)
212
{
213    if (err == -EPIPE) {    /* over-run */
214        if (show_xrun)
215            printf("!!!!!!overrun happend!!!!!!");
216        err = snd_pcm_prepare(handle);
217        if (err < 0) {
218            if (show_xrun)
219                printf("Can't recovery from overrun,"
220                "prepare failed: %s\n", snd_strerror(err));
221            return err;
222        }
223        return 0;
224    } else if (err == -ESTRPIPE) {
225        while ((err = snd_pcm_resume(handle)) == -EAGAIN)
226            usleep(200000); /* wait until the suspend flag is released */
227        if (err < 0) {
228            err = snd_pcm_prepare(handle);
229            if (err < 0) {
230                if (show_xrun)
231                    printf("Can't recovery from suspend,"
232                    "prepare failed: %s\n", snd_strerror(err));
233                return err;
234            }
235        }
236        return 0;
237    }
238    return err;
239}
240static ssize_t pcm_read(struct recorder *rec, size_t rcount)
241
{
242    ssize_t r;
243    size_t count = rcount;
244    char *data;
245    snd_pcm_t *handle = (snd_pcm_t *)rec->wavein_hdl;
246    if(!handle)
247        return -EINVAL;
248    data = rec->audiobuf;
249    while (count > 0) {
250        r = snd_pcm_readi(handle, data, count);
251        if (r == -EAGAIN || (r >= 0 && (size_t)r < count)) {
252            snd_pcm_wait(handle, 100);
253        } else if (r < 0) {
254            if(xrun_recovery(handle, r) < 0) {
255                return -1;
256            }
257        }
258        if (r > 0) {
259            count -= r;
260            data += r * rec->bits_per_frame / 8;
261        }
262    }
263    return rcount;
264}
265static void * record_thread_proc(void * para)
266
{
267    struct recorder * rec = (struct recorder *) para;
268    size_t frames, bytes;
269    sigset_t mask, oldmask;
270    sigemptyset(&mask);
271    sigaddset(&mask, SIGINT);
272    sigaddset(&mask, SIGTERM);
273    pthread_sigmask(SIG_BLOCK, &mask, &oldmask);
274    while(1) {
275        frames = rec->period_frames;
276        bytes = frames * rec->bits_per_frame / 8;
277        /* closing, exit the thread */
278        if (rec->state == RECORD_STATE_CLOSING)
279            break;
280        if(rec->state < RECORD_STATE_RECORDING)
281            usleep(100000);
282        if (pcm_read(rec, frames) != frames) {
283            return NULL;
284        }
285        if (rec->on_data_ind)
286            rec->on_data_ind(rec->audiobuf, bytes,
287                    rec->user_cb_para);
288    }
289    return rec;
290}
291static int create_record_thread(void * para, pthread_t * tidp)
292
{
293    int err;
294    err = pthread_create(tidp, NULL, record_thread_proc, (void *)para);
295    if (err != 0)
296        return err;
297    return 0;
298}
299static void free_rec_buffer(struct recorder * rec)
300
{
301    if (rec->audiobuf) {
302        free(rec->audiobuf);
303        rec->audiobuf = NULL;
304    }
305}
306static int prepare_rec_buffer(struct recorder * rec )
307
{
308    /* the read and QISRWrite is blocked, currently only support one buffer,
309     * if overrun too much, need more buffer and another new thread
310     * to write the audio to network */

311    size_t sz = (rec->period_frames * rec->bits_per_frame / 8);
312    rec->audiobuf = (char *)malloc(sz);
313    if(!rec->audiobuf)
314        return -ENOMEM;
315    return 0;
316}
317#endif
318static int open_recorder_internal(struct recorder * rec,
319        record_dev_id dev, WAVEFORMATEX * fmt)

320
{
321    int err = 0;
322    err = snd_pcm_open((snd_pcm_t **)&rec->wavein_hdl, dev.u.name,
323            SND_PCM_STREAM_CAPTURE, 0);
324    if(err < 0)
325        goto fail;
326    err = set_params(rec, fmt, DEF_BUFF_TIME, DEF_PERIOD_TIME);
327    if(err)
328        goto fail;
329    assert(rec->bufheader == NULL);
330    err = prepare_rec_buffer(rec);
331    if(err)
332        goto fail;
333    err = create_record_thread((void*)rec,
334            &rec->rec_thread);
335    if(err)
336        goto fail;
337    return 0;
338fail:
339    if(rec->wavein_hdl)
340        snd_pcm_close((snd_pcm_t *) rec->wavein_hdl);
341    rec->wavein_hdl = NULL;
342    free_rec_buffer(rec);
343    return err;
344}
345static void close_recorder_internal(struct recorder *rec)
346
{
347    snd_pcm_t * handle;
348    handle = (snd_pcm_t *) rec->wavein_hdl;
349    /* may be the thread is blocked at read, cancel it */
350    pthread_cancel(rec->rec_thread);
351    /* wait for the pcm thread quit first */
352    pthread_join(rec->rec_thread, NULL);
353    if(handle) {
354        snd_pcm_close(handle);
355        rec->wavein_hdl = NULL;
356    }
357    free_rec_buffer(rec);
358}
359/* return the count of pcm device */
360/* list all cards */
361static int get_pcm_device_cnt(snd_pcm_stream_t stream)
362
{
363    void **hints, **n;
364    char *io, *filter, *name;
365    int cnt = 0;
366    if (snd_device_name_hint(-1"pcm", &hints) < 0)
367        return 0;
368    n = hints;
369    filter = stream == SND_PCM_STREAM_CAPTURE ? "Input" : "Output";
370    while (*n != NULL) {
371        io = snd_device_name_get_hint(*n, "IOID");
372        name = snd_device_name_get_hint(*n, "NAME");
373        if (name && (io == NULL || strcmp(io, filter) == 0))
374            cnt ++;
375        if (io != NULL)
376            free(io);
377        if (name != NULL)
378            free(name);
379        n++;
380    }
381    snd_device_name_free_hint(hints);
382    return cnt;
383}
384/* -------------------------------------
385 * Interfaces
386 --------------------------------------*/

387/* the device id is a pcm string name in linux */
388record_dev_id  get_default_input_dev()
389
{
390    record_dev_id id;
391    id.u.name = "default";
392    return id;
393}
394record_dev_id * list_input_device()
395
{
396    // TODO: unimplemented
397    return NULL;
398}
399int get_input_dev_num()
400
{
401    return get_pcm_device_cnt(SND_PCM_STREAM_CAPTURE);
402}
403/* callback will be run on a new thread */
404int create_recorder(struct recorder ** out_rec,
405                void (*on_data_ind)
(char *data, unsigned long len, void *user_cb_para),
406                void* user_cb_para)
407
{
408    struct recorder * myrec;
409    myrec = (struct recorder *)malloc(sizeof(struct recorder));
410    if(!myrec)
411        return -RECORD_ERR_MEMFAIL;
412    memset(myrec, 0sizeof(struct recorder));
413    myrec->on_data_ind = on_data_ind;
414    myrec->user_cb_para = user_cb_para;
415    myrec->state = RECORD_STATE_CREATED;
416    *out_rec = myrec;
417    return 0;
418}
419void destroy_recorder(struct recorder *rec)
420
{
421    if(!rec)
422        return;
423    free(rec);
424}
425int open_recorder(struct recorder * rec, record_dev_id dev, WAVEFORMATEX * fmt)
426
{
427    int ret = 0;
428    if(!rec )
429        return -RECORD_ERR_INVAL;
430    if(rec->state >= RECORD_STATE_READY)
431        return 0;
432    ret = open_recorder_internal(rec, dev, fmt);
433    if(ret == 0)
434        rec->state = RECORD_STATE_READY;
435    return 0;
436}
437void close_recorder(struct recorder *rec)
438
{
439    if(rec == NULL || rec->state < RECORD_STATE_READY)
440        return;
441    if(rec->state == RECORD_STATE_RECORDING)
442        stop_record(rec);
443    rec->state = RECORD_STATE_CLOSING;
444    close_recorder_internal(rec);
445    rec->state = RECORD_STATE_CREATED;
446}
447int start_record(struct recorder * rec)
448
{
449    int ret;
450    if(rec == NULL)
451        return -RECORD_ERR_INVAL;
452    if( rec->state < RECORD_STATE_READY)
453        return -RECORD_ERR_NOT_READY;
454    if( rec->state == RECORD_STATE_RECORDING)
455        return 0;
456    ret = start_record_internal((snd_pcm_t *)rec->wavein_hdl);
457    if(ret == 0)
458        rec->state = RECORD_STATE_RECORDING;
459    return ret;
460}
461int stop_record(struct recorder * rec)
462
{
463    int ret;
464    if(rec == NULL)
465        return -RECORD_ERR_INVAL;
466    if( rec->state < RECORD_STATE_RECORDING)
467        return 0;
468    rec->state = RECORD_STATE_STOPPING;
469    ret = stop_record_internal((snd_pcm_t *)rec->wavein_hdl);
470    if(ret == 0) {
471        rec->state = RECORD_STATE_READY;
472    }
473    return ret;
474}
475int is_record_stopped(struct recorder *rec)
476
{
477    if(rec->state == RECORD_STATE_RECORDING)
478        return 0;
479    return is_stopped_internal(rec);
480}

这里对应的头文件为linuxrec.h,内容如下:

  1/*
2 * @file
3 * @brief a record demo in linux
4 *
5 * a simple record code. using alsa-lib APIs.
6 * keep the function same as winrec.h
7 *
8 * Common steps:
9 *    create_recorder,
10 *    open_recorder, 
11 *    start_record, 
12 *    stop_record, 
13 *    close_recorder,
14 *    destroy_recorder
15 *
16 * @author        taozhang9
17 * @date        2016/06/01
18 */

19
20#ifndef __IFLY_WINREC_H__
21#define __IFLY_WINREC_H__
22
23#include "formats.h"
24/* error code */
25enum {
26    RECORD_ERR_BASE = 0,
27    RECORD_ERR_GENERAL,
28    RECORD_ERR_MEMFAIL,
29    RECORD_ERR_INVAL,
30    RECORD_ERR_NOT_READY
31};
32
33typedef struct {
34    union {
35        char *  name;
36        int index;
37        void *  resv;
38    }u;
39}record_dev_id;
40
41/* recorder object. */
42struct recorder {
43    void (*on_data_ind)(char *data, unsigned long len, void *user_para);
44    void * user_cb_para;
45    volatile int state;     /* internal record state */
46
47    void * wavein_hdl;
48    /* thread id may be a struct. by implementation 
49     * void * will not be ported!! */

50    pthread_t rec_thread; 
51    /*void * rec_thread_hdl;*/
52
53    void * bufheader;
54    unsigned int bufcount; 
55
56    char *audiobuf;
57    int bits_per_frame;
58    unsigned int buffer_time;
59    unsigned int period_time;
60    size_t period_frames;
61    size_t buffer_frames;
62};
63
64#ifdef __cplusplus
65extern "C" {
66#endif /* C++ */
67
68/** 
69 * @fn
70 * @brief    Get the default input device ID
71 *
72 * @return    returns "default" in linux.
73 *
74 */

75record_dev_id get_default_input_dev();
76
77/**
78 * @fn 
79 * @brief    Get the total number of active input devices.
80 * @return    
81 */

82int get_input_dev_num();
83
84/**
85 * @fn 
86 * @brief    Create a recorder object.
87 *
88 * Never call the close_recorder in the callback function. as close
89 * action will wait for the callback thread to quit. 
90 *
91 * @return    int         - Return 0 in success, otherwise return error code.
92 * @param    out_rec     - [out] recorder object holder
93 * @param    on_data_ind - [in]  callback. called when data coming.
94 * @param    user_cb_para    - [in] user params for the callback.
95 * @see
96 */

97int create_recorder(struct recorder ** out_rec, 
98                void (*on_data_ind)
(char *data, unsigned long len, void *user_para)
99                void* user_cb_para)
;
100
101/**
102 * @fn 
103 * @brief    Destroy recorder object. free memory. 
104 * @param    rec - [in]recorder object
105 */

106void destroy_recorder(struct recorder *rec);
107
108/**
109 * @fn 
110 * @brief    open the device.
111 * @return    int         - Return 0 in success, otherwise return error code.
112 * @param    rec         - [in] recorder object
113 * @param    dev         - [in] device id, from 0.
114 * @param    fmt         - [in] record format.
115 * @see
116 *     get_default_input_dev()
117 */

118int open_recorder(struct recorder * rec, record_dev_id dev, WAVEFORMATEX * fmt);
119
120/**
121 * @fn
122 * @brief    close the device.
123 * @param    rec         - [in] recorder object
124 */

125
126void close_recorder(struct recorder *rec);
127
128/**
129 * @fn
130 * @brief    start record.
131 * @return    int         - Return 0 in success, otherwise return error code.
132 * @param    rec         - [in] recorder object
133 */

134int start_record(struct recorder * rec);
135
136/**
137 * @fn
138 * @brief    stop record.
139 * @return    int         - Return 0 in success, otherwise return error code.
140 * @param    rec         - [in] recorder object
141 */

142int stop_record(struct recorder * rec);
143
144/**
145 * @fn
146 * @brief    test if the recording has been stopped.
147 * @return    int         - 1: stopped. 0 : recording.
148 * @param    rec         - [in] recorder object
149 */

150int is_record_stopped(struct recorder *rec);
151
152#ifdef __cplusplus
153/* extern "C" */    
154#endif /* C++ */
155
156#endif

这里还有一个对应的formats.h头文件,用来表示wav文件格式的,源码如下:

 1#ifndef FORMATS_H_160601_TT
2#define FORMATS_H_160601_TT        1
3
4#ifndef WAVE_FORMAT_PCM  
5#define WAVE_FORMAT_PCM  1
6typedef struct tWAVEFORMATEX {
7    unsigned short    wFormatTag;
8    unsigned short    nChannels;
9    unsigned int      nSamplesPerSec;
10    unsigned int      nAvgBytesPerSec;
11    unsigned short    nBlockAlign;
12    unsigned short    wBitsPerSample;
13    unsigned short    cbSize;
14} WAVEFORMATEX;
15#endif
16
17#endif

这里需要注意的是上述三个新增的代码放的位置,两个头文件需要放到include目录下,linuxrec.c录音源码需要和唤醒测试的源码放到一起。最后,我们就可以来调用linuxrec.c下面的各种录音接口函数来开始录音了,然后将录音语料不断的发送给检测唤醒的函数来执行。完整代码如下:

  1#include <stdlib.h>
2#include <stdio.h>
3#include <unistd.h>
4#include <errno.h>
5
6
7#include "../../include/msp_cmn.h"
8#include "../../include/qivw.h"
9#include "../../include/msp_errors.h"
10#include "../../include/linuxrec.h"
11#include "../../include/formats.h"
12
13
14#define SAMPLE_RATE_16K     (16000)
15
16#define DEFAULT_FORMAT    \
17{\
18    WAVE_FORMAT_PCM,\
19    1,          \
20    16000,      \
21    32000,      \
22    2,          \
23    16,         \
24    sizeof(WAVEFORMATEX)\
25}

26
27struct recorder *recorder = NULL;
28
29void sleep_ms(int ms)
30
{
31    usleep(ms * 1000);
32}
33
34/* the record call back */
35void record_data_cb(char *data, unsigned long len, void *user_para)
36
{
37    int errcode = 0;
38    const char *session_id = (const char *)user_para;
39
40    if(len == 0 || data == NULL)
41        return;
42
43    errcode = QIVWAudioWrite(session_id, (const void *)data, len, MSP_AUDIO_SAMPLE_CONTINUE);
44    if (MSP_SUCCESS != errcode)
45    {
46        printf("QIVWAudioWrite failed! error code:%d\n",errcode);
47        int ret = stop_record(recorder);
48        if (ret != 0) {
49            printf("Stop failed! \n");
50        }
51        QIVWAudioWrite(session_id, NULL0, MSP_AUDIO_SAMPLE_LAST);
52    }
53}
54
55int cb_ivw_msg_procconst char *sessionID, int msg, int param1, int param2, const void *info, void *userData )
56
{
57  if (MSP_IVW_MSG_ERROR == msg) //唤醒出错消息
58  {
59    printf("\n\nMSP_IVW_MSG_ERROR errCode = %d\n\n", param1);
60  }else if (MSP_IVW_MSG_WAKEUP == msg) //唤醒成功消息
61  {
62    //printf("\n\nMSP_IVW_MSG_WAKEUP result = %s\n\n", (char*)info);
63    system("play ~/Music/ding.wav");
64  }
65
66  return 0;
67}
68
69
70void run_ivw(const char* session_begin_params)
71
{
72    const char *session_id = NULL;
73    int err_code = MSP_SUCCESS;
74    char sse_hints[128];
75
76    WAVEFORMATEX wavfmt = DEFAULT_FORMAT;
77    wavfmt.nSamplesPerSec = SAMPLE_RATE_16K;
78    wavfmt.nAvgBytesPerSec = wavfmt.nBlockAlign * wavfmt.nSamplesPerSec;
79
80    //start QIVW
81    session_id=QIVWSessionBegin(NULL, session_begin_params, &err_code);
82    if (err_code != MSP_SUCCESS)
83    {
84        printf("QIVWSessionBegin failed! error code:%d\n",err_code);
85        goto exit;
86    }
87
88    err_code = QIVWRegisterNotify(session_id, cb_ivw_msg_proc, NULL);
89    if (err_code != MSP_SUCCESS)
90    {
91        snprintf(sse_hints, sizeof(sse_hints), "QIVWRegisterNotify errorCode=%d", err_code);
92        printf("QIVWRegisterNotify failed! error code:%d\n",err_code);
93        goto exit;
94    }
95
96    //1.create recorder
97    err_code = create_recorder(&recorder, record_data_cb, (void*)session_id);
98    if (recorder == NULL || err_code != 0)
99    {
100            printf("create recorder failed: %d\n", err_code);
101            err_code = MSP_ERROR_FAIL;
102            goto exit;
103    }
104
105    //2.open_recorder
106    err_code = open_recorder(recorder, get_default_input_dev(), &wavfmt);
107    if (err_code != 0)
108    {
109        printf("recorder open failed: %d\n", err_code);
110        err_code = MSP_ERROR_FAIL;
111        goto exit;
112    }
113
114    //3.start record
115    err_code = start_record(recorder);
116    if (err_code != 0) {
117        printf("start record failed: %d\n", err_code);
118        err_code = MSP_ERROR_FAIL;
119        goto exit;
120    }
121
122    while(1)
123    {
124        sleep_ms(2000); 
125        printf("Listening... Press Ctrl+C to exit\n");
126    }
127    snprintf(sse_hints, sizeof(sse_hints), "success");
128
129exit:
130    if (recorder)
131        {
132        if(!is_record_stopped(recorder))
133            stop_record(recorder);
134        close_recorder(recorder);
135        destroy_recorder(recorder);
136        recorder = NULL;
137    }
138    if (NULL != session_id)
139    {
140        QIVWSessionEnd(session_id, sse_hints);
141    }
142}
143
144
145int main(int argc, char* argv[])
146
{
147    int         ret       = MSP_SUCCESS;
148    const char *lgi_param = "appid = 5d5b9efd, work_dir = .";
149    const char *ssb_param = "ivw_threshold=0:1450, sst=wakeup, ivw_res_path =fo|res/ivw/wakeupresource.jet";
150
151
152    ret = MSPLogin(NULLNULL, lgi_param);
153    if (MSP_SUCCESS != ret)
154    {
155        printf("MSPLogin failed, error code: %d.\n", ret);
156        MSPLogout();//登录失败,退出登录
157    }
158
159    run_ivw(ssb_param);
160    return 0;
161}

从上述代码我们可以看出来实现的逻辑流程,首先根据linuxrec.c中实现的录音功能,创建一个录音设备。然后打开录音设备,最后开启设备进行录音。在录音过程中设置了回调函数,当有数据返回时自动调用record_data_cb()函数处理录音数据。这里处理数据就是直接丢给QIVWAudioWrite()进行唤醒词的检测,此时在录音检测的时候也是设置了回调函数。当检测到录音语料中包含有唤醒词时,就自动的回调cb_ivw_msg_proc()函数进行处理。当然这里回调函数也不是百分百都是检测到唤醒词的,当检测程序异常的时候,也会抛出MSP_IVW_MSG_ERROR消息,只要返回MSP_IVW_MSG_WAKEUP消息,这才是最终的检测到唤醒词了。所以最终的唤醒提示是在cb_ivw_msg_proc()回调函数中提示的,如果我们需要在唤醒后有后续操作就可以在这个回调函数中进行设置。

0x04 编译程序并测试唤醒效果

这里最后就是准备编译程序了,不过在编译程序前,需要修改下Makefile文件。因为这里我们调用了alsa库中的一些API,所以在编译的时候需要加上-lasound这个选型就可以了,这样在编译程序的时候就会链接到对应的动态库。完整的Makefile文件如下:

 1#common makefile header
2
3DIR_INC = ../../include
4DIR_BIN = ../../bin
5DIR_LIB = ../../libs
6
7TARGET    = awaken_offline_sample
8BIN_TARGET = $(DIR_BIN)/$(TARGET)
9
10CROSS_COMPILE = 
11CFLAGS = -g -Wall -I$(DIR_INC)
12
13LDFLAGS := -L$(DIR_LIB)/
14LDFLAGS += -lmsc -lrt -ldl -lpthread -lstdc++ -lasound
15
16OBJECTS := $(patsubst %.c,%.o,$(wildcard *.c))
17
18$(BIN_TARGET) : $(OBJECTS)
19    $(CROSS_COMPILE)gcc $(CFLAGS) $^ -o $@ $(LDFLAGS)
20
21%.o : %.c
22    $(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@
23clean:
24    @rm -f *.o $(BIN_TARGET)
25
26.PHONY:clean
27
28#common makefile foot

在我们开始编译前,再来整体看一下各个文件的存储路径以及编译过程,如下图所示:

最终源码组成

接下来就可以来编译源码了,这里我把以前的bash脚本给修改了下名字,整个编译、执行过程如下图:

编译和执行唤醒检测过程

只看图片感受不到科大讯飞唤醒词的效果,接下来通过视频演示来看看唤醒效果吧,我这里设置的唤醒词是“灵龟机器人”。就是我们在前面看到的这个唤醒词的得分是5颗星:

科大讯飞唤醒效果演示

通过上面视频可以得知,无论科大讯飞的唤醒还是snowboy的唤醒效果,我们都需要根据不同唤醒词来配置不同的唤醒门限。不然就会有误唤醒率,这里仍然是需要不断的修改,不断的测试唤醒效果,最终才能得到最好的唤醒效果。对于科大讯飞的唤醒门限可以参考官方的文档库,如下图所示:

科大讯飞唤醒词门限设置

0x05 唤醒源码下载

对于上述修改好的测试源码,大家可以直接从以下代码仓库中下载:

https://code.corvin.cn/corvin_zhang/AIVoiceSystem

下载源码

0x06 参考资料

[1].讯飞开放平台官网. https://www.xfyun.cn/

[2].科大讯飞语音唤醒sdk文档. https://www.xfyun.cn/doc/asr/awaken/Linux-SDK.html

[3].科大讯飞语音唤醒介绍. https://www.xfyun.cn/services/awaken?type=awaken

[4].科大讯飞实时语音唤醒+离线命令词识别在Linux及ROS下的应用. https://haoqchen.site/2018/04/26/iflytek-awaken-asr/

[5].语音唤醒头文件qivw.h的API介绍. http://mscdoc.xfyun.cn/windows/api/iFlytekMSCReferenceManual/qivw_8h.html#details


0x07 问题反馈

大家在按照教程操作过程中有任何问题,可以直接在文章末尾给我留言,或者关注ROS小课堂的官方微信公众号,在公众号中给我发消息反馈问题也行。我基本上每天都会处理公众号中的留言!当然如果你要是顺便给ROS小课堂打个赏,我也会感激不尽的,打赏30块还会被邀请进ROS小课堂的微信群,与更多志同道合的小伙伴一起学习和交流!

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据