python调用SAPI生成wav音频文件

## 核心功能的实现
### 简单说明
    今天我们一起写出一个简单的文本转语音文件的小工具, 使用的是Microsoft Speech Object这个库, 也就是SAPI5。  
    通过这个例子你可以看到, 
* sapi的简单使用 
* python调用Windows的COM对象模型 
* python多任务处理和共享数据 
* 使用WXPython编写图形用户界面 

    你需要python语言的基础有助于理解下面所述!  
    我们暂且把这个项目命名为TTS_WAV, 我们的需求就是把文本内容转换为wav音频文件。

### 第一个例子, 让程序开口
    我们刚开始的时候目标不要太高, 但是基本架构必须有。  
所以我们的目标是在你的"tts2wav\src\v1.0\tts2wav.py"文件里写下如下内容:
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:tts2wav.py

import win32com.client # pywin32模块需要pip安装, python使用COM对象的时候用到

# TTS引擎封装在这个类里
class Engine:
  def __init__(self, content):
    self.content = content # 要朗读的内容
    self.spVoice = win32com.client.Dispatch("SAPI.SpVoice") # 创建SpVoice对象, 这是SAPI的核心对象, tts功能的核心

  # 朗读内容
  def Speak(self):
    self.spVoice.Speak(self.content)

'''
这样我们可以搞第一个测试了, 在当前目录下打开python交互解释器, 做如下测试:
'''
D:\Src\python\tts\tts2wav\src\v1.0>python
Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from tts2wav import Engine
>>> e = Engine("你好")
>>> e.Speak()
>>> e.content = "hello"
>>> e.Speak()
>>>
'''
    这样我们发现第一个问题解决了, 能喘气, 挺好!  
    接下来就是想办法把这音频数据写入到一个wav文件里。  
    我们可以使用SAPI.SpFileStream对象来完成这个任务。  
    直接给Engine类添加一个方法"ConvertWAV", 如下:
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:tts2wav.py

import win32com.client # pywin32模块需要pip安装, python使用COM对象的时候用到

# TTS引擎封装在这个类里
class Engine:
  def __init__(self, content):
    self.content = content # 要朗读的内容
    self.SpVoice = win32com.client.Dispatch("SAPI.SpVoice") # 创建SpVoice对象, 这是SAPI的核心对象, tts功能的核心
    self.SpFileStream = win32com.client.Dispatch("SAPI.SpFileStream") # 创建com对象文件流

  # 朗读内容
  def Speak(self):
    self.SpVoice.Speak(self.content)

# 转换到wav文件
  def ConvertWAV(self, fileName):
    self.SpFileStream.Open(fileName, 3, False) # 参数1文件路径, 参数2和参数3固定不变
    oldStream = self.SpVoice.AudioOutputStream # 备份原来的输出设备
    self.SpVoice.AudioOutputStream = self.SpFileStream # 设置文件流为输出设备
    self.SpVoice.Speak(self.content) # 开始转换, 和直接朗读没有区别, 只是更改了输出设备而已, 原来是默认声卡, 现在是文件流
    self.SpFileStream.Close()
    self.SpVoice.AudioOutputStream = oldStream # 把输出设备换回来
'''
然后准备一个full.txt的文本文件, 放到程序的当前目录下, 然后做如下测试:
test1.py文件的内容如下:
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:test1.py

from tts2wav import Engine

fp = open("full.txt", encoding="UTF-8")
content = fp.read()
fp.close()

