- 编程园地
- python调用SAPI生成wav音频文件
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调用。
在运行转换的时候我们的用户界面不能卡死, 所以需要引入多任务处理, 当然朝思暮想的窗口还早, 把多任务给搞定后在来加窗口。
通常情况下多任务有两种实现, 轻量级的多线程和开销大的多进程, 根据情况选择使用就可以了。
这里我使用多线程调用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
经过上两篇的铺垫你应该非常容易的看明白下面的一些逻辑。
### 操作码和操作参数
我们的主进程通过发送操作码来让子进程工作的, 这些操作码大体上两类有参数和无参数的。
比如说, 有参数的设置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
未完待续……