wxpython融合matplotlib显示实时声音波形

任务目标

1、监听麦克风,获取实时声音数据。
2、当环境声强大于阈值,触发播放器随机播放一段话。
3、用wxpython做界面,融合matplotlib实时显示波形。
4、用pyinstaller打包成一个单独的exe文件。

开发环境

win7、python3.8

输出结果

运行目录

Root
	-- Listener.exe  # 这个是pyinstaller输出文件
	-- audio  # 这个文件夹存放音频文件
		-- callyou.wav
		-- hello.wav

效果图
在这里插入图片描述

具体实现

工程目录

root
    --wx_plot.fbp  # wxFormBuilder的工程文件,生成display_ui.py
	--display_ui.py  # UI设计
	--wx_plot.py  # 监听,播放,更新UI
	--xx.ico  # 生成exe所使用的图标

wxpython部分

UI文件(display_ui.py)

# -*- coding: utf-8 -*-

###########################################################################
## Python code generated with wxFormBuilder (version 3.9.0 Dec  4 2019)
## http://www.wxformbuilder.org/
##
## PLEASE DO *NOT* EDIT THIS FILE!
###########################################################################

import wx
import wx.xrc
import wx.adv

###########################################################################
## Class DspUI
###########################################################################

class DspUI ( wx.Frame ):

	def __init__( self, parent ):
		wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"Listener", pos = wx.DefaultPosition, size = wx.Size( 524,300 ), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL, name = u"Listener" )

		self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )

		bSizer1 = wx.BoxSizer( wx.VERTICAL )

		self.m_panel2 = wx.Panel( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
		bSizer5 = wx.BoxSizer( wx.VERTICAL )

		self.m_panel3 = wx.Panel( self.m_panel2, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.TAB_TRAVERSAL )
		bSizer6 = wx.BoxSizer( wx.HORIZONTAL )

		bSizer11 = wx.BoxSizer( wx.VERTICAL )


		bSizer11.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		self.m_staticText3 = wx.StaticText( self.m_panel3, wx.ID_ANY, u"阈值:", wx.DefaultPosition, wx.Size( -1,-1 ), 0 )
		self.m_staticText3.Wrap( -1 )

		self.m_staticText3.SetFont( wx.Font( 13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )

		bSizer11.Add( self.m_staticText3, 0, wx.ALL, 5 )


		bSizer11.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer11, 1, wx.EXPAND, 5 )

		bSizer14 = wx.BoxSizer( wx.VERTICAL )


		bSizer14.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		m_thresholdChoices = []
		self.m_threshold = wx.Choice( self.m_panel3, wx.ID_ANY, wx.DefaultPosition, wx.Size( 78,-1 ), m_thresholdChoices, 0 )
		self.m_threshold.SetSelection( 0 )
		bSizer14.Add( self.m_threshold, 0, wx.ALL, 5 )


		bSizer14.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer14, 1, wx.EXPAND, 5 )

		bSizer15 = wx.BoxSizer( wx.VERTICAL )


		bSizer15.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		self.m_playshow = wx.RadioButton( self.m_panel3, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
		self.m_playshow.SetFont( wx.Font( 18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )

		bSizer15.Add( self.m_playshow, 0, wx.ALL, 5 )


		bSizer15.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer15, 1, wx.EXPAND, 5 )

		bSizer10 = wx.BoxSizer( wx.VERTICAL )


		bSizer10.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		self.m_staticText6 = wx.StaticText( self.m_panel3, wx.ID_ANY, u"声强:", wx.DefaultPosition, wx.DefaultSize, 0 )
		self.m_staticText6.Wrap( -1 )

		self.m_staticText6.SetFont( wx.Font( 13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )

		bSizer10.Add( self.m_staticText6, 0, wx.ALL, 5 )


		bSizer10.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer10, 1, wx.EXPAND, 5 )

		bSizer9 = wx.BoxSizer( wx.VERTICAL )


		bSizer9.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		self.m_voice_pow = wx.TextCtrl( self.m_panel3, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( 78,-1 ), 0 )
		self.m_voice_pow.SetFont( wx.Font( 13, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, wx.EmptyString ) )

		bSizer9.Add( self.m_voice_pow, 0, wx.ALL, 5 )


		bSizer9.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer9, 1, wx.EXPAND, 5 )

		bSizer8 = wx.BoxSizer( wx.VERTICAL )


		bSizer8.Add( ( 0, 0), 1, wx.EXPAND, 5 )

		self.m_playing_file = wx.StaticText( self.m_panel3, wx.ID_ANY, u"Playing: xxxxxxx.wav", wx.DefaultPosition, wx.DefaultSize, 0 )
		self.m_playing_file.Wrap( -1 )

		bSizer8.Add( self.m_playing_file, 0, wx.ALL, 5 )


		bSizer8.Add( ( 0, 0), 1, wx.EXPAND, 5 )


		bSizer6.Add( bSizer8, 5, wx.EXPAND, 5 )


		self.m_panel3.SetSizer( bSizer6 )
		self.m_panel3.Layout()
		bSizer6.Fit( self.m_panel3 )
		bSizer5.Add( self.m_panel3, 1, wx.EXPAND |wx.ALL, 5 )


		self.m_panel2.SetSizer( bSizer5 )
		self.m_panel2.Layout()
		bSizer5.Fit( self.m_panel2 )
		bSizer1.Add( self.m_panel2, 1, wx.EXPAND |wx.ALL, 5 )

		self.m_animCtrl4 = wx.adv.AnimationCtrl( self, wx.ID_ANY, wx.adv.NullAnimation, wx.DefaultPosition, wx.Size( 500,200 ), wx.adv.AC_DEFAULT_STYLE )
		bSizer1.Add( self.m_animCtrl4, 0, wx.ALL, 5 )


		self.SetSizer( bSizer1 )
		self.Layout()

		self.Centre( wx.BOTH )

		# Connect Events
		self.Bind( wx.EVT_CLOSE, self.close_even )
		self.m_threshold.Bind( wx.EVT_CHOICE, self.threshold_choice_handle )

	def __del__( self ):
		pass


	# Virtual event handlers, overide them in your derived class
	def close_even( self, event ):
		event.Skip()

	def threshold_choice_handle( self, event ):
		event.Skip()

