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

7.语音板使用科大讯飞离线语音合成

0x00 离线语音合成介绍

语音合成技术(Text to speech,简称TTS),是可以将文本转换成语音文件的技术。而且合成的语音文件,可以根据不同需要而合成出不同音色、语速和语调的声音,让合成的语音就跟真人说话的声音几乎一样。利用这项技术我们就可以让我们的机器人、电脑、手机、音箱等各种电子设备多了一种更为自然的方式来与人们进行交互。现在我们大家接触最多的语音合成技术,可能就是用高德导航的时候,它会通过语音给我们播报,指引我们到达目的地。当然还有一些其他应用场景,例如各种办公大厅里的叫号系统,机场、医院的语音播报,很多基本上都是用这种语音合成技术来实现的。

语音合成的典型应用

语音合成分为在线语音合成和离线的语音合成,在线语音合成使用的是在线引擎(TYPE_CLOUD),又称为云端模式,它需要使用网络,速度稍慢,并产生一定流量,但有更好的识别和合成的效果,如更高的识别匹配度,更多的发音人等。在默认情况下,在线语音合成引擎,提供5种免费的发音人供大家使用,如果要想使用更多的发音人,那就需要收费了:

在线语音合成免费发音人列表

离线语音合成使用的是离线引擎(TYPE_LOCAL),又称为本地模式,不需要使用网络,且识别和合成的速度更快,但同时要求购买并使用对应的离线资源(下载对应离线功能的SDK包)。那离线语音合成默认就提供2个发音人,如果要想使用其他离线发音人,那就需要收费了:

科大讯飞离线发音人库

0x01 下载离线语音合成SDK

这里下载科大讯飞的离线语音合成SDK,仍然是比较简单的,只要去讯飞开放平台上下载即可,具体网址如下:

https://www.xfyun.cn/sdk/dispatcher

下载离线语音合成Linux平台的SDK

当我们下载好SDK后,就可以将SDK发送树莓派上解压使用了,完整的发送命令如下,大家需要根据自己的文件名和树莓派的IP地址做修改即可:

scp Linux_aisound1226_5d5b9efd.zip corvin@192.168.3.46:~/

将SDK文件发送到树莓派

0x02 编译离线语音合成测试源码

在树莓派上我们将SDK解压,然后可以先浏览一下整个SDK的组成架构,方便我们后面进行修改:

离线语音合成SDK

下面就是该使用树莓派版本的语音合成库了,这里的动态库文件跟前面介绍的语音唤醒、命令词识别都是一个库。所以说如果大家在前面下载过的话,这里就不用再次下载了,可以直接使用。同样,在这里如果要下载树莓派版本的动态库的话也是要收费的,因为如果大家下载使用的话,会占用我的科大讯飞语音唤醒装机量的。所以需要收费的,希望可以理解:

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

当下载好该离线命令词识别库后,就可以将其放到树莓派中来使用了,这里下载后该库是可以永久使用的,没有时间限制:

修改为ARM版本的动态库

接下来就可以来修改编译相关的脚本和makefile文件了,首先来修改一下bash脚本,修改过程如下:

修改编译脚本

修改好编译脚本后,接下来就是修改Makefile文件了。因为这里gcc编译选项需要修改,这里在引用动态库的路径需要修改一下就可以了。主要就是修改13行的LDFLAGS参数,修改后的Makefile文件如下:

 1#common makefile header
2
3DIR_INC = ../../include
4DIR_BIN = ../../bin
5DIR_LIB = ../../libs
6
7TARGET    = tts_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++
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

做好一切准备后,接下来就可以来编译sample代码了。编译就是执行make.sh脚本即可,生成的执行文件是在bin目录下:

编译生成执行文件

我们可以尝试安装gcc编译警告提示来修改一些tts_offline_sample.c源码,就是在最开始包含头文件地方,新增一个#include <string.h>,然后再编译看看效果:

重新编译

0x03 运行测试程序

当编译生成了可执行文件后,我们就可以来运行下看看效果是什么样的了,如下视频所示:

离线语音合成效果演示

看过演示视频后,我们要想修改这个源码的前提是要先明白现在的代码流程才行。这里首先要认识的一个重要流程就是科大讯飞离线语音合成API的调用流程,如下图所示:

科大讯飞离线语音合成API调用流程图

