2. 多音色选择与切换
任务说明
本案例实现一个带图形界面的多音色语音合成器:用户通过下拉菜单选择不同音色,点击“合成”按钮后,系统调用语音合成引擎生成对应音色的音频文件并自动播放。核心目标是验证音色参数能够正确传递至合成引擎,并实现运行时无缝切换。
实现方案
采用 pyttsx3 作为语音合成引擎(支持 Windows SAPI5 / macOS NSSpeech / Linux eSpeak),通过 tkinter 构建界面。音色切换通过动态修改引擎的 voice.id 属性完成,每次合成前重新设置音色,确保切换即时生效。代码包含完整的音色枚举、合成回调与异常处理,不依赖任何未展示的外部代码。
代码实现
import tkinter as tk
from tkinter import ttk, messagebox
import pyttsx3
import threading
import os
import tempfile
class VoiceSynthesizer:
def __init__(self, root):
self.root = root
self.root.title("多音色语音合成器")
self.root.geometry("480x220")
# 初始化引擎(惰性加载,避免启动阻塞)
self.engine = None
self.voices = []
self._init_engine()
# 界面控件
ttk.Label(root, text="选择音色:").grid(row=0, column=0, padx=10, pady=15, sticky="w")
self.voice_combo = ttk.Combobox(root, state="readonly", width=40)
self.voice_combo.grid(row=0, column=1, padx=10, pady=15)
self.voice_combo.bind("<<ComboboxSelected>>", self._on_voice_change)
ttk.Label(root, text="输入文本:").grid(row=1, column=0, padx=10, pady=5, sticky="w")
self.text_entry = ttk.Entry(root, width=42)
self.text_entry.grid(row=1, column=1, padx=10, pady=5)
self.text_entry.insert(0, "你好,欢迎体验多音色语音合成。")
self.speak_btn = ttk.Button(root, text="合成并播放", command=self._on_speak)
self.speak_btn.grid(row=2, column=1, padx=10, pady=20, sticky="e")
self.status_label = ttk.Label(root, text="就绪", foreground="gray")
self.status_label.grid(row=3, column=0, columnspan=2, pady=5)
# 填充音色列表
self._populate_voices()
def _init_engine(self):
"""延迟初始化引擎,避免UI卡顿"""
try:
self.engine = pyttsx3.init()
self.voices = self.engine.getProperty('voices')
except Exception as e:
messagebox.showerror("初始化失败", f"语音引擎加载错误:\n{e}")
self.root.destroy()
def _populate_voices(self):
"""将系统音色名称填充到下拉框"""
if not self.voices:
self.voice_combo['values'] = ["(无可用音色)"]
self.voice_combo.current(0)
self.speak_btn.config(state="disabled")
return
names = [f"{i}: {v.name}" for i, v in enumerate(self.voices)]
self.voice_combo['values'] = names
self.voice_combo.current(0)
self._set_voice(0)
def _on_voice_change(self, event=None):
"""下拉框切换时立即更新引擎音色"""
idx = self.voice_combo.current()
if idx >= 0 and idx < len(self.voices):
self._set_voice(idx)
self.status_label.config(text=f"已切换: {self.voices[idx].name}", foreground="green")
def _set_voice(self, idx):
"""核心音色切换逻辑"""
if self.engine and 0 <= idx < len(self.voices):
self.engine.setProperty('voice', self.voices[idx].id)
def _on_speak(self):
"""合成线程入口"""
text = self.text_entry.get().strip()
if not text:
messagebox.showwarning("输入为空", "请输入要合成的文本")
return
if not self.voices:
messagebox.showerror("无音色", "系统没有可用音色")
return
self.speak_btn.config(state="disabled")
self.status_label.config(text="合成中...", foreground="blue")
threading.Thread(target=self._synthesize, args=(text,), daemon=True).start()
def _synthesize(self, text):
"""后台执行合成并播放(音色已在UI切换时设置)"""
try:
# 确保当前选中音色生效(防止切换后未触发事件)
idx = self.voice_combo.current()
self._set_voice(idx)
# 保存到临时文件再播放,避免引擎阻塞UI
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_path = f.name
self.engine.save_to_file(text, tmp_path)
self.engine.runAndWait()
# 播放音频(Windows/macOS/Linux通用)
if os.name == "nt":
os.startfile(tmp_path) # Windows
elif os.name == "posix":
import subprocess
subprocess.Popen(["afplay", tmp_path] if os.uname().sysname == "Darwin"
else ["aplay", tmp_path])
self.root.after(0, lambda: self.status_label.config(
text=f"播放完成: {self.voices[idx].name}", foreground="green"))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("合成错误", str(e)))
finally:
self.root.after(0, lambda: self.speak_btn.config(state="normal"))
if __name__ == "__main__":
root = tk.Tk()
app = VoiceSynthesizer(root)
root.mainloop()
运行与验证
依赖安装:
pip install pyttsx3
# Linux 用户还需安装 espeak-ng(如: sudo apt install espeak-ng)
启动方式:保存为 voice_switcher.py,执行 python voice_switcher.py。
验证步骤与判定标准:
- 程序启动后,下拉框应列出至少一个系统音色(Windows 通常有 Microsoft Huihui / Kangkang 等;macOS 有多种语言音色)。
- 从下拉框选择不同音色,状态栏立即显示“已切换: [音色名]”,且无报错。
- 输入文本后点击“合成并播放”,应听到所选音色的语音;切换另一音色再次合成,声音特质应明显不同(如音调、性别或语言)。
- 连续快速切换音色并合成 3 次以上,程序不崩溃,每次均能正常播放。
判定标准
- 功能通过:下拉框可选 ≥2 种不同音色(若系统仅 1 种,需在 Windows 设置中添加语音包),切换后合成声音明显不同。
- 健壮性通过:快速切换 5 次以上再合成,无异常退出或卡死。
- 代码自洽:音色切换通过
engine.setProperty('voice', voice_id)实现,合成前再次读取当前下拉索引确保一致性。