如果没有工科背景,就不要纠结于什么是示波器以及为什么要加上存储这个限定词了,我们还是关注重点吧:什么是音频信号?我们人耳能听到的声音的频率范围,大约在20Hz到20000Hz之间,低于下限的,叫次声波,超过上限的,叫超声波。麦克将声音变成了电流,这就是音频信号。音频信号有频率和幅度的变化,存储示波器可以把一段时间内的音频信号直观地显示在屏幕上。
声音是连续的,麦克输出的音频信号也是连续的。计算机只能处理数字化信息,所以要对音频信号做数字化处理,这就是所谓的模(拟)数(字)转换或A/D转换,其本质是每隔一个固定间隔时间测量一次信号的大小,并用这个测量值近似代替这个时间间隔内的信号幅度。如果测量的频率超过信号最高频率的两倍,A/D转换就可以取得很好的效果。这个测量频率就是采样频率,业界的标准之一是44100Hz,是音频上限的两倍多一点。
A/D转换过程中每次采样得到的数据都需要保存下来。采集到的信号大小,如果用一个字节表示,则信号的动态范围是从-128到127,用两个字节表示,则信号的动态范围是从-32768到32767。这就是所谓的量化精度。
让我们来想象一个包饺子的场景:有人负责擀皮儿,擀好的饺子皮儿一张张摞成一摞;有人负责包饺子,从成摞的饺子皮儿上揭起一张,放馅儿、捏紧,码放在平板上;有人负责煮饺子,一次取走一平板。擀皮儿、包饺子、煮饺子,是三道相互依赖又各自独立的工序,前道工序是生产者,后道工序是消费者,生产者和消费者之间使用缓冲区作为隔离,最大限度地解除二者之间的相互影响。
为了描述方便,我先把最终的效果贴在下面。
文件或文件夹 | 说明 |
---|---|
DSO.py | 主程序,实现程序框架 |
audioCapture.py | 音频采集模块,定了一个音频采集类AudioCapture |
plotPanel.py | 数据绘图模块,定了一个示波器屏幕类WaveScreen |
res | 资源文件夹 |
data | 用户数据文件夹 |
pyaudio模块是python最常用的声卡模块,可以使用 pip install pyaudio 下载安装。我们在audioCapture.py文件中定义了AudioCapture类,用于从声卡采集数据。
源码:audioCapture.py
# -*- coding: utf-8 -*-
import pyaudio
import numpy as np
class AudioCapture(object):
'''通过声卡采集音频,数据存入队列'''
def __init__(self, dq, mode=0, level=256, over=32):
'''构造函数'''
self.dq = dq # 数据队列
self.mode = mode # 实时模式(mode=0)/触发模式(mode=1)
self.level = level # 触发模式下的触发阈值
self.over = over # 触发模式下的触发数量
self.chunk = 1024 # 数据块大小
self.running = False # 声音采集工作状态
def set(self, **kwds):
'''设置参数'''
if 'mode' in kwds:
self.mode = kwds['mode']
if 'level' in kwds:
self.level = kwds['level']
if 'over' in kwds:
self.over = kwds['over']
def run(self):
'''音频采集'''
pa = pyaudio.PyAudio()
stream = pa.open(
format = pyaudio.paInt16, # 量化精度
channels = 1, # 通道数
rate = 44100, # 采样速率
frames_per_buffer = self.chunk, # pyAudio内部缓存的数据块大小
input = True
)
self.running = True
while self.running:
data = stream.read(self.chunk)
data = np.fromstring(data, dtype=np.int16)
# 实时模式下,或者触发模式下超过触发阈值的数据量多于触发数量(1个数据块内)
if self.mode == 0 or np.sum([data > self.level, data < -self.level]) > self.over:
try:
self.dq.put(data, block=False)
except:
print 'The data queue is Full!'
pass
stream.close()
pa.terminate()
def stop(self):
'''停止采集'''
self.running = False
这段代码定义了一个音频采集(AudioCapture)类中,实例化时需要提供一个数据队列。从声卡读出的数据是str类型,需要使用numpy的fromstring()方法转成numpy的array类型。另外请注意,向队列中写数据时,采用的是非阻塞式的,如果队列已满,则会抛出异常,所以需要捕获该异常。
下面的代码,演示了一个典型的生产者/消费者模式:一个子线程负责采集数据并写入队列,一个子线程负责从队列中取出数据并显示。同时,也展示了如何创建及使用队列、如何创建及管理线程。
import Queue
import threading
import time
# 生产者/消费者模式
# 音频采集——生产数据,使用子线程,运行线程函数,本例是ac.run()
# 数据绘图——消费数据,使用子线程,运行线程函数,本例是read_queue()
# 生产线程和消费线程之间,使用先进先出(FIFO)的队列缓冲区
dq = Queue.Queue(100)
ac = AudioCapture(dq)
def read_queue(dq):
while True:
data = dq.get(block=True)
print data.min(), data.max(), data.var()
reading_thread = threading.Thread(target=read_queue, args=(dq,))
reading_thread.setDaemon(True)
reading_thread.start()
capture_thread = threading.Thread(target=ac.run)
capture_thread.setDaemon(True)
capture_thread.start()
cmd = raw_input('Waiting...Press any key to stop.')
ac.stop()
while capture_thread.isAlive():
#print 'running...'
time.sleep(0.01)
print 'Game Over.'
万丈高楼平地起。几乎所有的窗口程序,都可以从下面这个基本框架开始。
源码:base.py
#-*- coding: utf-8 -*-
import sys, os
import wx, win32api
APP_NAME = u'Digital Storage Oscilloscope'
APP_ICON_NAME = "res/wave.ico"
class mainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, APP_NAME, style=wx.DEFAULT_FRAME_STYLE)
self.Maximize()
self.SetBackgroundColour(wx.Colour(240, 240, 240))
# 图标显示
if hasattr(sys, "frozen") and getattr(sys, "frozen") == "windows_exe":
exeName = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
icon = wx.Icon(exeName, wx.BITMAP_TYPE_ICO)
else :
icon = wx.Icon(APP_ICON_NAME, wx.BITMAP_TYPE_ICO)
self.SetIcon(icon)
#----------------------------------------------------------------------
class mainApp(wx.App):
def OnInit(self):
frame = mainFrame(None)
frame.Show()
return True
#----------------------------------------------------------------------
if __name__ == "__main__":
app = mainApp(redirect=True, filename="debug.txt")
app.MainLoop()
在开始UI设计之前,有必要先来了解一下wxPython的控件布局理论。wx的所有控件几乎都有parent/id/pos/size/style等属性,其中pos是position的简写,这是一个二元组,表示控件左上角距离在其父级控件左上角的像素距离。我们可以通过设置每个控件的pos实现控件布局,这就是所谓的静态布局法。当程序窗口尺寸变化时,静态布局很难保持好的显示效果,所以更常用的布局方法是使用布局管理控件。
wx.BoxSizer是最常用的布局管理控件,可以将其视为控件容器。装入wx.BoxSizer中的所有控件,垂直或者水平排列。不同于大多数的控件有具体的形象,wx.BoxSizer是无形的、不可见的,实例化时也不需要parent/id/pos/size/style等属性,只需要指定是水平的还是垂直的。下面这段代码演示了如何使用wx.BoxSizer实现布局。
#-*- coding: utf-8 -*-
import sys, os
import wx, win32api
APP_NAME = u'Digital Storage Oscilloscope'
APP_ICON_NAME = "res/wave.ico"
class mainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, APP_NAME, style=wx.DEFAULT_FRAME_STYLE)
self.SetBackgroundColour(wx.Colour(240, 240, 240))
self.SetSize((400,200))
self.Center()
# 图标显示
if hasattr(sys, "frozen") and getattr(sys, "frozen") == "windows_exe":
exeName = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
icon = wx.Icon(exeName, wx.BITMAP_TYPE_ICO)
else :
icon = wx.Icon(APP_ICON_NAME, wx.BITMAP_TYPE_ICO)
self.SetIcon(icon)
# 2个文本控件、4个数据输入框、1个按钮
st1 = wx.StaticText(self, -1, u'幅度', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
st2 = wx.StaticText(self, -1, u'时间', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
tc11 = wx.TextCtrl(self, -1, u'', style=wx.TE_CENTER)
tc12 = wx.TextCtrl(self, -1, u'', style=wx.TE_CENTER)
tc21 = wx.TextCtrl(self, -1, u'', style=wx.TE_CENTER)
tc22 = wx.TextCtrl(self, -1, u'', style=wx.TE_CENTER)
btn = wx.Button(self, -1, u'确定')
sizer_0 = wx.BoxSizer(wx.VERTICAL) # 垂直布局控件
sizer_11 = wx.BoxSizer() # 水平布局空间
sizer_12 = wx.BoxSizer() # 水平布局空间
# sizer_11 装入1个文本控件(st1)、2个数据输入框(tc11/tc12)
sizer_11.Add(st1, 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 10)
sizer_11.Add(tc11, 2, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 0)
sizer_11.Add(tc12, 1, wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5)
# sizer_12 装入1个文本控件(st2、2个数据输入框(tc21/tc22)
sizer_12.Add(st2, 0, wx.ALIGN_CENTER_VERTICAL|wx.RIGHT, 10)
sizer_12.Add(tc21, 2, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 0)
sizer_12.Add(tc22, 3, wx.ALIGN_CENTER_VERTICAL|wx.LEFT, 5)
# sizer_0 装入sizer_11、sizer_12和按钮(btn)
sizer_0.Add(sizer_11, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 20)
sizer_0.Add(sizer_12, 0, wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, 20)
sizer_0.Add(btn, 1, wx.EXPAND|wx.ALL, 20)
# 将sizer_0放置到父级控件上
self.SetSizer(sizer_0)
self.SetAutoLayout(True)
#----------------------------------------------------------------------
class mainApp(wx.App):
def OnInit(self):
frame = mainFrame(None)
frame.Show()
return True
#----------------------------------------------------------------------
if __name__ == "__main__":
app = mainApp()
app.MainLoop()
改变窗口大小,可以看到控件位置会自动调整。显示效果如下图所示。
为了保持代码结构清晰,我们把示波器屏幕代码独立出来,单独保存为一个模块,文件名为plotPanel.py。示波器屏幕类WaveScreen继承自wx.Panel类,wx.Panel类是UI设计中的面板控件,可以在其上放置按钮、图片、文字、输入框等控件。
plotPanel_0.py
# -*- coding: utf-8 -*-
import wx
class WaveScreen(wx.Panel):
'''示波器显示屏幕'''
def __init__(self, parent):
'''构造函数'''
wx.Panel.__init__(self, parent, -1, style=wx.EXPAND)
self.SetBackgroundColour(wx.Colour(0, 0, 0))
self.parent = parent
self.ML,self.MR,self.MT,self.MB = 70,70,40,40 # 绘图边框距屏幕边缘距离(左右上下)
self.Bind(wx.EVT_SIZE, self.onSize)
self.Bind(wx.EVT_PAINT, self.onPaint)
def onSize(self, evt):
'''响应窗口大小变化'''
w, h = self.parent.GetSize()
self.w_scr, self.h_scr = w-176, h-118 # 示波器屏幕宽度、高度
self.rePaint()
def onPaint(self, evt):
'''响应重绘事件'''
dc = wx.PaintDC(self)
self.plot(dc)
def rePaint(self):
'''手动重绘'''
dc = wx.ClientDC(self)
self.plot(dc)
def plot(self, dc):
'''绘制屏幕'''
dc.Clear()
# 绘制外边框
dc.SetPen(wx.Pen(wx.Colour(224,0,0), 1))
dc.DrawLine(self.ML, self.MT, self.w_scr-self.MR, self.MT)
dc.DrawLine(self.ML, self.h_scr-self.MB, self.w_scr-self.MR, self.h_scr-self.MB)
dc.DrawLine(self.ML, self.MT, self.ML, self.h_scr-self.MB)
dc.DrawLine(self.w_scr-self.MR, self.MT, self.w_scr-self.MR, self.h_scr-self.MB)
根据总体设计规划,在最简单的窗口程序框架的基础上,应用布局管理控件,将数字存储示波器的界面写成代码如下。这段代码,只包含了控件和控件布局,不涉及任何的处理逻辑。运行显示的效果已经和设计目标完全一样了,只是无法做任何操作,除了点击“关于”菜单。
DSO_0.py
#-*- coding: utf-8 -*-
import sys, os
import wx, win32api
import wx.lib.buttons as buttons
import wx.lib.agw.knobctrl as KC
from wx.lib.wordwrap import wordwrap
# 请注意:此处导入的是plotPanel_0,而非plotPanel
from plotPanel_0 import *
APP_NAME = u'Digital Storage Oscilloscope'
APP_ICON_NAME = "res/wave.ico"
APP_VERSION = '0.99'
class mainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, APP_NAME, style=wx.DEFAULT_FRAME_STYLE)
self.Maximize()
self.SetBackgroundColour(wx.Colour(240, 240, 240))
# 图标显示
if hasattr(sys, "frozen") and getattr(sys, "frozen") == "windows_exe":
exeName = win32api.GetModuleFileName(win32api.GetModuleHandle(None))
icon = wx.Icon(exeName, wx.BITMAP_TYPE_ICO)
else :
icon = wx.Icon(APP_ICON_NAME, wx.BITMAP_TYPE_ICO)
self.SetIcon(icon)
self.__create_menu_bar() # 创建菜单栏
self.__create_status_bar() # 创建状态栏
self.mode_ch = [u'实时模式', u'触发模式'] # 触发模式选择项
self.level_ch = ['128', '256', '512', '1024'] # 触发幅度选择项
self.over_ch = ['1', '8', '32', '128'] # 触发数量选择项
# ------------------------------------------------------
# 0. 创建布局管理控件
sizer_max = wx.BoxSizer() # 最顶层的布局控件,水平布局
sizer_left = wx.BoxSizer(wx.VERTICAL) # 左侧区域布局控件,垂直布局
sizer_right = wx.BoxSizer(wx.VERTICAL) # 右侧区域布局控件,垂直布局
# 1. 实例化示波器屏幕
self.screen = WaveScreen(self)
# 2. 创建垂直轴(幅度)调整旋钮
self.label_knob_V = wx.StaticText(self, -1, u'幅度调整', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
self.knob_V = KC.KnobCtrl(self, -1, size=(120, 120))
self.knob_V.SetBackgroundColour(wx.Colour(240, 240, 240))
self.knob_V.SetTags(range(0, 171, 10))
self.knob_V.SetAngularRange(-45, 225)
self.knob_V.SetValue(150)
# 3. 创建水平轴(时间)调整旋钮
self.label_knob_H = wx.StaticText(self, -1, u'宽度调整', style=wx.ALIGN_CENTER|wx.ST_NO_AUTORESIZE)
self.knob_H = KC.KnobCtrl(self, -1, size=(120, 120))
self.knob_H.SetBackgroundColour(wx.Colour(240, 240, 240))
self.knob_H.SetTags(range(0, 131, 10))
self.knob_H.SetAngularRange(-45, 225)
self.knob_H.SetValue(40)
# 4. 创建模式选择、幅度阈值选择和数量阈值选择
self.mode_rb = wx.RadioBox(self,
id = -1,
label = u'模式选择',
choices = self.mode_ch,
majorDimension = 1,
style = wx.RA_SPECIFY_COLS,
name = 'mode'
)
self.level_rb = wx.RadioBox(self,
id = -1,
label = u'触发阈值',
choices = self.level_ch,
majorDimension = 2,
style = wx.RA_SPECIFY_COLS,
name = 'level'
)
self.over_rb = wx.RadioBox(self,
id = -1,
label = u'触发数量',
choices = self.over_ch,
majorDimension = 2,
style = wx.RA_SPECIFY_COLS,
name = 'over'
)
self.mode_rb.SetSelection(0)
self.level_rb.SetSelection(1)
self.over_rb.SetSelection(2)
# 5. 创建启动/停止按钮
self.start_btm = wx.Bitmap(os.path.join('res', 'start.png'), wx.BITMAP_TYPE_ANY)
self.stop_btm = wx.Bitmap(os.path.join('res', 'stop.png'), wx.BITMAP_TYPE_ANY)
self.op_btn = buttons.GenBitmapToggleButton(self, -1, bitmap=self.start_btm, size=(-1,80))
self.op_btn.SetBackgroundColour(wx.Colour(192, 224, 224))
self.op_btn.SetBitmapSelected(self.stop_btm)
# 6. 创建滑块
self.slider = wx.Slider(self, -1, 0, 0, 100, size=wx.DefaultSize, style=wx.SL_HORIZONTAL)
# 7. 部件组装
sizer_left.Add(self.screen, 1, wx.EXPAND|wx.ALL, 0)
sizer_left.Add(self.slider, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
sizer_right.Add(self.knob_V, 0, wx.TOP, 0)
sizer_right.Add(self.label_knob_V, 0, wx.EXPAND|wx.TOP, 10)
sizer_right.Add(self.knob_H, 0, wx.TOP, 20)
sizer_right.Add(self.label_knob_H, 0, wx.EXPAND|wx.TOP, 10)
sizer_right.Add(self.mode_rb, 0, wx.EXPAND|wx.TOP, 40)
sizer_right.Add(self.level_rb, 0, wx.EXPAND|wx.TOP, 20)
sizer_right.Add(self.over_rb, 0, wx.EXPAND|wx.TOP, 20)
sizer_right.Add(self.op_btn, 0, wx.EXPAND|wx.TOP, 30)
sizer_max.Add(sizer_left, 1, wx.EXPAND|wx.ALL, 0)
sizer_max.Add(sizer_right, 0, wx.ALL, 20)
# 8. 大功告成
self.SetSizer(sizer_max)
self.SetAutoLayout(True)
def __create_menu_bar(self):
'''创建菜单栏'''
id_open = wx.NewId()
id_save_data = wx.NewId()
id_save_img = wx.NewId()
id_quit = wx.NewId()
id_start = wx.NewId()
id_stop = wx.NewId()
id_about = wx.NewId()
mb = wx.MenuBar()
m = wx.Menu()
m.Append(id_open, u'打开数据文件\tCtrl+O', u'打开保存的数据文件')
m.Append(id_save_data, u'保存数据为文件\tCtrl+S', u'将当前数据保存为文件')
m.Append(id_save_img, u'保存波形为图片\tCtrl+P', u'将当前波形保存为图片')
m.AppendSeparator()
m.Append(id_quit, u'退出\tCtrl+C', u'退出系统')
mb.Append(m, u'文件(&F)')
m = wx.Menu()
m.Append(id_start, u'启动\tCtrl+R', u'启动数据采集')
m.Append(id_stop, u'停止\tCtrl+T', u'停止数据采集')
mb.Append(m, u'操作(&O)')
m = wx.Menu()
m.Append(id_about, u'关于\tCtrl+A', '')
mb.Append(m, u'帮助(&H)')
self.SetMenuBar(mb)
self.Bind(wx.EVT_MENU, self.onMenuOpen, id=id_open)
self.Bind(wx.EVT_MENU, self.onMenuSaveData, id=id_save_data)
self.Bind(wx.EVT_MENU, self.onMenuSaveImage, id=id_save_img)
self.Bind(wx.EVT_MENU, self.OnMenuQuit, id=id_quit)
self.Bind(wx.EVT_MENU, self.onMenuStart, id=id_start)
self.Bind(wx.EVT_MENU, self.onMenuStop, id=id_stop)
self.Bind(wx.EVT_MENU, self.onMenuAbout, id=id_about)
def __create_status_bar(self):
'''创建状态栏'''
self.statusbar = self.CreateStatusBar()
self.statusbar.SetFieldsCount(3)
self.statusbar.SetStatusWidths([-1,-3, -1])
self.statusbar.SetStatusStyles([wx.SB_RAISED, wx.SB_RAISED, wx.SB_RAISED])
self.statusbar.SetStatusText(u'[email protected], Jilin University', 2)
def onMenuOpen(self, evt):
'''打开数据文件'''
pass
def onMenuSaveData(self, evt):
'''保存数据为文件'''
pass
def onMenuSaveImage(self, evt):
'''保存为图片'''
pass
def OnMenuQuit(self, evt):
'''关闭窗口'''
pass
def onMenuStart(self, evt):
'''响应启动捕捉菜单'''
pass
def onMenuStop(self, evt):
'''响应停止捕捉菜单'''
pass
def onMenuAbout(self, evt):
'''关于'''
about = wx.AboutDialogInfo()
about.Name = APP_NAME
about.Version = APP_VERSION
about.Copyright = u"(C) 吉林大学数学学院 许棪"
about.Description = wordwrap(
u"音频信号存储示波器是用计算机声卡采集音频输入信号,并将音频数据绘制在屏幕上的一款软件,"
u"可以实时模式或触发模式工作,并可将数据和波形保存为文件。"
u"\n\n你可以尝试着用它来记录并显示你的口哨声,或者找到更多更有趣的应用。"
u"我曾经用它来观察导体切割磁场产生的电流。"
u'如果你也想重复我的实验,请谨慎操作,以免损坏声卡或电脑。',
400, wx.ClientDC(self), margin=5)
#about.WebSite = ("[email protected]", u"给开发者发邮件")
about.Developers = [u"许棪" ]
licenseText = u"欢迎非商业性的使用、复制、传播和二次开发。"
about.License = wordwrap(licenseText, 400, wx.ClientDC(self), margin=5)
wx.AboutBox(about)
#----------------------------------------------------------------------
class mainApp(wx.App):
def OnInit(self):
frame = mainFrame(None)
frame.Show()
return True
#----------------------------------------------------------------------
if __name__ == "__main__":
app = mainApp()
app.MainLoop()
根据规划,示波器有两种工作模式:实时模式和触发模式。模式选择控件(RedioButton)可以改变工作模式,而数据采集线程需要根据当前模式选择恰当的处理方式,因此,当前工作模式是一个很多地方都会用到的数据,有必要把它设置成主窗口类的属性之一。类似的情况还有当前触发阈值、当前触发数量、滑块位置表示的当前时间,时间轴窗口宽度、当前纵轴最大值等。
我们还需要创建一个声卡采集对象,用于采集声卡数据。声卡采集对象具有run()和stop()方法,受控于程序界面上启动/停止按钮,run()是以线程的方式运行的,采集到的数据写入队列缓冲区。另外,从数据队列中顺序读出的数据块,也需要保存在预先设定的数据结构中,为此我们准备了一个list来存储这些数据。
class mainFrame(wx.Frame):
'''音频信号存储示波器窗口类'''
def __init__(self, parent):
'''构造函数'''
... ...
if not os.path.isdir('data'): # 如果数据存储文件夹不存在,则创建
os.mkdir('data')
self.mode = 0 # 当前模式
self.level = 256 # 当前触发阈值
self.over = 32 # 当前触发数量
self.curr_pos = 0 # 滑块位置表示的当前时间
self.time_width = 10 # 时间轴窗口宽度(单位:毫秒)
self.value_max = 32768 # 当前纵轴最大值
self.audio = list() # 保存从队列中读出的数据
self.dq = Queue.Queue(100) # 数据缓存队列
self.ac = AudioCapture(
self.dq,
mode=self.mode,
level=self.over,
over=self.over) # 创建音频采集对象
self.capture_thread = None # 音频采集线程
... ...
为什么声音采集线程是None呢?因为这个线程只有在点击启动按钮时才会被创建和运行,构造函数里仅仅是声明。不提前声明,也完全没有问题,这样做是为了提供程序的可读性。需要说明的是,把采集线程定义为类的属性,是为了关闭窗口时检查这个线程是否还在运行,若还在运行,则先关闭声再终止线程。为此,我们需要将窗口关闭事件wx.EVT_CLOSE绑定到事件函数OnMenuQuit()上,该函数也是菜单中“退出系统”的响应函数。
class mainFrame(wx.Frame):
'''音频信号存储示波器窗口类'''
def __init__(self, parent):
'''构造函数'''
... ...
self.Bind(wx.EVT_CLOSE, self.OnMenuQuit) # 将窗口关闭事件绑定到事件函数
... ...
def OnMenuQuit(self, evt):
'''关闭窗口'''
if self.capture_thread and self.capture_thread.isAlive():
self.ac.stop()
while self.capture_thread and self.capture_thread.isAlive():
time.sleep(0.1)
self.Destroy()
在创建状态栏时,已经演示了如何在状态蓝的指定区域显示信息。为了更简洁一点,我们为mainFrame定义了一个显示数据时间长度的专用方法setTip()。那么数据时长如何计算呢?假定声卡采样频率为44100Hz,每次读取1024字节的数据块,那么一个数据块对应的时间长度是23.219954648526078毫秒(1024*1000/44100),我们把这个数据写成一个常量。
TIME_K = 23.219954648526078 # 采样速率为44100时,1024个数据时长,单位毫秒
class mainFrame(wx.Frame):
'''音频信号存储示波器窗口类'''
def setTip(self):
'''设置状态条上数据长度信息'''
length = len(self.audio) * TIME_K
self.statusbar.SetStatusText(u'总时长:%.03f秒'%(length/1000.0), 1)
在数据生产者/消费者模式中,数据的生产和消费是各自独立的,二者使用数据缓冲区耦合。在本例中,从声卡采集数据的线程,就是数据生产者,对应的,从队列中读出数据的线程,就是数据消费者。线程的创建时需要将线程函数作为参数传入,而线程函数的参数(如果有的话),则视为创建线程的args参数或kargs参数。在窗口程序中,如果线程函数需要调用窗口类的方法,一般需要借助于wx.CallAfter()。
class mainFrame(wx.Frame):
'''音频信号存储示波器窗口类'''
def __init__(self, parent):
'''构造函数'''
... ...
# 启动线程:以阻塞方式从队列中读出数据
read_thread = threading.Thread(target=self.readData)
read_thread.setDaemon(True)
read_thread.start()
... ...
def readData(self):
'''从队列中读取数据'''
while True:
data = self.dq.get(block=True)
self.audio.append(data)
length = len(self.audio) * TIME_K
if length > self.time_width:
self.curr_pos = length - self.time_width
else:
self.curr_pos = 0.0
self.screen.rePaint()
wx.CallAfter(self.setTip)