在认识了合成语音的流程后,我们就可以来修改源码了。现在的代码流程是将固定的文本合成一个wav文件,然后我们再播放。现在我们修改成可以在运行程序时,从运行参数argv[1]中获取要合成的文本,然后在合成wav文件后,可以自动的播放出来。那最终修改后的完整代码如下:

  1/*
2* 语音合成(Text To Speech,TTS)技术能够自动将任意文字实时转换为连续的
3* 自然语音,是一种能够在任何时间、任何地点,向任何人提供语音信息服务的
4* 高效便捷手段,非常符合信息时代海量数据、动态更新和个性化查询的需求。
5*/

6
7#include <stdlib.h>
8#include <stdio.h>
9#include <unistd.h>
10#include <errno.h>
11#include <string.h>
12#include "../../include/qtts.h"
13#include "../../include/msp_cmn.h"
14#include "../../include/msp_errors.h"
15typedef int SR_DWORD;
16typedef short int SR_WORD ;
17
18/* wav音频头部格式 */
19typedef struct _wave_pcm_hdr
20{

21    char            riff[4];                // = "RIFF"
22    int             size_8;                 // = FileSize - 8
23    char            wave[4];                // = "WAVE"
24    char            fmt[4];                 // = "fmt "
25    int             fmt_size;               // = 下一个结构体的大小 : 16
26
27    short int       format_tag;             // = PCM : 1
28    short int       channels;               // = 通道数 : 1
29    int             samples_per_sec;        // = 采样率 : 8000 | 6000 | 11025 | 16000
30    int             avg_bytes_per_sec;      // = 每秒字节数 : samples_per_sec * bits_per_sample / 8
31    short int       block_align;            // = 每采样点字节数 : wBitsPerSample / 8
32    short int       bits_per_sample;        // = 量化比特数: 8 | 16
33
34    char            data[4];                // = "data";
35    int             data_size;              // = 纯数据长度 : FileSize - 44
36} wave_pcm_hdr;
37
38/* 默认wav音频头部数据 */
39wave_pcm_hdr default_wav_hdr =
40{
41    { 'R''I''F''F' },
42    0,
43    {'W''A''V''E'},
44    {'f''m''t'' '},
45    16,
46    1,
47    1,
48    16000,
49    32000,
50    2,
51    16,
52    {'d''a''t''a'},
53    0
54};
55/* 文本合成 */
56int text_to_speech(const char* src_text, const char* des_path, const char* params)
57
{
58    int          ret          = -1;
59    FILE*        fp           = NULL;
60    const char*  sessionID    = NULL;
61    unsigned int audio_len    = 0;
62    wave_pcm_hdr wav_hdr      = default_wav_hdr;
63    int          synth_status = MSP_TTS_FLAG_STILL_HAVE_DATA;
64
65    if (NULL == src_text || NULL == des_path)
66    {
67        printf("params is error!\n");
68        return ret;
69    }
70    fp = fopen(des_path, "wb");
71    if (NULL == fp)
72    {
73        printf("open %s error.\n", des_path);
74        return ret;
75    }
76    /* 开始合成 */
77    sessionID = QTTSSessionBegin(params, &ret);
78    if (MSP_SUCCESS != ret)
79    {
80        printf("QTTSSessionBegin failed, error code: %d.\n", ret);
81        fclose(fp);
82        return ret;
83    }
84    ret = QTTSTextPut(sessionID, src_text, (unsigned int)strlen(src_text), NULL);
85    if (MSP_SUCCESS != ret)
86    {
87        printf("QTTSTextPut failed, error code: %d.\n",ret);
88        QTTSSessionEnd(sessionID, "TextPutError");
89        fclose(fp);
90        return ret;
91    }
92    printf("正在合成 ...\n");
93    fwrite(&wav_hdr, sizeof(wav_hdr) ,1, fp); //添加wav音频头,使用采样率为16000
94    while (1)
95    {
96        /* 获取合成音频 */
97        const void* data = QTTSAudioGet(sessionID, &audio_len, &synth_status, &ret);
98        if (MSP_SUCCESS != ret)
99            break;
100        if (NULL != data)
101        {
102            fwrite(data, audio_len, 1, fp);
103            wav_hdr.data_size += audio_len; //计算data_size大小
104        }
105        if (MSP_TTS_FLAG_DATA_END == synth_status)
106            break;
107    }
108    printf("\n");
109    if (MSP_SUCCESS != ret)
110    {
111        printf("QTTSAudioGet failed, error code: %d.\n",ret);
112        QTTSSessionEnd(sessionID, "AudioGetError");
113        fclose(fp);
114        return ret;
115    }
116    /* 修正wav文件头数据的大小 */
117    wav_hdr.size_8 += wav_hdr.data_size + (sizeof(wav_hdr) - 8);
118
119    /* 将修正过的数据写回文件头部,音频文件为wav格式 */
120    fseek(fp, 40);
121    fwrite(&wav_hdr.size_8,sizeof(wav_hdr.size_8), 1, fp); //写入size_8的值
122    fseek(fp, 400); //将文件指针偏移到存储data_size值的位置
123    fwrite(&wav_hdr.data_size,sizeof(wav_hdr.data_size), 1, fp); //写入data_size的值
124    fclose(fp);
125    fp = NULL;
126    /* 合成完毕 */
127    ret = QTTSSessionEnd(sessionID, "Normal");
128    if (MSP_SUCCESS != ret)
129    {
130        printf("QTTSSessionEnd failed, error code: %d.\n",ret);
131    }
132
133    return ret;
134}
135
136int main(int argc, char* argv[])
137
{
138    int         ret                  = MSP_SUCCESS;
139    const char* login_params         = "appid = 5d5b9efd, work_dir = .";//登录参数,appid与msc库绑定,请勿随意改动
140    /*
141    * rdn:           合成音频数字发音方式
142    * volume:        合成音频的音量
143    * pitch:         合成音频的音调
144    * speed:         合成音频对应的语速
145    * voice_name:    合成发音人
146    * sample_rate:   合成音频采样率
147    * text_encoding: 合成文本编码格式
148    */

149    const char* session_begin_params = "engine_type = local,voice_name=xiaoyan, text_encoding = UTF8, tts_res_path = fo|res/tts/xiaoyan.jet;fo|res/tts/common.jet, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 2";
150    const char* filename             = "tts_sample.wav"//合成的语音文件名称
151
152    if(argc < 2)
153    {
154        printf("Please input text to compose !\n");
155        return 1;
156    }
157    /* 用户登录 */
158    ret = MSPLogin(NULLNULL, login_params); //第一个参数是用户名,第二个参数是密码,第三个参数是登录参数,用户名和密码可在http://www.xfyun.cn注册获取
159    if (MSP_SUCCESS != ret)
160    {
161        printf("MSPLogin failed, error code: %d.\n", ret);
162        goto exit ;//登录失败,退出登录
163    }
164
165    /* 文本合成 */
166    printf("开始合成 ...\n");
167    ret = text_to_speech(argv[1], filename, session_begin_params);
168    if (MSP_SUCCESS != ret)
169    {
170        printf("text_to_speech failed, error code: %d.\n", ret);
171    }
172    printf("合成完毕,现在开始播放合成的语音文件:\n");
173    system("play tts_sample.wav");
174
175exit:
176    MSPLogout(); //退出登录
177
178    return 0;
179}

