6.3.3 语音合成集成实现

1.语音合成核心实现

功能包ros2_xf_bridge的src目录下,新建C++文件t2v.cpp,并编辑文件,输入如下内容:

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

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "ros2_xf_bridge/qtts.h"
#include "ros2_xf_bridge/msp_cmn.h"
#include "ros2_xf_bridge/msp_errors.h"

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

/* 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(">");
        usleep(150 * 1000); // 防止频繁占用CPU
    }
    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;
}

void voiceWordsCallback(const std_msgs::msg::String::ConstPtr &msg)
{
    char cmd[2000];
    const char *text;
    int ret = MSP_SUCCESS;
    const char *session_begin_params = "voice_name = xiaoyan, text_encoding = utf8, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 2";
    const char *filename = "tts_sample.wav"; // 合成的语音文件名称

    std::cout << "I heard :" << msg->data.c_str() << std::endl;
    text = msg->data.c_str();

    /* 文本合成 */
    printf("开始合成 ...\n");
    ret = text_to_speech(text, filename, session_begin_params);
    if (MSP_SUCCESS != ret)
    {
        printf("text_to_speech failed, error code: %d.\n", ret);
    }
    printf("合成完毕\n");

    popen("play tts_sample.wav", "r");
    sleep(1);
}

void toExit()
{
    printf("按任意键退出 ...\n");
    getchar();
    MSPLogout(); // 退出登录
}

int main(int argc, char *argv[])
{

    rclcpp::init(argc, argv);
    rclcpp::Rate loop_rate(10);
    auto node = std::make_shared<rclcpp::Node>("TextToSpeech_node");

    node->declare_parameter("appid","d7389bad");
    std::string lp =  "appid = " + node->get_parameter("appid").as_string() + ", work_dir = ."; // 登录参数,appid与msc库绑定,请勿随意改动
    const char *login_params = lp.c_str();


    int ret = MSP_SUCCESS;
    /*
     * rdn:           合成音频数字发音方式
     * volume:        合成音频的音量
     * pitch:         合成音频的音调
     * speed:         合成音频对应的语速
     * voice_name:    合成发音人
     * sample_rate:   合成音频采样率
     * text_encoding: 合成文本编码格式
     *
     */
    // const char *session_begin_params = "voice_name = xiaoyan, text_encoding = utf8, sample_rate = 16000, speed = 50, volume = 50, pitch = 50, rdn = 2";
    // const char *filename = "tts_sample.wav";                                                                                                                                                                                // 合成的语音文件名称
    // const char *text = "科大讯飞作为智能语音技术提供商,在智能语音技术领域有着长期的研究积累,并在中文语音合成、语音识别、口语评测等多项技术上拥有技术成果。科大讯飞是我国以语音技术为产业化方向的国家863计划产业化基地。"; // 合成文本

    /* 用户登录 */
    ret = MSPLogin(NULL, NULL, login_params); // 第一个参数是用户名,第二个参数是密码,第三个参数是登录参数,用户名和密码可在http://www.xfyun.cn注册获取
    if (MSP_SUCCESS != ret)
    {
        printf("MSPLogin failed, error code: %d.\n", ret);
        // goto exit; // 登录失败,退出登录
        toExit();
    }
    printf("\n###########################################################################\n");
    printf("## 语音合成(Text To Speech,TTS)技术能够自动将任意文字实时转换为连续的 ##\n");
    printf("## 自然语音,是一种能够在任何时间、任何地点,向任何人提供语音信息服务的  ##\n");
    printf("## 高效便捷手段,非常符合信息时代海量数据、动态更新和个性化查询的需求。  ##\n");
    printf("###########################################################################\n\n");


    // 创建一个订阅者
    auto wakeUpSub = node->create_subscription<std_msgs::msg::String>("ttswords", 10, voiceWordsCallback);
    rclcpp::spin(node);

exit:
    printf("按任意键退出 ...\n");
    getchar();
    MSPLogout(); // 退出登录

    return 0;
}

在上述文件中,与语音识别类似的,也需要注意参数appid的设置。

还有一行代码:

popen("play tts_sample.wav", "r");

它可以直接播放音频文件。

2.编辑配置文件

CMakeLists.txt 中需要添加的配置如下:

add_executable(t2v src/t2v.cpp)
target_include_directories(t2v PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
ament_target_dependencies(
  t2v
  "rclcpp"
  "std_msgs"
  "geometry_msgs"
)
target_link_libraries(t2v
  ${catkin_LIBRARIES}
  libmsc.so -ldl -pthread 
)
install(TARGETS t2v
  DESTINATION lib/${PROJECT_NAME})

3.编译

终端中进入当前工作空间,编译功能包:

colcon build --packages-select ros2_xf_bridge

4.执行

当前工作空间下,启动终端,并输入如下指令:

. install/setup.bash
ros2 run ros2_xf_bridge t2v

再新建一个终端,可以发布要被合成为语音的文本,指令如下:

ros2 topic pub -1 /ttswords std_msgs/msg/String "data: '你好'"

如无异常,第一个终端会播放第二个终端发布文本的对应语音。

results matching ""

    No results matching ""