javaee论坛

普通会员

225648

帖子

355

回复

369

积分

楼主
发表于 2019-11-03 06:52:13 | 查看: 95 | 回复: 1

文章目录用声卡实现的存储示波器背景知识采样频率量化精度生产者/消费者模式总体规划设计目标功能规划界面规划程序结构从声卡采集数据声音采集类的定义消费者/生产者实例wxPython布局基础最简单的窗口程序框架界面布局方法界面设计示波器屏幕原型框架原型逻辑处理声明主窗口的若干重要属性在状态栏上显示采集到的数据时间长度从数据队列中读出数据

背景知识

如果没有工科背景,就不要纠结于什么是示波器以及为什么要加上存储这个限定词了,我们还是关注重点吧:什么是音频信号?我们人耳能听到的声音的频率范围,大约在20Hz到20000Hz之间,低于下限的,叫次声波,超过上限的,叫超声波。麦克将声音变成了电流,这就是音频信号。音频信号有频率和幅度的变化,存储示波器可以把一段时间内的音频信号直观地显示在屏幕上。

采样频率

声音是连续的,麦克输出的音频信号也是连续的。计算机只能处理数字化信息,所以要对音频信号做数字化处理,这就是所谓的模(拟)数(字)转换或A/D转换,其本质是每隔一个固定间隔时间测量一次信号的大小,并用这个测量值近似代替这个时间间隔内的信号幅度。如果测量的频率超过信号最高频率的两倍,A/D转换就可以取得很好的效果。这个测量频率就是采样频率,业界的标准之一是44100Hz,是音频上限的两倍多一点。

量化精度

A/D转换过程中每次采样得到的数据都需要保存下来。采集到的信号大小,如果用一个字节表示,则信号的动态范围是从-128到127,用两个字节表示,则信号的动态范围是从-32768到32767。这就是所谓的量化精度。

生产者/消费者模式

让我们来想象一个包饺子的场景:有人负责擀皮儿,擀好的饺子皮儿一张张摞成一摞;有人负责包饺子,从成摞的饺子皮儿上揭起一张,放馅儿、捏紧,码放在平板上;有人负责煮饺子,一次取走一平板。擀皮儿、包饺子、煮饺子,是三道相互依赖又各自独立的工序,前道工序是生产者,后道工序是消费者,生产者和消费者之间使用缓冲区作为隔离,最大限度地解除二者之间的相互影响。

总体规划设计目标

为了描述方便,我先把最终的效果贴在下面。

功能规划支持实时采集和触发采集两种模式触发模式下,可设置触发幅度阈值和触发数量阈值点击开始按钮则启动数据采集并同步显示(支持快捷键)点击停止按钮则停止数据采集(支持快捷键)可调整幅度显示比例(支持鼠标滚轮)可调整窗口时间宽度可在数据时间轴上快速滑动时间窗,实现快速数据定位可保存当前数据为文件(支持快捷键)可打开历史数据文件(支持快捷键)可保存当前屏幕波形为图片文件(支持快捷键)自动适应不同屏幕分辨率,改变窗口大小时自动调整界面界面规划屏幕分成两个区域:中心区域和右侧操作区域中心区域主体是示波器屏幕,示波器屏幕是用于定位数据时间点的滑块右侧操作区域,自上而下,依次是幅度旋钮、时间窗宽度旋钮、模式选择、幅度阈值-选择、数量阈值选择和启动/停止按钮程序结构文件或文件夹说明DSO.py主程序,实现程序框架audioCapture.py音频采集模块,定了一个音频采集类AudioCaptureplotPanel.py数据绘图模块,定了一个示波器屏幕类WaveScreenres资源文件夹data用户数据文件夹从声卡采集数据声音采集类的定义

pyaudio模块是python最常用的声卡模块,可以使用pipinstallpyaudio下载安装。我们在audioCapture.py文件中定义了AudioCapture类,用于从声卡采集数据。

源码:audioCapture.py

#-*-coding:utf-8-*-importpyaudioimportnumpyasnpclassAudioCapture(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#声音采集工作状态defset(self,**kwds):'''设置参数'''if'mode'inkwds:self.mode=kwds['mode']if'level'inkwds:self.level=kwds['level']if'over'inkwds:self.over=kwds['over']defrun(self):'''音频采集'''pa=pyaudio.PyAudio()stream=pa.open(format=pyaudio.paInt16,#量化精度channels=1,#通道数rate=44100,#采样速率frames_per_buffer=self.chunk,#pyAudio内部缓存的数据块大小input=True)self.running=Truewhileself.running:data=stream.read(self.chunk)data=np.fromstring(data,dtype=np.int16)#实时模式下,或者触发模式下超过触发阈值的数据量多于触发数量(1个数据块内)ifself.mode==0ornp.sum([data>self.level,data<-self.level])>self.over:try:self.dq.put(data,block=False)except:print'ThedataqueueisFull!'passstream.close()pa.terminate()defstop(self):'''停止采集'''self.running=False

