C++实现Matcha-TTS中文语音合成:从文本预处理到音频生成的完整技术解析

1. 引言

语音合成(Text-to-Speech, TTS)技术是人工智能领域的重要分支,随着深度学习的发展,基于神经网络的TTS系统在音质和自然度方面取得了显著突破。本文将详细介绍如何在C++环境中实现基于Matcha-TTS的中文语音合成系统,包含完整的文本预处理、模型推理和音频后处理流程。

仓库地址:https://github.com/muggle-stack/e2e_voice

本项目是一个完整的中文智能语音对话系统,集成了自动语音识别(ASR)、大语言模型(LLM)和文本转语音(TTS)功能,支持实时语音交互。

完整的语音对话链路

  • ASR (语音识别):基于SenseVoice模型的中文语音识别
  • LLM (大语言模型):集成Ollama支持多种开源模型
  • TTS (文本转语音):基于Matcha-TTS的高质量语音合成
  • 流式处理:LLM流式输出+实时TTS播放,自然对话体验

技术特性

  • C++高性能实现:使用ONNX Runtime进行模型推理
  • 模块化设计:ASR、LLM、TTS可独立使用或组合
  • 多线程优化:并行处理提升响应速度
  • 有序音频播放:确保TTS按句子顺序播放
  • 自动模型管理:首次运行自动下载所需模型

音频处理

  • 多种VAD算法:支持能量VAD和Silero VAD
  • 多设备支持:支持各种音频输入设备
  • 自动重采样:支持多种采样率自动转换
  • 实时音频队列:保证音频播放的连续性和顺序性

本文仅介绍 TTS 的CPP前后处理实现:

2. 系统架构概述

我们的TTS系统采用经典的两阶段架构:

文本输入 → 文本预处理 → 声学模型(Matcha) → 梅尔频谱 → 声码器(Vocos) → 音频波形

2.1 核心组件

  • 文本预处理模块:中文分词、音素转换、Token映射
  • Matcha声学模型:文本到梅尔频谱的转换
  • Vocos声码器:频谱到音频波形的重建
  • ISTFT后处理:频域到时域的高质量转换

3. 文本预处理实现

3.1 中文文本标准化

中文文本预处理是TTS系统的关键步骤,需要处理标点符号标准化、分词和音素转换:

std::string preprocessText(const std::string& text) {
    std::string processed = text;
    
    std::regex punct_re1(":|、|;");
    processed = std::regex_replace(processed, punct_re1, ",");
    std::regex punct_re2("[.]");
    processed = std::regex_replace(processed, punct_re2, "。");
    std::regex punct_re3("[?]");
    processed = std::regex_replace(processed, punct_re3, "?");
    std::regex punct_re4("[!]");
    processed = std::regex_replace(processed, punct_re4, "!");
    
    return processed;
}

3.2 Jieba中文分词集成

使用Jieba进行精确的中文分词,这对后续的音素映射至关重要:

void initializeJieba() {
    // 初始化Jieba分词器
    std::string dict_path = config_.dict_dir + "/jieba.dict.utf8";
    std::string hmm_path = config_.dict_dir + "/hmm_model.utf8";
    std::string user_dict = config_.dict_dir + "/user.dict.utf8";
    std::string idf_path = config_.dict_dir + "/idf.utf8";
    std::string stop_words = config_.dict_dir + "/stop_words.utf8";
    
    jieba_ = std::make_unique<cppjieba::Jieba>(
        dict_path, hmm_path, user_dict, idf_path, stop_words
    );
}

std::vector<std::string> segmentText(const std::string& text) {
    std::vector<std::string> words;
    jieba_->Cut(text, words, true);  // 使用HMM模式
    return words;
}

3.3 词汇到音素的映射

实现词汇查询和音素转换的核心逻辑:

