用声卡实现的存储示波器

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

用声卡实现的存储示波器


背景知识

如果没有工科背景,就不要纠结于什么是示波器以及为什么要加上存储这个限定词了,我们还是关注重点吧:什么是音频信号?我们人耳能听到的声音的频率范围,大约在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

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

这段代码定义了一个音频采集(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.'

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

wxPython布局基础

最简单的窗口程序框架

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

源码: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()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

界面布局方法

在开始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()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

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

在这里插入图片描述

在这里插入图片描述


界面设计

示波器屏幕原型

为了保持代码结构清晰,我们把示波器屏幕代码独立出来,单独保存为一个模块,文件名为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)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

框架原型

根据总体设计规划,在最简单的窗口程序框架的基础上,应用布局管理控件,将数字存储示波器的界面写成代码如下。这段代码,只包含了控件和控件布局,不涉及任何的处理逻辑。运行显示的效果已经和设计目标完全一样了,只是无法做任何操作,除了点击“关于”菜单。

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'xuyan0105@outlook.com, 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 = ("xuyan0105@outlook.com", 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()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232

逻辑处理

声明主窗口的若干重要属性

根据规划,示波器有两种工作模式:实时模式和触发模式。模式选择控件(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 # 音频采集线程 ... ...

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

为什么声音采集线程是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()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

在状态栏上显示采集到的数据时间长度

在创建状态栏时,已经演示了如何在状态蓝的指定区域显示信息。为了更简洁一点,我们为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)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

从数据队列中读出数据

在数据生产者/消费者模式中,数据的生产和消费是各自独立的,二者使用数据缓冲区耦合。在本例中,从声卡采集数据的线程,就是数据生产者,对应的,从队列中读出数据的线程,就是数据消费者。线程的创建时需要将线程函数作为参数传入,而线程函数的参数(如果有的话),则视为创建线程的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)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

文章来源: xufive.blog.csdn.net,作者:天元浪子,版权归原作者所有,如需转载,请联系作者。

原文链接:xufive.blog.csdn.net/article/details/82893131

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。