engine = Engine(content)
engine.ConvertWAV("full.wav")
engine.content = "完成任务, Ok."
engine.Speak()
'''
    直接运行然后看看有没有在当前目录下有full.wav文件, 然后播放试试。  
    如果出错了, 回头在看看哪里出现的问题。
    源代码可以从这里下载:
http://39.104.103.44:8082/downloads/tts2wav_v1.0.zip

### 一些必要的说明
    我们发现在python里使用com对象还是非常容易的, 调用wi32com.client.Dispatch函数, 就可以了,  这样能拿到com对象的引用, 可以调用这个对象的属性和方法。  
这样的话我们通过com对象来实现很多有意思的功能, 当然掌握com对象模型非常难。  
    核心功能完成了, 接下来我们要做的事多任务场景下的tts调用。  
 



2021-08-10 15:53:22 查看数:349     回复数:2 显示全部楼层
## 多任务场景下的数据传递
   在运行转换的时候我们的用户界面不能卡死, 所以需要引入多任务处理, 当然朝思暮想的窗口还早, 把多任务给搞定后在来加窗口。  
   通常情况下多任务有两种实现, 轻量级的多线程和开销大的多进程, 根据情况选择使用就可以了。  
   这里我使用多线程调用SpVoice对象出错了, 至于什么原因无从探究, 一条路走不通赶紧换一个, 这个叫审时度势, 所以我们直接使用多进程来搞定即可!  
### 多任务下的数据共享和传递问题
   如果在单任务场景下我们使用数据没有任何问题, 首先所有的数据都在一个进程空间里, 然后也不可能出现多个执行流同时访问一个数据的可能, 但是一旦有了多任务情况就变的复杂了。  
   当然在python里这样的问题事不存在的, 我们可以使用一些技术完美解决多进程管理数据的问题。  
   比如我们的multiprocessing.Pipe, 这是一个好东西, 因为有这个我们的多任务处理显得特别简单。  
### 收发数据双方的约定
   在此之前我们约定一系列规定,如果你现在看也许一头雾水, 但是到最后你可能恍然大悟,  
* property, 设置或者获取某个属性
* command父进程下达的指令, 如果是kill那么子进程自我了结, 如果是close那么子进程关闭输出设备, 也许是某个wav文件
* text 发送要处理的文本
其中要说明的是, property, 这里至少包括了如下几个属性:
* voice 朗读角色
* voices 朗读角色列表
* rate 朗读速度
* audioOutput 输出设备, default, 默认声卡, path某个wav文件
这些规定也许根据需要增加或者减少。  
小时牛刀我们开始折腾吧。  
为了突出多任务你们期盼的窗口也来了, 先看最小用户界面。
gui.py
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:gui.py

# 最少的UI
import wx

# 窗口上的面板, 我的习惯把所有的控件都放到上面
class Panel(wx.Panel):
 # 顶层窗口的实力和tts引擎的实力
 def __init__(self, frame, engine):
   wx.Panel.__init__(self, frame)
   self.frame = frame # 顶层窗口的实力
   self.engine = engine # tts引擎的实力
   self.readButton = wx.Button(self, label="朗读(&R)") # 添加一个按钮
   self.readButton.Bind(wx.EVT_BUTTON, self.OnRead) # 绑定按钮的事件处理函数
   self.frame.Show(True) # 显示顶层窗口

 # 按钮的事件处理函数
 def OnRead(self, event):
   self.engine.Speak("大家好才是真的好, Very Good.")
'''
   功能就是有个按钮点一下就发送那个固定不变的字符串给engine子线程, 其他没有。  
   然后我们看一下tts引擎的代码, 这个相对来说比较复杂一些, 但这个已经最少了, 上面约定好的规定一个都没有使用。  
tts2wav.py
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:tts2wav.py

# 最简单的多任务架构下的tts引擎
import win32com.client
import threading
import multiprocessing
from queue import Queue # 在线程之间共享数据的, 先进后出的队列

# tts引擎, 多线程实现
class Engine(threading.Thread):
 def __init__(self):
   threading.Thread.__init__(self)
   self.setDaemon(True) # 设为守护线程
   self.cache = Queue() # 在多线程之间缓存数据
   self.mainPipe, self.subPipe = multiprocessing.Pipe() # 在多进程之间收发数据
   # 创建并且启动一个进程, 而且把调用mutltiprocessing.Pipe返回的第二个pipe实力传入新创建的进程
   # 这样两个进程可以同等的相互收发数据了
   task = multiprocessing.Process(target = Engine.Task, args = [self.subPipe]) # 这里的Task是Engine类的静态方法, 如果设为实力方法就会出错
   task.start()

 # 和Java一样线程的执行函数, 使用Thread父类的start方法来调用, 如果直接调用不会创建新线程
 def run(self):
   while True:
     self.mainPipe.send(self.cache.get()) # cache.get阻塞等待新的数据, 如果有新数据返回给pipe.send方法, 随后send把数据发送给另外一个pipe对象, 也就是我们创建的子进程

 # 我们的按钮单机的时候调用的就是这个方法, 在这个线程的缓存里放入一个数据
 def Speak(self, text):
   self.cache.put(text) # cache.get阻塞等待这个方法的调用

 # 子进程的任务在这个静态方法里, 参数就是从父进程传入的第二个pipe实力
 def Task(pipe):
   SpVoice = win32com.client.Dispatch("SAPI.SpVoice") # 创建SpVoice组件对象
   while True:
     SpVoice.Speak(pipe.recv()) # 这里的recv也是阻塞方法, 得到数据后就调用spVoice的Speak方法了