std::vector<int64_t> convertWordToIds(const std::string& word) {
    // 将词转换为小写进行查询(遵循sherpa-onnx规范)
    std::string lower_word = word;
    std::transform(lower_word.begin(), lower_word.end(), lower_word.begin(), ::tolower);
    
    // 1. 优先查询词典
    auto lex_it = lexicon_.find(lower_word);
    if (lex_it != lexicon_.end()) {
        return convertPhonemesToIds(lex_it->second);
    }
    
    // 2. 直接Token查询
    auto token_it = token_to_id_.find(word);
    if (token_it != token_to_id_.end()) {
        return {token_it->second};
    }
    
    // 3. 标点符号处理
    if (isPunctuation(word)) {
        std::string punct_token = mapPunctuation(word);
        if (!punct_token.empty()) {
            auto punct_it = token_to_id_.find(punct_token);
            if (punct_it != token_to_id_.end()) {
                return {punct_it->second};
            }
        }
    }
    
    // 4. 字符级回退处理
    std::vector<int64_t> result;
    std::vector<std::string> chars = splitUtf8(word);
    for (const auto& char_str : chars) {
        auto char_lex_it = lexicon_.find(char_str);
        if (char_lex_it != lexicon_.end()) {
            auto char_ids = convertPhonemesToIds(char_lex_it->second);
            result.insert(result.end(), char_ids.begin(), char_ids.end());
        }
    }
    
    return result;
}

3.4 音素到Token ID转换

实现音素序列到模型Token ID的精确映射:

std::vector<int64_t> convertPhonemesToIds(const std::string& phonemes) {
    std::vector<int64_t> ids;
    std::istringstream iss(phonemes);
    std::string phone;
    
    while (iss >> phone) {
        auto token_it = token_to_id_.find(phone);
        if (token_it != token_to_id_.end()) {
            ids.push_back(token_it->second);
        } else {
            // 音素映射回退策略
            std::string mapped_phone = mapPhoneme(phone);
            if (mapped_phone != phone) {
                auto mapped_it = token_to_id_.find(mapped_phone);
                if (mapped_it != token_to_id_.end()) {
                    ids.push_back(mapped_it->second);
                }
            }
        }
    }
    
    return ids;
}

4. Matcha声学模型推理

4.1 添加Blank Tokens

Matcha模型要求在音素间插入空白标记,使用从模型元数据中读取的pad_id:

std::vector<int64_t> addBlankTokens(const std::vector<int64_t>& tokens) {
    // Matcha模型期望在音素间插入blank tokens
    // 格式: [pad_id, token1, pad_id, token2, pad_id, ...]
    std::vector<int64_t> result(tokens.size() * 2 + 1, pad_id_);
    
    int32_t i = 1;
    for (auto token : tokens) {
        result[i] = token;
        i += 2;
    }
    
    return result;
}

4.2 声学模型推理实现

std::vector<float> runAcousticModel(const std::vector<int64_t>& tokens, 
                                  int speaker_id, float speed) {
    // 准备输入张量
    std::vector<int64_t> token_shape = {1, static_cast<int64_t>(tokens.size())};
    std::vector<int64_t> length_data = {static_cast<int64_t>(tokens.size())};
    std::vector<int64_t> length_shape = {1};
    std::vector<float> noise_scale_data = {config_.noise_scale};
    std::vector<float> length_scale_data = {speed * config_.length_scale};
    
    auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    
    // 创建输入张量
    std::vector<Ort::Value> input_tensors;
    
    // Input 0: x (tokens序列)
    input_tensors.emplace_back(Ort::Value::CreateTensor<int64_t>(
        memory_info, const_cast<int64_t*>(tokens.data()), tokens.size(),
        token_shape.data(), token_shape.size()
    ));
    
    // Input 1: x_length (序列长度)
    input_tensors.emplace_back(Ort::Value::CreateTensor<int64_t>(
        memory_info, length_data.data(), 1,
        length_shape.data(), length_shape.size()
    ));
    
    // Input 2: noise_scale (噪声尺度)
    input_tensors.emplace_back(Ort::Value::CreateTensor<float>(
        memory_info, noise_scale_data.data(), 1,
        noise_scale_shape.data(), noise_scale_shape.size()
    ));
    
    // Input 3: length_scale (长度尺度,控制语速)
    input_tensors.emplace_back(Ort::Value::CreateTensor<float>(
        memory_info, length_scale_data.data(), 1,
        length_scale_shape.data(), length_scale_shape.size()
    ));
    
    // 执行推理
    const char* model_input_names[] = {"x", "x_length", "noise_scale", "length_scale"};
    const char* model_output_names[] = {"mel"};
    
    auto output_tensors = acoustic_model_->Run(
        Ort::RunOptions{nullptr},
        model_input_names, input_tensors.data(), 4,
        model_output_names, 1
    );
    
    // 提取梅尔频谱数据
    float* mel_data = output_tensors[0].GetTensorMutableData<float>();
    auto mel_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
    size_t mel_size = std::accumulate(mel_shape.begin(), mel_shape.end(),
                                     1, std::multiplies<size_t>());
    
    return std::vector<float>(mel_data, mel_data + mel_size);
}