实现文件(wx_plot.py)

# -*- coding: utf-8 -*-
import wx
import os
import wave
import pyaudio
import random
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.lines as line
from matplotlib.backends import backend_wxagg
from multiprocessing import Process, Queue, freeze_support
from display_ui import DspUI
from threading import Thread
import time
import ctypes
q = Queue(10)


def cb(cb_q):
    def callback(in_data, frame_count, time_info, status):  # all parameters are need!
        rt_data = np.frombuffer(in_data, np.dtype('<i2'))
        val_list = np.hsplit(rt_data, 32)
        avg_list = [each.mean() for each in val_list]  # data average
        if cb_q.full():
            cb_q.get()
        cb_q.put(avg_list)
        return None, pyaudio.paContinue
    return callback


class Lintener(Process):
    def __init__(self, _q):
        Process.__init__(self)
        self.q = _q
        self.CHUNK = 1024
        self.FORMAT = pyaudio.paInt16
        self.CHANNELS = 2
        self.RATE = 44100

    def run(self):
        p = pyaudio.PyAudio()
        p.open(format=self.FORMAT,
               channels=self.CHANNELS,
               rate=self.RATE,
               input=True,
               frames_per_buffer=self.CHUNK,
               stream_callback=cb(self.q))

        print("\nListening...")
        while True:
            pass


class Player(object):
    def __init__(self):
        self.waveOutGetVolume = ctypes.windll.winmm.waveOutGetVolume
        self.waveOutSetVolume = ctypes.windll.winmm.waveOutSetVolume
        self.MINIMUM_VOLUME = 0
        self.MAXIMUM_VOLUME = 65535
        self.CHUNK = 1024
        self.busy = False
        self.p = pyaudio.PyAudio()

    def __del__(self):
        self.p.terminate()

    def set_volume(self, volume):

        volume = int(volume * self.MAXIMUM_VOLUME / 100)
        volume = (volume << 16) | volume
        ret = self.waveOutSetVolume(0, volume)
        get_val = ctypes.c_long(0)
        print(self.waveOutGetVolume(0, ctypes.byref(get_val)))  # b'\xff\xff\xff\xff' 高16位,低16位代表两个声道
        print(bytes(get_val))

        if ret != 0:
            raise WindowsError("Error %d while setting volume" % ret)

    def _play(self, _cb, select_file_path):

        wf = wave.open(select_file_path, 'rb')

        stream = self.p.open(format=self.p.get_format_from_width(wf.getsampwidth()),
                             channels=wf.getnchannels(),
                             rate=wf.getframerate(),
                             output=True)
        data = wf.readframes(self.CHUNK)
        while data != b'':
            stream.write(data)
            data = wf.readframes(self.CHUNK)
        stream.stop_stream()
        stream.close()

        if _cb:
            _cb()
        time.sleep(1)
        self.busy = False

    def play(self, _cb=None):
        self.busy = True
        dir_path = './audio'
        files = os.listdir(dir_path)
        select_file = random.randint(0, len(files) - 1)
        select_file_path = os.path.join(dir_path, files[select_file])
        print('Selected:' + files[select_file])

        th_play = Thread(target=self._play, args=(_cb,select_file_path,))
        th_play.setDaemon(True)
        th_play.start()
        return files[select_file]