这段代码定义了一个音频采集(AudioCapture)类中,实例化时需要提供一个数据队列。从声卡读出的数据是str类型,需要使用numpy的fromstring()方法转成numpy的array类型。另外请注意,向队列中写数据时,采用的是非阻塞式的,如果队列已满,则会抛出异常,所以需要捕获该异常。

消费者/生产者实例

下面的代码,演示了一个典型的生产者/消费者模式:一个子线程负责采集数据并写入队列,一个子线程负责从队列中取出数据并显示。同时,也展示了如何创建及使用队列、如何创建及管理线程。

importQueueimportthreadingimporttime#生产者/消费者模式#音频采集——生产数据,使用子线程,运行线程函数,本例是ac.run()#数据绘图——消费数据,使用子线程,运行线程函数,本例是read_queue()#生产线程和消费线程之间,使用先进先出(FIFO)的队列缓冲区dq=Queue.Queue(100)ac=AudioCapture(dq)defread_queue(dq):whileTrue:data=dq.get(block=True)printdata.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...Pressanykeytostop.')ac.stop()whilecapture_thread.isAlive():#print'running...'time.sleep(0.01)print'GameOver.'wxPython布局基础最简单的窗口程序框架

万丈高楼平地起。几乎所有的窗口程序,都可以从下面这个基本框架开始。

源码:base.py

#-*-coding:utf-8-*-importsys,osimportwx,win32apiAPP_NAME=u'DigitalStorageOscilloscope'APP_ICON_NAME="res/wave.ico"classmainFrame(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))#图标显示ifhasattr(sys,"frozen")andgetattr(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)#----------------------------------------------------------------------classmainApp(wx.App):defOnInit(self):frame=mainFrame(None)frame.Show()returnTrue#----------------------------------------------------------------------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-*-importsys,osimportwx,win32apiAPP_NAME=u'DigitalStorageOscilloscope'APP_ICON_NAME="res/wave.ico"classmainFrame(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()#图标显示ifhasattr(sys,"frozen")andgetattr(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)#----------------------------------------------------------------------classmainApp(wx.App):defOnInit(self):frame=mainFrame(None)frame.Show()returnTrue#----------------------------------------------------------------------if__name__=="__main__":app=mainApp()app.MainLoop()

改变窗口大小,可以看到控件位置会自动调整。显示效果如下图所示。

界面设计示波器屏幕原型

为了保持代码结构清晰,我们把示波器屏幕代码独立出来,单独保存为一个模块,文件名为plotPanel.py。示波器屏幕类WaveScreen继承自wx.Panel类,wx.Panel类是UI设计中的面板控件,可以在其上放置按钮、图片、文字、输入框等控件。

plotPanel_0.py

#-*-coding:utf-8-*-importwxclassWaveScreen(wx.Panel):'''示波器显示屏幕'''def__init__(self,parent):'''构造函数'''wx.Panel.__init__(self,parent,-1,style=wx.EXPAND)self.SetBackgroundColour(wx.Colour(0,0,0))self.parent=parentself.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)defonSize(self,evt):'''响应窗口大小变化'''w,h=self.parent.GetSize()self.w_scr,self.h_scr=w-176,h-118#示波器屏幕宽度、高度self.rePaint()defonPaint(self,evt):'''响应重绘事件'''dc=wx.PaintDC(self)self.plot(dc)defrePaint(self):'''手动重绘'''dc=wx.ClientDC(self)self.plot(dc)defplot(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-*-importsys,osimportwx,win32apiimportwx.lib.buttonsasbuttonsimportwx.lib.agw.knobctrlasKCfromwx.lib.wordwrapimportwordwrap#请注意:此处导入的是plotPanel_0,而非plotPanelfromplotPanel_0import*APP_NAME=u'DigitalStorageOscilloscope'APP_ICON_NAME="res/wave.ico"APP_VERSION='0.99'classmainFrame(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))#图标显示ifhasattr(sys,"frozen")andgetattr(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'xuyan0105@outlook.com,JilinUniversity',2)defonMenuOpen(self,evt):'''打开数据文件'''passdefonMenuSaveData(self,evt):'''保存数据为文件'''passdefonMenuSaveImage(self,evt):'''保存为图片'''passdefOnMenuQuit(self,evt):'''关闭窗口'''passdefonMenuStart(self,evt):'''响应启动捕捉菜单'''passdefonMenuStop(self,evt):'''响应停止捕捉菜单'''passdefonMenuAbout(self,evt):'''关于'''about=wx.AboutDialogInfo()about.Name=APP_NAMEabout.Version=APP_VERSIONabout.Copyright=u"(C)吉林大学数学学院许棪"about.Description=wordwrap(u"音频信号存储示波器是用计算机声卡采集音频输入信号,并将音频数据绘制在屏幕上的一款软件,"u"可以实时模式或触发模式工作,并可将数据和波形保存为文件。"u"\n\n你可以尝试着用它来记录并显示你的口哨声,或者找到更多更有趣的应用。"u"我曾经用它来观察导体切割磁场产生的电流。"u'如果你也想重复我的实验,请谨慎操作,以免损坏声卡或电脑。',400,wx.ClientDC(self),margin=5)#about.WebSite=("xuyan0105@outlook.com",u"给开发者发邮件")about.Developers=[u"许棪"]licenseText=u"欢迎非商业性的使用、复制、传播和二次开发。"about.License=wordwrap(licenseText,400,wx.ClientDC(self),margin=5)wx.AboutBox(about)#----------------------------------------------------------------------classmainApp(wx.App):defOnInit(self):frame=mainFrame(None)frame.Show()returnTrue#----------------------------------------------------------------------if__name__=="__main__":app=mainApp()app.MainLoop()逻辑处理声明主窗口的若干重要属性

