7.语音板使用科大讯飞离线语音合成
0x00 离线语音合成介绍
语音合成技术(Text to speech,简称TTS),是可以将文本转换成语音文件的技术。而且合成的语音文件,可以根据不同需要而合成出不同音色、语速和语调的声音,让合成的语音就跟真人说话的声音几乎一样。利用这项技术我们就可以让我们的机器人、电脑、手机、音箱等各种电子设备多了一种更为自然的方式来与人们进行交互。现在我们大家接触最多的语音合成技术,可能就是用高德导航的时候,它会通过语音给我们播报,指引我们到达目的地。当然还有一些其他应用场景,例如各种办公大厅里的叫号系统,机场、医院的语音播报,很多基本上都是用这种语音合成技术来实现的。
语音合成分为在线语音合成和离线的语音合成,在线语音合成使用的是在线引擎(TYPE_CLOUD),又称为云端模式,它需要使用网络,速度稍慢,并产生一定流量,但有更好的识别和合成的效果,如更高的识别匹配度,更多的发音人等。在默认情况下,在线语音合成引擎,提供5种免费的发音人供大家使用,如果要想使用更多的发音人,那就需要收费了:
离线语音合成使用的是离线引擎(TYPE_LOCAL),又称为本地模式,不需要使用网络,且识别和合成的速度更快,但同时要求购买并使用对应的离线资源(下载对应离线功能的SDK包)。那离线语音合成默认就提供2个发音人,如果要想使用其他离线发音人,那就需要收费了:
0x01 下载离线语音合成SDK
这里下载科大讯飞的离线语音合成SDK,仍然是比较简单的,只要去讯飞开放平台上下载即可,具体网址如下:
https://www.xfyun.cn/sdk/dispatcher
当我们下载好SDK后,就可以将SDK发送树莓派上解压使用了,完整的发送命令如下,大家需要根据自己的文件名和树莓派的IP地址做修改即可:
scp Linux_aisound1226_5d5b9efd.zip corvin@192.168.3.46:~/
0x02 编译离线语音合成测试源码
在树莓派上我们将SDK解压,然后可以先浏览一下整个SDK的组成架构,方便我们后面进行修改:
下面就是该使用树莓派版本的语音合成库了,这里的动态库文件跟前面介绍的语音唤醒、命令词识别都是一个库。当下载好该离线命令词识别库后,就可以将其放到树莓派中来使用了,这里下载后该库是可以永久使用的,没有时间限制:
接下来就可以来修改编译相关的脚本和makefile文件了,首先来修改一下bash脚本,修改过程如下:
修改好编译脚本后,接下来就是修改Makefile文件了。因为这里gcc编译选项需要修改,这里在引用动态库的路径需要修改一下就可以了。主要就是修改13行的LDFLAGS参数,修改后的Makefile文件如下:
#common makefile header
DIR_INC = ../../include
DIR_BIN = ../../bin
DIR_LIB = ../../libs
TARGET = tts_offline_sample
BIN_TARGET = $(DIR_BIN)/$(TARGET)
CROSS_COMPILE =
CFLAGS = -g -Wall -I$(DIR_INC)
LDFLAGS := -L$(DIR_LIB)/
LDFLAGS += -lmsc -lrt -ldl -lpthread -lstdc++
OBJECTS := $(patsubst %.c,%.o,$(wildcard *.c))
$(BIN_TARGET) : $(OBJECTS)
$(CROSS_COMPILE)gcc $(CFLAGS) $^ -o $@ $(LDFLAGS)
%.o : %.c
$(CROSS_COMPILE)gcc -c $(CFLAGS) $< -o $@
clean:
@rm -f *.o $(BIN_TARGET)
.PHONY:clean
#common makefile foot
做好一切准备后,接下来就可以来编译sample代码了。编译就是执行make.sh脚本即可,生成的执行文件是在bin目录下:
我们可以尝试安装gcc编译警告提示来修改一些tts_offline_sample.c源码,就是在最开始包含头文件地方,新增一个#include <string.h>,然后再编译看看效果:
0x03 运行测试程序
当编译生成了可执行文件后,我们就可以来运行下看看效果是什么样的了,如下视频所示:
看过演示视频后,我们要想修改这个源码的前提是要先明白现在的代码流程才行。这里首先要认识的一个重要流程就是科大讯飞离线语音合成API的调用流程,如下图所示:
在认识了合成语音的流程后,我们就可以来修改源码了。现在的代码流程是将固定的文本合成一个wav文件,然后我们再播放。现在我们修改成可以在运行程序时,从运行参数argv[1]中获取要合成的文本,然后在合成wav文件后,可以自动的播放出来。那最终修改后的完整代码如下:
/*
* 语音合成(Text To Speech,TTS)技术能够自动将任意文字实时转换为连续的
* 自然语音,是一种能够在任何时间、任何地点,向任何人提供语音信息服务的
* 高效便捷手段,非常符合信息时代海量数据、动态更新和个性化查询的需求。
*/
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include "../../include/qtts.h"
#include "../../include/msp_cmn.h"
#include "../../include/msp_errors.h"
typedef int SR_DWORD;
typedef short int SR_WORD ;
/* wav音频头部格式 */
typedef struct _wave_pcm_hdr
{
char riff[4]; // = "RIFF"
int size_8; // = FileSize - 8
char wave[4]; // = "WAVE"
char fmt[4]; // = "fmt "
int fmt_size; // = 下一个结构体的大小 : 16
short int format_tag; // = PCM : 1
short int channels; // = 通道数 : 1
int samples_per_sec; // = 采样率 : 8000 | 6000 | 11025 | 16000
int avg_bytes_per_sec; // = 每秒字节数 : samples_per_sec * bits_per_sample / 8
short int block_align; // = 每采样点字节数 : wBitsPerSample / 8
short int bits_per_sample; // = 量化比特数: 8 | 16
char data[4]; // = "data";
int data_size; // = 纯数据长度 : FileSize - 44
} wave_pcm_hdr;
/* 默认wav音频头部数据 */
wave_pcm_hdr default_wav_hdr =
{
{ 'R', 'I', 'F', 'F' },
0,
{'W', 'A', 'V', 'E'},
{'f', 'm', 't', ' '},
16,
1,
1,
16000,
32000,
2,
16,
{'d', 'a', 't', 'a'},
0
};
/* 文本合成 */
int text_to_speech(const char* src_text, const char* des_path, const char* params)
{
int ret = -1;
FILE* fp = NULL;
const char* sessionID = NULL;
unsigned int audio_len = 0;
wave_pcm_hdr wav_hdr = default_wav_hdr;
int synth_status = MSP_TTS_FLAG_STILL_HAVE_DATA;
if (NULL == src_text || NULL == des_path)
{
printf("params is error!\n");
return ret;
}
fp = fopen(des_path, "wb");
if (NULL == fp)
{
printf("open %s error.\n", des_path);
return ret;
}
/* 开始合成 */
sessionID = QTTSSessionBegin(params, &ret);
if (MSP_SUCCESS != ret)
{
printf("QTTSSessionBegin failed, error code: %d.\n", ret);
fclose(fp);
return ret;
}
ret = QTTSTextPut(sessionID, src_text, (unsigned int)strlen(src_text), NULL);
if (MSP_SUCCESS != ret)
{
printf("QTTSTextPut failed, error code: %d.\n",ret);
QTTSSessionEnd(sessionID, "TextPutError");
fclose(fp);
return ret;
}
printf("正在合成 ...\n");
fwrite(&wav_hdr, sizeof(wav_hdr) ,1, fp); //添加wav音频头,使用采样率为16000
while (1)
{
/* 获取合成音频 */
const void* data = QTTSAudioGet(sessionID, &audio_len, &synth_status, &ret);
if (MSP_SUCCESS != ret)
break;
if (NULL != data)
{
fwrite(data, audio_len, 1, fp);
wav_hdr.data_size += audio_len; //计算data_size大小
}
if (MSP_TTS_FLAG_DATA_END == synth_status)
break;
}
printf("\n");
if (MSP_SUCCESS != ret)
{
printf("QTTSAudioGet failed, error code: %d.\n",ret);
QTTSSessionEnd(sessionID, "AudioGetError");
fclose(fp);
return ret;
}
/* 修正wav文件头数据的大小 */
wav_hdr.size_8 += wav_hdr.data_size + (sizeof(wav_hdr) - 8);
/* 将修正过的数据写回文件头部,音频文件为wav格式 */
fseek(fp, 4, 0);
fwrite(&wav_hdr.size_8,sizeof(wav_hdr.size_8), 1, fp); //写入size_8的值
fseek(fp, 40, 0); //将文件指针偏移到存储data_size值的位置
fwrite(&wav_hdr.data_size,sizeof(wav_hdr.data_size), 1, fp); //写入data_size的值
fclose(fp);
fp = NULL;
/* 合成完毕 */
ret = QTTSSessionEnd(sessionID, "Normal");
if (MSP_SUCCESS != ret)
{
printf("QTTSSessionEnd failed, error code: %d.\n",ret);
}
return ret;
}
int main(int argc, char* argv[])
{
int ret = MSP_SUCCESS;
const char* login_params = "appid = 5d5b9efd, work_dir = .";//登录参数,appid与msc库绑定,请勿随意改动
/*
* rdn: 合成音频数字发音方式
* volume: 合成音频的音量
* pitch: 合成音频的音调
* speed: 合成音频对应的语速
* voice_name: 合成发音人
* sample_rate: 合成音频采样率
* text_encoding: 合成文本编码格式
*/
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";
const char* filename = "tts_sample.wav"; //合成的语音文件名称
if(argc < 2)
{
printf("Please input text to compose !\n");
return 1;
}
/* 用户登录 */
ret = MSPLogin(NULL, NULL, login_params); //第一个参数是用户名,第二个参数是密码,第三个参数是登录参数,用户名和密码可在http://www.xfyun.cn注册获取
if (MSP_SUCCESS != ret)
{
printf("MSPLogin failed, error code: %d.\n", ret);
goto exit ;//登录失败,退出登录
}
/* 文本合成 */
printf("开始合成 ...\n");
ret = text_to_speech(argv[1], filename, session_begin_params);
if (MSP_SUCCESS != ret)
{
printf("text_to_speech failed, error code: %d.\n", ret);
}
printf("合成完毕,现在开始播放合成的语音文件:\n");
system("play tts_sample.wav");
exit:
MSPLogout(); //退出登录
return 0;
}
其实这里修改的代码很简单,就是把main()函数中修改了一下。首先判断一下在执行程序的时候,是否有带参数。接下来就是在text_to_speech()函数中,第一个参数为要合成的文本,直接用argv[1]。最后就是在合成完毕后,直接用system()函数来执行播放命令,把合成好的wav文件播放出来就行了。具体修改地方,如下图所示:
修好好代码,我们就可以编译来测试了,通过下面视频来查看语音合成效果:
这里通过视频可以查看到离线语音合成的效果,我认为还是很好的,合成迅速,播放清晰。这里特别要注意的是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小课堂的微信群,与更多志同道合的小伙伴一起学习和交流!
[wshop_reward]