5. Vocos声码器和音频重建

5.1 Vocos神经声码器推理

Vocos声码器将梅尔频谱转换为频域复数表示:

std::vector<float> vocoderInference(Ort::Session& session, 
                                  const std::vector<float>& mel, int mel_dim) {
    // 准备输入张量
    int64_t num_frames = mel.size() / mel_dim;
    std::vector<int64_t> input_shape = {1, mel_dim, num_frames};
    
    auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
        memory_info, const_cast<float*>(mel.data()), mel.size(),
        input_shape.data(), input_shape.size()
    );
    
    // Vocos输出三个张量:magnitude, x (real), y (imag)
    const char* input_names[] = {"mels"};
    const char* output_names[] = {"mag", "x", "y"};
    
    auto output_tensors = session.Run(Ort::RunOptions{nullptr}, 
                                     input_names, &input_tensor, 1,
                                     output_names, 3);
    
    // 提取频域数据
    float* mag_data = output_tensors[0].GetTensorMutableData<float>();
    float* x_data = output_tensors[1].GetTensorMutableData<float>();
    float* y_data = output_tensors[2].GetTensorMutableData<float>();
    
    auto vocoder_shape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
    int32_t batch_size = vocoder_shape[0];
    int32_t n_fft_bins = vocoder_shape[1];  // 513 = (1024/2 + 1)
    int32_t vocoder_frames = vocoder_shape[2];
    
    return reconstructAudio(mag_data, x_data, y_data, n_fft_bins, vocoder_frames);
}

5.2 复数STFT重建和ISTFT处理

实现频域到时域的高质量音频重建:

std::vector<float> reconstructAudio(float* mag_data, float* x_data, float* y_data,
                                  int32_t n_fft_bins, int32_t vocoder_frames) {
    // 重建复数STFT:complex = magnitude * (x + j*y)
    std::vector<float> stft_real(vocoder_frames * n_fft_bins);
    std::vector<float> stft_imag(vocoder_frames * n_fft_bins);
    
    // 数据布局转换:(batch, freq, time) → (time, freq)
    for (int32_t frame = 0; frame < vocoder_frames; ++frame) {
        for (int32_t bin = 0; bin < n_fft_bins; ++bin) {
            int32_t vocoder_idx = bin * vocoder_frames + frame;  // (freq, time)
            int32_t stft_idx = frame * n_fft_bins + bin;         // (time, freq)
            
            stft_real[stft_idx] = mag_data[vocoder_idx] * x_data[vocoder_idx];
            stft_imag[stft_idx] = mag_data[vocoder_idx] * y_data[vocoder_idx];
        }
    }
    
    // 使用FFTW执行ISTFT
    return performISTFT(stft_real, stft_imag, n_fft_bins, vocoder_frames);
}

5.3 FFTW ISTFT实现

使用FFTW库进行高效的逆短时傅里叶变换:

