一、项目释义:为什么需要一款纯本地 TTS 引擎?
在语音合成领域,云端 API 虽然方便,但延迟、隐私和数据主权始终是痛点。当你的应用场景是离线语音助手、隐私敏感的客服系统、或者需要极低延迟的实时对话机器人时,你需要的是一款能在本地跑起来的 TTS 引擎。
这个项目正是为此而生:它是一个用 C++17 编写的本地文本转语音引擎,底层推理由 GGML 张量库驱动,完整移植自 Qwen3-TTS 12Hz 模型。支持 11 种语言(含四川话、北京话等方言),输出 24kHz 单声道音频,可在 CPU、NVIDIA GPU、AMD/Intel GPU(Vulkan)、Apple Metal 上运行。
它的核心设计哲学非常明确:头文件即库、零运行时依赖、量化感知、流式输出。你不需要安装 PyTorch、不需要下载几百 MB 的 Python 环境包,只需要一个编译好的二进制文件 + 两个 GGUF 模型文件,就能在本地跑起一套完整的语音合成管线。
二、行业技术背景:从端到端语音合成到神经声码器
2.1 语音合成技术演进
传统 TTS 管线是串式的:文本分析 → 音素对齐 → 声学模型 → 声码器。每一步独立训练、独立调优,误差会逐级累积。而现代端到端方案(如 VITS、Bark、Qwen3-TTS)把整个管线打包成一个或几个联合训练的神经网络,直接从文本映射到音频波形,质量大幅提升。
Qwen3-TTS 采用的架构是LLM + 神经声码器的两阶段范式:
文本 → [Talker LM] → 语义代码序列 → [Code Predictor MTP] → 声学代码 → [RVQ Codec Decoder] → 音频波形2.2 关键组件拆解
| 组件 | 作用 | 技术选型 |
|---|---|---|
| Talker LM | 基于 Transformer 的语言模型,将文本和语音上下文映射为语义代码book序列 | Qwen3 架构,支持 KV Cache 推理 |
| Code Predictor (MTP) | Multi-Token Prediction 头,从语义代码预测 15 个声学代码/帧 | 轻量级预测头,与 LM 共享部分参数 |
| RVQ Codec | Residual Vector Quantizer 声码器,将离散代码重建为连续波形 | SEANet 编码器 + ConvNeXt + DAC v2 + RVQ |
| BPE Tokenizer | 文本分词器,将自然语言转为模型可处理的 token 序列 | 字节级 BPE,支持多语言 |
2.3 12Hz 帧率的含义
"12Hz"指的是声学代码的生成速率:每秒生成 12 帧代码。每帧包含 15 个声学代码(来自 RVQ 的 15 个 codebook),每个代码是一个离散索引。音频采样率为 24kHz,因此每帧对应 24000/12 = 2000 个采样点(约 83ms 的音频)。这种低帧率设计大幅减少了自回归生成的步数,是 TTS 推理速度的关键。
三、架构设计:从模型加载到音频输出的完整管线
3.1 整体工程结构
项目的源码组织非常清晰,按功能模块拆分为独立的头文件和源文件:
src/ ├── qwen.h / qwen.cpp # 公共 C ABI 接口层 ├── pipeline-tts.h / .cpp # TTS 主管线(Talker LM + Code Predictor) ├── pipeline-codec.h / .cpp # 声码器管线(RVQ 编解码) ├── backend.h # 后端抽象(CPU/CUDA/Metal/Vulkan) ├── kv-cache.h # KV Cache 实现 ├── bpe.h # BPE 分词器 ├── philox.h # Philox 随机数生成器 ├── gguf-weights.h # GGUF 权重加载器 ├── graph-arena.h # 计算图内存池 ├── causal-trans-conv.h # 因果转置卷积 ├── convnext-block.h # ConvNeXt 块 ├── dac-decoder-v2.h # DAC v2 解码器 ├── code-predictor-*.h # Code Predictor 相关 ├── quantizer-*.h # 量化/反量化 ├── audio-*.h # 音频 I/O、梅尔频谱、重采样 └── speaker-encoder-*.h # 说话人编码器If you need the complete source code, please add the WeChat number (c17865354792)
3.2 核心数据结构
从源码中可以看到,整个引擎围绕几个核心结构体构建:
PipelineTTS结构体—— 管线的核心上下文,持有所有模型权重、缓存和计算图:
structPipelineTTS{GGUFModel gguf_talker;// Talker 模型的 GGUF 加载器TalkerWeights talker;// Talker LM 权重CodePredictorWeights code_predictor;// Code Predictor MTP 头权重SpeakerEncoderWeights speaker_encoder;// 说话人编码器权重(懒加载)boolhas_speaker_encoder;// 是否加载了说话人编码器boolspk_enc_loaded;// 说话人编码器是否已驻留PipelineCodec codec;// RVQ 编解码管线std::string tokenizer_type;// 分词器类型std::string model_size;// 模型规模 ("0.6B" / "1.7B")std::string model_type;// 模型类型 ("base"/"customvoice"/"voicedesign")intnum_code_groups;// RVQ codebook 组数intmax_batch;// 批处理容量intcodec_chunk_frames;// 编解码帧块大小intcodec_left_ctx_frames;// 解码器左上下文BackendPair bp;// CPU/GPU 后端对ggml_backend_t backend;// GGML 后端ggml_backend_sched_t sched;// 后端调度器KVCache talker_kv;// Talker KV 缓存KVCache code_predictor_kv;// Code Predictor KV 缓存structggml_context*bridge_ctx;// Bridge 计算图上下文ggml_backend_buffer_t bridge_buf;// Bridge 后端缓冲区structggml_tensor*hidden_bridge;// 隐藏层桥接张量GraphArena talker_arena;// Talker 计算图内存池std::vector<TalkerDecodeGraph>talker_decode_graphs;// 解码图std::vector<CodePredGraphSet>cp_graphs;// Code Predictor 图集};qt_context结构体—— 公共 API 的不透明句柄,封装了完整的引擎状态:
structqt_context{BackendPair bp;// 计算后端PipelineTTS pt;// TTS 管线(已嵌入 Codec 管线)BPETokenizer tok;// BPE 分词器std::thread worker;// 计算工作线程std::mutex mu;// 线程同步锁std::condition_variable cv_work;// 工作通知std::condition_variable cv_done;// 完成通知std::deque<TtsJob*>queue;// 待处理任务队列std::deque<RefJob*>ref_queue;// 语音参考提取队列boolstop=false;};3.3 线程模型:单工作线程架构
源码中的线程模型非常简洁高效:
[调用线程] [Worker 线程] │ │ ├── qt_synthesize() ──enqueue──▶ │ TtsJob 入队 │ │ │ 等待帧边界 │ │ │ 批量 Talker decode │ │ │ 批量 Code Predictor │ │ │ Codec 流式解码 │ │◀──callback─── │ on_chunk 回调 │ │ │ 退休完成任务 │◀──audio─── │关键设计点:
- 单计算线程:
qt_context内部只有一个std::thread worker,所有 GGML 计算都在该线程上执行,避免了多线程竞争和锁开销 - 任务队列:
qt_synthesize将合成任务入队,worker 线程在帧边界处批量处理 - 懒加载说话人编码器:说话人编码器权重不会在初始化时加载,而是在第一次语音克隆请求时才懒加载,节省启动内存
四、核心技术深度解析
4.1 两阶段生成管线
TTS 的生成过程分为两个阶段,这是整个引擎性能的核心:
第一阶段:Talker LM 生成语义代码
Talker 是一个基于 Transformer 的语言模型,它的工作是:给定文本 prompt 和已有的语音上下文,自回归地生成语义代码序列。这个过程使用了 KV Cache 来加速自回归推理——每次生成新 token 时,不需要重新计算之前所有 token 的 key-value 对。
// pipeline-tts.h 中的 TtsEngine 批处理引擎接口structTtsEngine;TtsEngine*tts_engine_new(PipelineTTS*pt,BPETokenizer*tok);voidtts_engine_free(TtsEngine*e);// 接纳一个合成任务:提示词组装、参考处理、Talker 预填充booltts_engine_admit(TtsEngine*e,TtsJob*job);// 为每个活跃槽运行一帧:批量 Talker decode → c0 采样 →// 批量 Code Predictor → 流式 Codec 解码 → 退休完成槽voidtts_engine_step(TtsEngine*e,std::vector<TtsJob*>*retired);// 当前活跃槽数量inttts_engine_active(constTtsEngine*e);第二阶段:Code Predictor 预测声学代码
Talker 输出的语义代码还不够直接可用,需要一个 Code Predictor(基于 MTP 多token预测头)来进一步预测每帧的 15 个声学代码。这两个阶段都做了 KV Cache,所以自回归生成的瓶颈主要在矩阵乘法,而不是注意力计算。
4.2 流式合成:帧级流式 Codec 解码
这是该项目区别于很多同类实现的重要特性:流式输出。传统方案必须等整个文本生成完所有代码后,才一次性送入声码器解码。而这个项目实现了状态帧级流式编解码:
这意味着:
- 第一个音频回调在第一帧 Talker 生成后即刻触发
- 流式输出与离线全量解码结果位级一致(bit-identical)
- 对于实时对话场景,首包延迟可以控制在 83ms(一帧)以内
实现原理是利用 Codec Decoder(DAC v2)的因果滑动窗口特性:解码器只需要过去的上下文,不需要未来信息,因此可以逐帧解码而不会引入失真。
4.3 采样链:可复现的随机采样
语音合成需要随机性来避免机械感,但也需要可复现性来调试。项目实现了一个 HuggingFace 对齐的采样链:
Repetition Penalty → Temperature → Top-K → Top-P → Multinomial Sampling并且使用Philox PRNG(可种子化的伪随机数生成器):
// qwen.cpp 中的种子解析逻辑staticint64_tqt_resolve_seed(int64_tseed){if(seed>=0){returnseed;// 用户指定种子,直接转发}// -1 表示使用硬件随机源,保证非确定性std::random_device rd;return(int64_t)(((uint64_t)rd()<<32)^(uint64_t)rd());}固定种子可以完全复现同一段语音输出,这对调试和测试非常重要。
4.4 量化方案:Q8_0 与 Q4_K_M
模型量化是本地部署的核心技术。该项目支持两种量化格式:
| 量化格式 | 精度 | 适用场景 | 模型大小 |
|---|---|---|---|
| Q8_0 | 8-bit 均匀量化 | 高质量需求,GPU 推理 | 1.7B 模型约 3.5GB |
| Q4_K_M | 4-bit 混合量化 | 资源受限,CPU 推理 | 1.7B 模型约 1GB |
关键设计决策:RVQ 编解码路径保持 F32 精度。量化只作用于 Talker 主干网络,而声学代码book和投影层保持全精度,确保音频质量不受量化影响。
五、语音克隆与声音设计
5.1 零样本语音克隆
项目支持两种语音克隆模式:
- X-vector 模式:仅使用参考音频的说话人嵌入向量(speaker embedding),不需要参考文本
- In-Context 模式:同时提供参考音频和对应文本转录,效果更精确
底层实现通过qt_extract_voice_ref函数完成:
// qwen.cpp - 语音参考提取核心逻辑staticqt_statusqt_extract_voice_ref_run(qt_context*q,RefJob*job){// 懒加载说话人编码器(首次请求时加载权重)if(!q->pt.spk_enc_loaded){if(!speaker_encoder_weights_load(...)||q->pt.speaker_encoder.weight_buf==NULL){returnQT_STATUS_GENERATE_FAILED;}q->pt.spk_enc_loaded=true;}// 提取说话人嵌入向量std::vector<float>emb;speaker_encoder_extract(&q->pt.speaker_encoder,q->pt.sched,job->audio,job->n_samples,emb);// 编码为 RVQ 代码std::vector<int32_t>codes=pipeline_codec_encode(&q->pt.codec,job->audio,aligned_n);// 写入输出缓冲区job->out->ref_spk_emb=emb_copy;job->out->ref_codes=codes_copy;job->out->ref_T=ref_T;job->out->num_codebooks=num_codebooks;returnQT_STATUS_OK;}预编码优化:可以通过qwen-codec工具预先将参考音频编码为.spk(说话人嵌入)和.rvq(ICL 代码)文件,后续合成时直接加载这两个文件,跳过每次重新编码的开销。
5.2 声音设计(Voice Design)
除了克隆已有声音,项目还支持通过自然语言描述来"设计"声音:
--instruct "male, young adult, moderate pitch"模型会根据描述中的性别、年龄、音高、风格等属性,在隐空间中定位到对应区域并生成声音。这本质上是利用了在大规模语音数据上训练的说话人属性解耦能力。
5.3 内置说话人
项目预置了多个命名说话人,支持方言覆盖:
| 说话人 | 方言覆盖 |
|---|---|
| serena | 标准英语 |
| vivian | 标准英语 |
| uncle_fu | 标准英语 |
| ryan | 标准英语 |
| aiden | 标准英语 |
| ono_anna | 标准英语 |
| sohee | 标准英语 |
| eric | 四川话 |
| dylan | 北京话 |
六、环境配置与编译运行教程
6.1 硬件与软件要求
| 项目 | 说明 |
|---|---|
| 编译器 | C++17 兼容(GCC 8+/Clang 7+/MSVC 2019+) |
| CUDA | 11.x / 12.x(NVIDIA GPU) |
| Vulkan | 1.2+(AMD/Intel GPU) |
| Metal | macOS 10.15+(Apple GPU) |
| 构建工具 | CMake 3.15+ |
| 模型文件 | GGUF 格式(可从 HuggingFace 下载预转换版本) |
6.2 编译构建
# 克隆仓库(递归获取 ggml 子模块)gitclone --recurse-submodules<repo-url>cd<project-dir># NVIDIA GPU 版本./buildcuda.sh# AMD/Intel GPU (Vulkan)./buildvulkan.sh# 仅 CPU 版本./buildcpu.sh# 所有后端(运行时动态加载)./buildall.sh# 滚动发行版(如 Arch 使用 GCC 16)指定 CCBINNVCC_CCBIN=g++-13 ./buildcuda.sh# 替换 ggml 子模块为其他树(上游 ggml 或 llama.cpp/ggml)cmake-DGGML_SOURCE_DIR=<path>..6.3 模型准备
方式一:下载预转换的 GGUF 文件(推荐)
# 从 HuggingFace 下载预转换模型# 放到 models/ 目录下即可跳过转换步骤方式二:从原始 checkpoint 自行转换
# 下载原始 checkpoint./checkpoints.sh# 转换为 F32 GGUF./convert.py# 量化(BF16 / Q8_0 / Q4_K_M)# RVQ codebooks 和投影层保持 F32./quantize.sh需要两个 GGUF 文件:
- Talker 模型:
qwen-talker-{size}-{mode}-{variant}.gguf(包含 LM + Code Predictor MTP 头 + 可选说话人编码器) - Tokenizer:
qwen-tokenizer-12hz-{variant}.gguf(包含 SEANet + ConvNeXt + DAC v2 + RVQ)
三种模式:base(基础)、customvoice(命名说话人)、voicedesign(声音设计,仅 1.7B)
两种规模:0.6B和1.7B
6.4 运行测试
基础合成(默认声音):
./build/qwen\--modelmodels/qwen-talker-1.7b-base-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--langEnglish-oout.wav<prompt.txt语音克隆(零样本,参考音频 + 转录文本):
./build/qwen\--modelmodels/qwen-talker-1.7b-base-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--ref-wav ref.wav --ref-text ref.txt\--langEnglish-oout.wav<prompt.txt预编码参考优化(跳过每次编码开销):
# 第一步:预编码参考音频./build/qwen-codec--modelmodels/qwen-tokenizer-12hz-Q8_0.gguf\--talkermodels/qwen-talker-1.7b-base-Q8_0.gguf-iref.wav# 第二步:使用预编码结果合成./build/qwen\--modelmodels/qwen-talker-1.7b-base-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--ref-spk ref.spk --ref-rvq ref.rvq --ref-text ref.txt\--langEnglish-oout.wav<prompt.txt命名说话人:
./build/qwen\--modelmodels/qwen-talker-1.7b-customvoice-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--speakervivian\--langEnglish-oout.wav<prompt.txt声音设计:
./build/qwen\--modelmodels/qwen-talker-1.7b-voicedesign-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--instruct"male, young adult, moderate pitch"\--langEnglish-oout.wav<prompt.txt6.5 部署为 HTTP 服务(OpenAI 兼容)
./build/server\--modelmodels/qwen-talker-1.7b-base-Q8_0.gguf\--codecmodels/qwen-tokenizer-12hz-Q8_0.gguf\--aliasqwen3-tts-base--port8080注册克隆声音:
curl-XPOST localhost:8080/v1/audio/voices-H"Content-Type: application/json"\-d'{"name":"freeman","ref_text":"$(cat ref.txt)", "spk_b64":"$(base64 -w0 ref.spk)","rvq_b64":"$(base64 -w0 ref.rvq)"}'合成语音:
curl-XPOST localhost:8080/v1/audio/speech-H"Content-Type: application/json"\-d'{"input":"Hello world.","voice":"freeman","language":"English", "response_format":"wav","seed":42,"temperature":0.8}'-oout.wav支持采样参数覆盖:seed、max_new_tokens、temperature、top_k、top_p、repetition_penalty。
6.6 Docker 部署
项目提供 CPU 和 CUDA 两种 Docker 镜像,每个 release 自动构建发布:
# 参考 docs/DOCKER.md 获取详细镜像使用说明dockerrun-p8080:8080<image-name>七、C 语言嵌入:跨语言调用
该项目提供了一个纯 C 链接的公共 ABI,使得 C、C++、Python(ctypes)、Rust(bindgen)、Go(cgo) 等语言可以无缝调用:
#include"qwen.h"// 初始化上下文structqt_init_paramsiparams;qt_init_default_params(&iparams);iparams.talker_path="models/qwen-talker-1.7b-base-Q8_0.gguf";iparams.codec_path="models/qwen-tokenizer-12hz-Q8_0.gguf";structqt_context*q=qt_init(&iparams);// 配置合成参数structqt_tts_paramsparams;qt_tts_default_params(¶ms);params.text="Hello world.";params.lang="English";// 执行合成structqt_audioaudio={0};qt_synthesize(q,¶ms,&audio);// audio.samples, audio.n_samples, audio.sample_rate, audio.channels// 释放资源qt_audio_free(&audio);qt_free(q);共享库构建:cmake -DQWEN_SHARED=ON ...生成libqwen.so/.dll/.dylib,仅导出qt_*符号,内部pipeline_*和backend_*全部隐藏。
八、落地用途
8.1 离线语音助手
无需联网、无需云端 API 的本地语音合成,适合智能家居、车载系统等隐私敏感场景。CPU 版本即可运行,对硬件要求极低。
8.2 内容创作与配音
视频创作者、播客主播可以用语音克隆功能快速生成配音,无需录音棚和专业配音员。声音设计功能则允许"描述出"想要的声音风格。
8.3 游戏 NPC 对话
游戏开发中,NPC 的实时对话需要低延迟语音合成。流式合成特性确保首帧延迟仅 83ms,配合 GPU 加速可以实时生成大量 NPC 对话语音。
8.4 教育与无障碍
为视障用户提供文本朗读、为语言学习者提供多语言发音示范。方言支持(四川话、北京话)让地方语言内容创作成为可能。
8.5 嵌入式部署
由于是纯 C++17 + GGML 架构,没有 Python 依赖,可以编译嵌入到嵌入式系统中运行。配合 Q4_K_M 量化,1.7B 模型仅需约 1GB 内存,在边缘设备上可行。
九、总结与展望
这个项目展示了在纯 C++ 环境下,利用 GGML 张量库实现一套完整、高效、可量化的神经语音合成引擎的可行性。从架构上看,它巧妙地将 LLM 推理管线(Talker LM + KV Cache)与声学编解码管线(RVQ Codec)串联,通过两阶段生成策略在质量和速度之间取得了良好平衡。
值得关注的后续优化方向:
- 更多量化格式:引入 NF4、FP8 等新兴量化格式进一步压缩模型
- GPU 批处理优化:提升
max_batch下的吞吐,支持多请求并发 - 更多激活函数:引入 SwiGLU、GELU 等激活函数的量化友好实现
- 实时性优化:进一步降低首包延迟,优化流式编解码的帧对齐策略
- 多模态扩展:结合视觉模态实现图文到语音的跨模态合成
对于想要深入理解语音合成底层实现、或者需要在资源受限环境中部署 TTS 的开发者来说,这个项目提供了一个非常优秀的参考实现。