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。

验证步骤与判定标准:

  1. 程序启动后,下拉框应列出至少一个系统音色(Windows 通常有 Microsoft Huihui / Kangkang 等;macOS 有多种语言音色)。
  2. 从下拉框选择不同音色,状态栏立即显示“已切换: [音色名]”,且无报错。
  3. 输入文本后点击“合成并播放”,应听到所选音色的语音;切换另一音色再次合成,声音特质应明显不同(如音调、性别或语言)。
  4. 连续快速切换音色并合成 3 次以上,程序不崩溃,每次均能正常播放。
多音色切换时序与数据流 用户界面 引擎控制 pyttsx3 引擎 选择音色 setProperty('voice', id) 切换内部音色 点击合成 save_to_file(text, tmp) 生成WAV文件 播放音频 runAndWait() 音频输出设备 关键验证点 ① 每次切换后引擎 voice.id 已更新 ② 合成时使用当前选中音色 ③ 连续切换不崩溃

判定标准

  • 功能通过:下拉框可选 ≥2 种不同音色(若系统仅 1 种,需在 Windows 设置中添加语音包),切换后合成声音明显不同。
  • 健壮性通过:快速切换 5 次以上再合成,无异常退出或卡死。
  • 代码自洽:音色切换通过 engine.setProperty('voice', voice_id) 实现,合成前再次读取当前下拉索引确保一致性。