std::vector<float> performISTFT(const std::vector<float>& stft_real,
                               const std::vector<float>& stft_imag,
                               int32_t n_fft_bins, int32_t vocoder_frames) {
    int32_t n_fft = 1024;
    int32_t hop_length = 256;
    int32_t win_length = 1024;
    
    // 计算输出音频长度
    int32_t audio_length = n_fft + (vocoder_frames - 1) * hop_length;
    std::vector<float> audio(audio_length, 0.0f);
    
    // 创建Hann窗
    std::vector<float> window(win_length);
    for (int32_t i = 0; i < win_length; ++i) {
        window[i] = 0.5f * (1.0f - std::cos(2.0f * M_PI * i / (win_length - 1)));
    }
    
    // 处理每一帧
    for (int32_t frame = 0; frame < vocoder_frames; ++frame) {
        // 分配FFTW内存
        fftwf_complex* in = fftwf_alloc_complex(n_fft / 2 + 1);
        float* out = fftwf_alloc_real(n_fft);
        fftwf_plan plan = fftwf_plan_dft_c2r_1d(n_fft, in, out, FFTW_ESTIMATE);
        
        // 填充复数数据
        const float *p_real = stft_real.data() + frame * n_fft_bins;
        const float *p_imag = stft_imag.data() + frame * n_fft_bins;
        
        for (int32_t i = 0; i < n_fft_bins; ++i) {
            in[i][0] = p_real[i];  // 实部
            in[i][1] = p_imag[i];  // 虚部
        }
        
        // 执行IFFT
        fftwf_execute(plan);
        
        // IFFT标准化
        float scale = 1.0f / n_fft;
        for (int32_t i = 0; i < n_fft; ++i) {
            out[i] *= scale;
        }
        
        // 应用窗函数
        for (int32_t i = 0; i < win_length; ++i) {
            out[i] *= window[i];
        }
        
        // 重叠相加 (Overlap-Add)
        int32_t start_pos = frame * hop_length;
        for (int32_t i = 0; i < n_fft; ++i) {
            if (start_pos + i < audio_length) {
                audio[start_pos + i] += out[i];
            }
        }
        
        // 清理FFTW资源
        fftwf_destroy_plan(plan);
        fftwf_free(in);
        fftwf_free(out);
    }
    
    // 音频幅度调整
    float max_amplitude = *std::max_element(audio.begin(), audio.end());
    if (max_amplitude > 0.0f) {
        float scale = 0.8f / max_amplitude;  // 调整到80%幅度
        for (float& sample : audio) {
            sample *= scale;
        }
    }
    
    return audio;
}

6. 流式处理和音频队列管理

6.1 有序音频播放队列

为解决多线程TTS生成导致的播放顺序问题,实现有序音频队列:

class OrderedAudioQueue {
private:
    std::map<size_t, OrderedAudioData> audio_map_;  // 按顺序号存储
    size_t next_play_order_;  // 下一个要播放的顺序号
    std::mutex mutex_;
    std::condition_variable cv_;
    std::thread playback_thread_;
    
public:
    void enqueue(const OrderedAudioData& audio) {
        {
            std::lock_guard<std::mutex> lock(mutex_);
            audio_map_[audio.order] = audio;
        }
        cv_.notify_one();
    }
    
    void playbackWorker() {
        while (!stop_flag_) {
            OrderedAudioData audio;
            bool found = false;
            
            // 等待指定顺序的音频
            {
                std::unique_lock<std::mutex> lock(mutex_);
                cv_.wait(lock, [this] { 
                    return audio_map_.find(next_play_order_) != audio_map_.end() || stop_flag_; 
                });
                
                if (!stop_flag_) {
                    auto it = audio_map_.find(next_play_order_);
                    if (it != audio_map_.end()) {
                        audio = std::move(it->second);
                        audio_map_.erase(it);
                        found = true;
                        next_play_order_++;
                    }
                }
            }
            
            // 播放音频
            if (found && !audio.samples.empty()) {
                playAudioBlocking(audio.samples, audio.sample_rate);
            }
        }
    }
};

6.2 流式文本缓冲

实现智能的句子分割和缓冲机制:

class TextBuffer {
private:
    std::string buffer_;
    std::queue<std::string> sentences_;
    std::mutex mutex_;
    static const std::string CHINESE_PUNCTUATION; // "。!?;.!?;"
    
public:
    void addText(const std::string& text) {
        std::lock_guard<std::mutex> lock(mutex_);
        buffer_ += text;
        processBuffer();
    }
    