其实这里修改的代码很简单,就是把main()函数中修改了一下。首先判断一下在执行程序的时候,是否有带参数。接下来就是在text_to_speech()函数中,第一个参数为要合成的文本,直接用argv[1]。最后就是在合成完毕后,直接用system()函数来执行播放命令,把合成好的wav文件播放出来就行了。具体修改地方,如下图所示:

修改main()函数

修好好代码,我们就可以编译来测试了,通过下面视频来查看语音合成效果:

科大讯飞离线语音合成自定义文本效果演示

这里通过视频可以查看到离线语音合成的效果,我认为还是很好的,合成迅速,播放清晰。这里特别要注意的是QTTSSessionBegin()函数中合成语音文件可以配置的参数,具体如下图所示:

可以配置的语音合成参数

这里可以看出我们可以配置的参数不只是sample代码中有的,例如这里我们还可以配置effect(合成音效)。下面我们就来修改看看效果,如下视频所示:

增加合成的音效

0x04 测试源码下载

我已经将所有的测试代码都上传到AIVoiceSystem这个代码仓库中,大家可以从以下链接中找到对应的代码,所有这些测试代码都是放在example目录下:

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

源码下载

0x05 参考资料

[1].离线语音合成Linux SDK文档. https://www.xfyun.cn/doc/tts/offline_tts/Linux-SDK.html#_1%E3%80%81%E7%AE%80%E4%BB%8B.

[2].离线语音合成产品. https://www.xfyun.cn/services/offline_tts#voice

[3].qtts.h文件参考. http://mscdoc.xfyun.cn/windows/api/iFlytekMSCReferenceManual/qtts_8h.html#details

[4].msp_cmn.h文件参考. http://mscdoc.xfyun.cn/windows/api/iFlytekMSCReferenceManual/msp__cmn_8h.html


0x06 问题反馈

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

发表评论

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