class Displayer(DspUI):
    def __init__(self, _q, player):
        DspUI.__init__(self, None)
        self.player = player
        self.trig_threshold = 0
        self.__thresh_items = ['1%', '2%', '5%', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', '100%']
        self.m_threshold.SetItems(self.__thresh_items)
        self.m_threshold.Select(0)  # init to select the first one
        self.update_trig_threshold(0)

        self.voice_power = 0
        self.m_playshow.Value = 0  #

        self.m_voice_pow.Enable(False)

        self.x_length = 768
        self.fig = plt.figure(1, figsize=(5, 2), facecolor='#000000',)  # (171,171,171)
        self.panel = backend_wxagg.FigureCanvasWxAgg(self.m_animCtrl4, -1, self.fig)
        self.ax = self.panel.figure.gca()  # *
        self.ax = plt.axes(xlim=(0, self.x_length), ylim=(-20000, 20000))  # , facecolor='#ABABAB')  # (-32768, 32767)
        self.linem = line.Line2D([], [], linewidth='0.5', color='#00FFFF')
        self.q = _q
        self.y_data = [0 for _ in range(self.x_length)]
        plt.axis('off')  # hide axis
        plt.xticks([])  # hide coordinate scale
        plt.yticks([])  # hide coordinate scale
        plt.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
        self.panel.draw()
        self.ani = animation.FuncAnimation(self.fig, self.update(),
                                           init_func=self.init,
                                           frames=1,
                                           interval=1,
                                           blit=True)

    def close_even(self, event):
        self.ani.event_source.stop()
        event.Skip()

    def update_trig_threshold(self, item_index):
        selected_item = self.__thresh_items[item_index].replace('%', '')
        self.trig_threshold = int(10000. * int(selected_item) / 100.)
        print(selected_item, self.trig_threshold)

    def threshold_choice_handle(self, event):
        self.update_trig_threshold(self.m_threshold.GetSelection())
        event.Skip()

    def init(self):
        self.ax.add_line(self.linem)
        return self.linem,

    def cb_form_player(self):
        self.m_playshow.Value = 0
        self.m_voice_pow.SetValue('')
        self.m_playing_file.SetLabel('')
        print('Done!')

    def update(self):
        def inner_update(i):
            if not q.empty():
                self.y_data = self.y_data[32:] + q.get()
            self.linem.set_xdata(np.arange(0, self.x_length, 1))
            self.linem.set_ydata(self.y_data)
            if not self.player.busy and (self.y_data[0] > self.trig_threshold):
                self.m_playshow.Value = 1
                self.voice_power = np.mean([abs(each) for each in self.y_data if abs(each) > 30])
                self.m_voice_pow.SetValue(str(self.voice_power))
                self.player.set_volume(min(100, int(self.voice_power/20)))
                play_file = self.player.play(self.cb_form_player)
                self.m_playing_file.SetLabel('Playing: ' + str(play_file))
            return self.linem,
        return inner_update


if __name__ == '__main__':
    freeze_support()
    listener = Lintener(q)
    listener.start()
    app = wx.App()
    display = Displayer(q, Player())
    display.Show(True)
    app.MainLoop()
    listener.terminate()

打包部分

打包指令:

pyinstaller -F -w -i xx.ico wx_plot.py
# 调试的时候可以不用 -w 参数,可以显示控制台,看发生了什么。

大坑总结

1、pyinstall的多进程的特殊处理

if __name__ == '__main__':
    freeze_support()  # 必须要有这么一句,否则程序会不断启动自己
    ...
    ..

2、matplotlib实时图形的背景颜色、线径等设置,参考代码。

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值