用声卡实现的存储示波器

文章目录

  • 用声卡实现的存储示波器
    • 背景知识
      • 采样频率
      • 量化精度
      • 生产者/消费者模式
    • 总体规划
      • 设计目标
      • 功能规划
      • 界面规划
      • 程序结构
    • 从声卡采集数据
      • 声音采集类的定义
      • 消费者/生产者实例
    • 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 npclass 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 = Truewhile 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!'passstream.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.'

wxPython布局基础

最简单的窗口程序框架

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

源码:base.py

#-*- coding: utf-8 -*-import sys, os
import wx, win32apiAPP_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, win32apiAPP_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 wxclass 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 = 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)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'xuyan0105@outlook.com, Jilin University', 2)def onMenuOpen(self, evt):'''打开数据文件'''passdef onMenuSaveData(self, evt):'''保存数据为文件'''passdef onMenuSaveImage(self, evt):'''保存为图片'''passdef OnMenuQuit(self, evt):'''关闭窗口'''passdef onMenuStart(self, evt):'''响应启动捕捉菜单'''passdef onMenuStop(self, evt):'''响应停止捕捉菜单'''passdef onMenuAbout(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)#----------------------------------------------------------------------
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_Kself.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_Kif length > self.time_width:self.curr_pos = length - self.time_widthelse:self.curr_pos = 0.0self.screen.rePaint()wx.CallAfter(self.setTip)

用声卡实现的存储示波器相关推荐

  1. [Ubuntu] 安装/卸载 声卡驱动

    卸载 sudo apt-get --purge remove linux-sound-base alsa-base alsa-utils 安装 sudo apt-get install linux-s ...

  2. c++采集声卡输出_耳上明珠 | 魅族双 C 耳机 — EP2C

    魅族双 C 耳机 魅族 16s 取消了 3.5mm 耳机接口,使用 USB 输出数字音频信号,支持 Type-C 数字耳机或数字转接器.除了无线蓝牙耳机和一线难求的魅族 HIFI 解码耳放外,与魅族 ...

  3. ALSA声卡驱动中的DAPM详解之四:在驱动程序中初始化并注册widget和route

    前几篇文章我们从dapm的数据结构入手,了解了代表音频控件的widget,代表连接路径的route以及用于连接两个widget的path.之前都是一些概念的讲解以及对数据结构中各个字段的说明,从本章开 ...

  4. 计算机中音乐设备数字接口,一种计算机用声卡封存装置的制作方法

    本实用新型涉及计算机声卡领域,尤其涉及一种计算机用声卡封存装置. 背景技术: 声卡的基本功能是把来自话筒.磁带.光盘的原始声音信号加以转换,输出到耳机.扬声器.扩音机.录音机等声响设备,或通过音乐设备 ...

  5. Linux ALSA声卡驱动之八:ASoC架构中的Platform

    1.  Platform驱动在ASoC中的作用 前面几章内容已经说过,ASoC被分为Machine,Platform和Codec三大部件,Platform驱动的主要作用是完成音频数据的管理,最终通过C ...

  6. 如何用计算机声卡,外置声卡怎么连接电脑

    外置声卡怎么连接电脑?随着生活水平的日益提高,直播行业的兴起,人们对录音.K歌.直播等的音质要求更高了.此时,配置一款外置声卡显然就是一件很有必要的事情了.那么,外置声卡怎么连接电脑?下面小编为您详细 ...

  7. matlab示波器模拟,声卡虚拟示波器-使用matlab DAQ工具箱中API实现

    声卡有两个模拟输入接口,Line In 和麦克风;有一个声音输出 Line Out,即Speeker.两个输入口都可以用作虚拟示波器的输入.但是由于声卡的输入端与内部放大器之间存在一个耦合电容,限制了 ...

  8. 基于Matlab的声波信号处理,基于声卡和Matlab平台的语音信号增强处理系统

    第29卷第6期 V01.29 No.6 企业技术开发 TECHNOLOGICAL DEVELOPMENT 0F ENTERPRISE 2010年3月 Mar.2010 基于声卡和Matlab平台的语 ...

  9. 声卡硬件测试软件,RMAA声卡检测(RightMark Audio Analyzer)

    RMAA声卡检测(RightMark Audio Analyzer) v5.5,用于测试模拟的质量和任何音频设备的数字声音部分,无论是声卡,便携式MP3播放器,家用CD/ DVD播放机,扬声器系统.使 ...

最新文章

  1. 综述:光流估计从传统方法到深度学习
  2. 常见windows 2000系统进程描述
  3. 数据中心存在不当投资吗?
  4. Unit Three-Program test
  5. PC HARDWARE SHARE NO.4
  6. Dapper试用简例
  7. 一篇文章讲清Go的内存布局和分配原理
  8. 2021-06-13读写锁=独占锁与共享锁
  9. 苹果屏蔽更新_苹果手机屏蔽IOS更新描述文件失效,越狱用户的紧急解决方案
  10. 半导体物理学——(一)半导体中的电子状态
  11. 让iphone死机的短信内容
  12. NXP JN5169 使用 ADC 模数转换器和比较器
  13. 计算机一级如何添加对角线,word中如何在单元格中添加对角线
  14. WebRTC源码研究(46)WebRCT统计信息
  15. C++ 类(继承中的构造和析构)
  16. iOS开发雕虫小技之傻瓜式定位神器-超简单方式解决iOS后台定时定位
  17. C语言编程>第二十七周 ③ 请补充fun函数,该函数的功能是计算并输出下列多项式的值:
  18. 大数据处理关键技术主要有五种,具体指的是什么?
  19. Arty A7-100(XC7A100TCSG324)开箱照
  20. 简易的MySQL主从复制

热门文章

  1. python 自动化发送邮件_Python自动化必备发送邮件报告脚本详解
  2. SyntaxError: Non-UTF-8 code starting with ‘\xe6‘ in file C:/Users/0moyi0/Desktop/DeepLearningExample
  3. xtraReport的简易使用方法
  4. comsume(comsumer怎么读)
  5. 数据库期末复习(1-5章)
  6. 僵尸java7723_僵尸王国7723游戏盒子
  7. vue扫码下载app,并区分安卓和ios
  8. [corefx注释说]-System.Collections.Generic.StackT
  9. 大数据技术之Structured Steaming课程
  10. 无线分组网关系统解决方案(GGSN、PDSN)