'''
   Engine作为独立线程运行, 而且创建了一个子进程。  
   这样最难的部分就过去了, 接下来就是主程序了, 非常简单, 无非就是创建wx应用程序, 创建tts引擎, 创建窗口, 然后启动就ok了。  
app.py
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:app.py

# 使用这个整合到一起
from tts2wav import Engine
from gui import Panel
import wx

if __name__ == "__main__":
 app = wx.App() # 初始化wx应用程序
 engine = Engine()
 engine.start()
 panel = Panel(wx.Frame(None, title="TTS2WAV"), engine) # 创建Panel传入一个窗口实力和tts引擎
 app.MainLoop() # 启动wx的事件循环
'''
   直接运行app.py看看效果如何!  
   虽然功能非常弱鸡, 但是我们已经证明tts引擎运行良好, 多任务架构也没有太大问题。  
   下一步我们开始搞出真正能只用的版本了, 我们发现从1.0到1.1基本架构完全变了, 但是接下来的所有升级完善都在这个基本面上继续。  
源代码可以从这里下载:
http://39.104.103.44:8082/downloads/tts2wav_v1.1.zip


2021-08-14 10:50:53 显示全部楼层
## 完整的核心功能
 经过上两篇的铺垫你应该非常容易的看明白下面的一些逻辑。  
### 操作码和操作参数
 我们的主进程通过发送操作码来让子进程工作的, 这些操作码大体上两类有参数和无参数的。  
 比如说, 有参数的设置tts引擎的参数, 无参数的获取tts引擎的参数值。  
 * VOLUME 15 设置tts引擎的音量为15, 如果无参数就返回tts引擎的音量, quit操作码可以让子进程结束运行。  
 完整的操作码定义请看/src/v1.2/tts_defines.py文件:
'''
# --*-- Encoding: UTF-8 --*--
#! fileName:tts_defines.py

# 定义几个常量, 这些常量是子进程的操作码
TTS_TEXT = 0
TTS_VOICES = 1
TTS_VOICE = 2
TTS_RATE = 3
TTS_VOLUME = 4
TTS_OPEN = 5
TTS_CLOSE = 6
TTS_QUIT = 7
TTS_ERROR = 9
TTS_OK = 10
TTS_BEGIN = 11
TTS_END = 12
TTS_PAUSE = 13
TTS_CONTINUE = 14

TTS_MOD_READ = "朗读模式"
TTS_MOD_WAV = "输出到wav文件模式"
'''
 这些操作码的含义不言而喻的, 有了这些我们可以非常灵活的控制子进程了。  
### 函数的调用顺序
 首先外部创建和启动tts引擎, 而tts引擎的初始化阶段创建并且启动一个子进程, 在这个子进程里完成实际工作的。  
 而tts引擎实际上是子进程的管理者, 它阻塞等待外部的数据, 外部可以调用tts引擎的Send方法给tts引擎传入数据。  
 Send方法需要一个必须参数和两个可选参数, 必须参数是操作码, 第一个可选参数是操作参数, 第二个可选参数是外部的回调函数。  
 tts引擎得到外部的数据后, 把操作码和操作参数发送给子进程, 随后子进程开始处理, 完成后返回结果数据,  两个数据, 第一个标志码, 成功或者错误, 第二个数据, 实际数据,  
 如果是成功标志就是结果, 比如子进程tts的实际音量等, 如果是错误标志第二个也许是异常对象, 绝大多情况无数据。  
 总之tts引擎获得子进程返回的数据后调用外部传入的回调函数, 外部可以通过传入回调函数来获取tts引擎的当前状态, 这样外部可以调整自身状态, 比如更新UI。  
 请看核心代码/src/v1.2/tts2wav.py
