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文件如下:

#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的调用流程,如下图所示:

科大讯飞离线语音合成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文件播放出来就行了。具体修改地方,如下图所示:

修改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小课堂的微信群,与更多志同道合的小伙伴一起学习和交流!

发表评论