    void processBuffer() {
        std::string current_sentence;
        
        for (size_t i = 0; i < buffer_.size(); ) {
            // 处理UTF-8字符
            int char_len = getUTF8CharLength(buffer_[i]);
            
            if (i + char_len <= buffer_.size()) {
                std::string utf8_char = buffer_.substr(i, char_len);
                current_sentence += utf8_char;
                
                // 检查是否为句子结束符
                if (char_len == 1) {
                    // ASCII标点
                    if (isEndOfSentence(utf8_char[0])) {
                        addSentenceToQueue(current_sentence);
                        current_sentence.clear();
                    }
                } else {
                    // 中文标点
                    if (CHINESE_PUNCTUATION.find(utf8_char) != std::string::npos) {
                        addSentenceToQueue(current_sentence);
                        current_sentence.clear();
                    }
                }
            }
            i += char_len;
        }
        
        buffer_ = current_sentence;  // 保留未完成的句子
    }
};

7. 性能优化和最佳实践

7.1 内存管理优化

// 使用RAII管理ONNX会话
class TTSModel::Impl {
private:
    std::unique_ptr<Ort::Env> env_;
    std::unique_ptr<Ort::Session> acoustic_model_;
    std::unique_ptr<Ort::Session> vocoder_model_;
    
    // 预分配内存池
    std::vector<float> mel_buffer_;
    std::vector<int64_t> token_buffer_;
    
public:
    ~Impl() {
        // 自动清理资源
    }
};

7.2 多线程优化

// TTS生成与播放并行处理
void generateAndEnqueueOrderedTTS(const std::string& sentence, size_t order) {
    auto start_time = std::chrono::high_resolution_clock::now();
    
    // 并行生成TTS
    tts::GeneratedAudio generated_audio = tts_model_->generate(
        sentence, params_.tts_speaker_id, params_.tts_speed
    );
    
    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration<double>(end_time - start_time).count();
    
    // 添加到有序播放队列
    OrderedAudioData audio_data(
        std::move(generated_audio.samples), 
        generated_audio.sample_rate, 
        sentence, 
        order
    );
    audio_queue_->enqueue(audio_data);
}

7.3 错误处理和容错机制

bool TTSModel::Impl::initialize() {
    try {
        // 模型加载
        acoustic_model_ = std::make_unique<Ort::Session>(*env_, 
            config_.acoustic_model_path.c_str(), session_options);
        
        // 元数据读取
        extractModelMetadata();
        
        // 词典加载
        token_to_id_ = readTokenToIdMap(config_.tokens_path);
        lexicon_ = readLexicon(config_.lexicon_path);
        
        initialized_ = true;
        return true;
        
    } catch (const std::exception& e) {
        std::cerr << "TTS initialization failed: " << e.what() << std::endl;
        return false;
    }
}

8. 部署和集成建议

8.1 依赖管理

# CMakeLists.txt核心配置
find_package(PkgConfig REQUIRED)
pkg_check_modules(PORTAUDIO REQUIRED portaudio-2.0)
pkg_check_modules(FFTW3 REQUIRED fftw3f)

# ONNX Runtime配置
find_library(ONNXRUNTIME_LIB NAMES onnxruntime PATHS /usr/local/lib)

# 链接库配置
target_link_libraries(asr_llm_tts 
    ${ONNXRUNTIME_LIB}
    ${PORTAUDIO_LIBRARIES}
    ${FFTW3_LIBRARIES}
    pthread
)

8.2 模型文件管理

class TTSModelDownloader {
public:
    bool ensureModelsExist() {
        std::vector<ModelInfo> models = {
            {"matcha-icefall-zh-baker/model-steps-3.onnx", MATCHA_ZH_MODEL},
            {"vocos-22khz-univ.onnx", VOCOS_VOCODER},
            {"lexicon.txt", MATCHA_ZH_LEXICON},
            {"tokens.txt", MATCHA_ZH_TOKENS}
        };
        
        for (const auto& model : models) {
            if (!fileExists(getModelPath(model.type))) {
                if (!downloadModel(model)) {
                    return false;
                }
            }
        }
        return true;
    }
};

参考资源


Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