根据规划,示波器有两种工作模式:实时模式和触发模式。模式选择控件(RedioButton)可以改变工作模式,而数据采集线程需要根据当前模式选择恰当的处理方式,因此,当前工作模式是一个很多地方都会用到的数据,有必要把它设置成主窗口类的属性之一。类似的情况还有当前触发阈值、当前触发数量、滑块位置表示的当前时间,时间轴窗口宽度、当前纵轴最大值等。

我们还需要创建一个声卡采集对象,用于采集声卡数据。声卡采集对象具有run()和stop()方法,受控于程序界面上启动/停止按钮,run()是以线程的方式运行的,采集到的数据写入队列缓冲区。另外,从数据队列中顺序读出的数据块,也需要保存在预先设定的数据结构中,为此我们准备了一个list来存储这些数据。

classmainFrame(wx.Frame):'''音频信号存储示波器窗口类'''def__init__(self,parent):'''构造函数'''......ifnotos.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()上,该函数也是菜单中“退出系统”的响应函数。

classmainFrame(wx.Frame):'''音频信号存储示波器窗口类'''def__init__(self,parent):'''构造函数'''......self.Bind(wx.EVT_CLOSE,self.OnMenuQuit)#将窗口关闭事件绑定到事件函数......defOnMenuQuit(self,evt):'''关闭窗口'''ifself.capture_threadandself.capture_thread.isAlive():self.ac.stop()whileself.capture_threadandself.capture_thread.isAlive():time.sleep(0.1)self.Destroy()在状态栏上显示采集到的数据时间长度

在创建状态栏时,已经演示了如何在状态蓝的指定区域显示信息。为了更简洁一点,我们为mainFrame定义了一个显示数据时间长度的专用方法setTip()。那么数据时长如何计算呢?假定声卡采样频率为44100Hz,每次读取1024字节的数据块,那么一个数据块对应的时间长度是23.219954648526078毫秒(1024*1000/44100),我们把这个数据写成一个常量。

TIME_K=23.219954648526078#采样速率为44100时,1024个数据时长,单位毫秒classmainFrame(wx.Frame):'''音频信号存储示波器窗口类'''defsetTip(self):'''设置状态条上数据长度信息'''length=len(self.audio)*TIME_Kself.statusbar.SetStatusText(u'总时长:%.03f秒'%(length/1000.0),1)从数据队列中读出数据

在数据生产者/消费者模式中,数据的生产和消费是各自独立的,二者使用数据缓冲区耦合。在本例中,从声卡采集数据的线程,就是数据生产者,对应的,从队列中读出数据的线程,就是数据消费者。线程的创建时需要将线程函数作为参数传入,而线程函数的参数(如果有的话),则视为创建线程的args参数或kargs参数。在窗口程序中,如果线程函数需要调用窗口类的方法,一般需要借助于wx.CallAfter()。

classmainFrame(wx.Frame):'''音频信号存储示波器窗口类'''def__init__(self,parent):'''构造函数'''......#启动线程:以阻塞方式从队列中读出数据read_thread=threading.Thread(target=self.readData)read_thread.setDaemon(True)read_thread.start()......defreadData(self):'''从队列中读取数据'''whileTrue:data=self.dq.get(block=True)self.audio.append(data)length=len(self.audio)*TIME_Kiflength>self.time_width:self.curr_pos=length-self.time_widthelse:self.curr_pos=0.0self.screen.rePaint()wx.CallAfter(self.setTip)

普通会员

0

帖子

336

回复

345

积分
沙发
发表于 2024-05-08 00:21:46

谢谢

您需要登录后才可以回帖 登录 | 立即注册

触屏版| 电脑版

技术支持 历史网 V2.0 © 2016-2017