'''
# --*-- Encoding:UTF-8 --*--
#! fileName:tts2wav.py

# 最简单的多任务架构下的tts引擎
from tts_defines import *
import win32com.client
import threading
import multiprocessing
from queue import Queue # 在线程之间共享数据的, 先进后出的队列

# tts引擎, 多线程实现
class Engine(threading.Thread):
 def __init__(self):
   threading.Thread.__init__(self)
   self.setDaemon(True) # 设为守护线程
   self.cache = Queue() # 在多线程之间缓存数据
   self.mainPipe, self.subPipe = multiprocessing.Pipe() # 在多进程之间收发数据
   # 创建并且启动一个进程, 而且把调用mutltiprocessing.Pipe返回的第二个pipe实力传入新创建的进程
   # 这样两个进程可以同等的相互收发数据了
   task = multiprocessing.Process(target = Engine.Task, args = [self.subPipe]) # 这里的Task是Engine类的静态方法, 如果设为实力方法就会出错
   task.start()

 # 和Java一样线程的执行函数, 使用Thread父类的start方法来调用, 如果直接调用不会创建新线程
 def run(self):
   while True:
     # 从本线程的缓存里取出三个字段, 操作标志, 操作数据和回调函数
     code, body, callback = self.cache.get()
     # 把操作标志和操作数据发送给子进程
     self.mainPipe.send((code, body))
     # 从子进程接受返回标志和返回数据
     err, result = self.mainPipe.recv()
     # 如果有回调函数就调用之
     if callback:
       callback(err, result)


   # 其他线程可以调用这个方法, 在本线程的缓存里放入数据 除了操作标志其他都可以省略
 def Send(self, code, body = None, callback = None):
   self.cache.put((code, body, callback))


 # 子进程的任务在这个静态方法里, 参数就是从父进程传入的第二个pipe实力
 def Task(pipe):
   SpVoice = win32com.client.Dispatch("SAPI.SpVoice") # 创建SpVoice组件对象
   backOutput = SpVoice.AudioOutputStream # 备份默认的输出设备
   SpFileStream = win32com.client.Dispatch("SAPI.SpFileStream") # 创建文件输出流
   # 在一个循环里接受父进程发送来的操作标志和操作数据, 经过处理后反发送给父进程
   while True:
     code, body = pipe.recv()
     # 朗读文本标志
     if code == TTS_TEXT:
       try:
         SpVoice.Speak(body) # 朗读文本或者转换文本到wav文件
         pipe.send((TTS_OK, None)) # 发送成功信号
       except Exception as e:
         pipe.send((TTS_ERROR, e)) # 发送错误型号, 第二个是异常对象的引用

     # 发音角色列表标志
     elif code == TTS_VOICES:
       voices = []
       for voice in SpVoice.GetVoices():
         voices.append(voice.GetDescription())

       pipe.send((TTS_OK, voices)) # 成功标志和结果数据, 也就是发音角色列表

     # 发音角色标志
     elif code == TTS_VOICE:
       # 判断有没有操作数据, 如果有就设置发音角色, 如果没有反发送发音角色
       if body:
         for voice in SpVoice.GetVoices():
           if voice.GetDescription() == body:
             SpVoice.Voice = voice # 设置发音角色
             pipe.send((TTS_OK, None)) # 发送成功标志
             break

       else:
         pipe.send((TTS_OK, Spvoice.Voice.GetDescription())) # 发送当前发音角色

       # 参考上面的
     elif code == TTS_RATE:
       if body:
         SpVoice.Rate = body
         pipe.send((TTS_OK, None))
       else:
         pipe.send((TTS_OK, SpVoice.Rate))

       # 参考上面的
     elif code == TTS_VOLUME:
       if body:
         SpVoice.Volume = body
         pipe.send((TTS_OK, None))
       else:
         pipe.send((TTS_OK, SpVoice.Volume))

       # 打开wav文件的标志
     elif code == TTS_OPEN:
       SpFileStream.Open(body, 3, False) # 打开wav文件
       SpVoice.AudioOutputStream = SpFileStream # 设置输出设备为wav文件流
       pipe.send((TTS_OK, None)) # 发送成功标志, 失败的机率少, 所以没有做异常处理, 后续需要完善
       # 关闭wav文件的标志
     elif code == TTS_CLOSE:
       SpFileStream.Close()
       SpVoice.AudioOutputStream = backOutput # 恢复默认输出设备
       pipe.send((TTS_OK, None))
       # 退出进程标志, 说白了就是让你自杀
     elif code == TTS_QUIT:
       pipe.send((TTS_OK, None))
       break
     else:
       pipe.send((TTS_ERROR, ValueError("不可识别的操作标志")))
'''
 这里我们没有必要花时间介绍子进程的内部如何工作的, 无非就是判断操作码, 然后采取不同的动作而已, 值得注意的是这里的错误处理不是非常完善。  
 接下来大家可以拿去测试一下, 在完整程序里包含了有很多bug的UI下一篇尝试去完善这个程序。  
http://39.104.103.44:8082/downloads/tts2wav_v1.2.zip
未完待续……


2021-08-25 20:36:08 显示全部楼层
访客