Matplotlib:音频播放器光标(滑动垂直线)的动画,在WxPython上打破了blitting外观

时间:2016-09-16 02:39:37

标签: python animation matplotlib wxpython blit

我正在建立一个需要音频播放器用于特定目的的项目。我目前正在使用WxPython,Matplotlib和PyAudio软件包。我也在使用Matplotlib的WXAgg后端(backend_wxagg)。

基本思路非常简单:音频数据将在主窗口上绘制并同时通过PyAudio播放,同时在绘图上显示播放进度 - 我打算用动画垂直线做(水平滑动光标),几乎就是您在Audacity中看到的类型。我已经尝试过通用的Matplotlib动画示例,其他一些通过网络传播,但它们要么太慢(没有blitting),要么依赖于FuncAnimation(一种不适合我项目的架构),或者使用我正在尝试使用的技术(到目前为止还没有使用)。

屏幕上实际有什么东西在移动,但总体情况一团糟......在我的Ubuntu 16桌面上的上出现了白色填充的矩形,而<我的Mint笔记本电脑上出现了强烈的黑色填充矩形。尽管已经努力工作了好几天并且几乎要让它发挥作用,但是时间已经到了谦卑地请求你帮助......:/

我坚持使用blit()方法,因为据我所知(i)允许我刷新自定义事件下的绘图(在这种情况下是音频帧消耗)和(ii)有一个好的性能(由于大型可变大小的数据集,这是一个关注点)。

将我的项目剥离到最低限度,这里的代码一旦得到处理,将有希望允许我修复我的整个应用程序(2000多行):

# -*- coding: UTF-8 -*-
#

import wx
import gettext
import struct
import matplotlib
matplotlib.use('WX')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure
import pyaudio
import numpy as np
import time

CHUNK_SIZE = 1024       # Size (in samples) of each audio_callback reading.
BYTES_PER_FRAME = 2     # Number of bytes in each audio frame.
CHANNELS = 1            # Number of channels.
SAMPLING_RATE = 11025   # Audio sampling rate.

audio_chunks = []

# A simple frame with a tab (generated by WxGlade and simplified for example purposes):
class PlayerFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.CAPTION | wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.MAXIMIZE | wx.MAXIMIZE_BOX | wx.SYSTEM_MENU | wx.RESIZE_BORDER | wx.CLIP_CHILDREN
        wx.Frame.__init__(self, *args, **kwds)
        self.main_notebook = wx.Notebook(self, wx.ID_ANY, style=0)
        self.__set_properties()
        self.__do_layout()
        self.initAudio()        # Initiates audio variables and event binding.
        self.initPlotting()     # Initiates plotting variables and widgets.
        self.startPlayback()    # Starts audio playback.
    def __set_properties(self):
        self.SetTitle(_("Audio signal plotting and playback with cursor"))
        self.SetSize((720, 654))
    def __do_layout(self):
        sizer_main = wx.BoxSizer(wx.VERTICAL)
        sizer_main.Add(self.main_notebook, 1, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 25)
        self.SetSizer(sizer_main)
        self.Layout()

    # Audio stuff initialization:
    def initAudio(self):
        # Binds the playback move event to a handler:
        self.Bind(EVT_PLAYBACK_MOVE, self.OnPlaybackMove)
        # Creates an empty audio chunk with "CHUNK_SIZE" samples of zero value ([0, 0, ..., 0, 0]):
        empty_chunk = struct.pack("<h", 0)*CHUNK_SIZE
        # Initializes audio chunk array with 20 empty audio chunks:
        audio_chunks.extend([empty_chunk]*20)
        # Points playback_counter to the first audio chunk:
        global playback_counter; playback_counter = 0

    def startPlayback(self):
        # Initializes audio playback:
        global p; p = pyaudio.PyAudio()
        global audio_stream; audio_stream = p.open  ( format = p.get_format_from_width(BYTES_PER_FRAME)
                                                    , channels = CHANNELS
                                                    , rate = SAMPLING_RATE
                                                    , output = True
                                                    , stream_callback = audio_callback
                                                    , frames_per_buffer = CHUNK_SIZE )

    # Plotting stuff initialization:
    def initPlotting(self):
        # Converts the raw audio chunks to a normal array:
        samples = np.fromstring(b''.join(audio_chunks), dtype=np.int16)
        # Creates plot supporting widgets:
        self.pane = wx.Panel(self.main_notebook, wx.ID_ANY)
        self.canvas = FigureCanvas(self.pane, wx.ID_ANY, Figure())
        self.figure = self.canvas.figure
        self.pane.SetMinSize((664, 355))
        sizer_15 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_16 = wx.BoxSizer(wx.VERTICAL)
        sizer_10 = wx.BoxSizer(wx.HORIZONTAL)
        sizer_10.Add(self.canvas, 1, wx.EXPAND, 0)
        sizer_16.Add(sizer_10, 2, wx.BOTTOM | wx.EXPAND, 25)
        sizer_15.Add(sizer_16, 1, wx.ALL | wx.EXPAND, 25)
        self.pane.SetSizer(sizer_15)
        self.main_notebook.AddPage(self.pane, _("my_audio.wav"))

        # ================================================
        # Initializes plotting (is the problem in here???)
        # ================================================
        t = range(len(samples))
        self.axes1 = self.figure.add_subplot(111)
        self.axes1.set_xlim(0, len(samples))
        self.axes1.set_ylim(-32768, 32767)
        self.line1, = self.axes1.plot(t, samples)
        self.Layout()
        self.background = self.figure.canvas.copy_from_bbox(self.axes1.bbox)
        self.playback_line = self.axes1.axvline(color="y", animated=True)

    # For each new chunk read by the audio_callback function, we update the cursor position on the plot.
    # It's important to notice that the audio_callback function CANNOT manipulate UI's widgets on it's
    # own, because they live in different threads and Wx allows only the main thread to perform UI changes.
    def OnPlaybackMove(self, event):
        # =================================================
        # Updates the cursor (vertical line) at each event:
        # =================================================
        self.figure.canvas.restore_region(self.background)
        new_position = playback_counter*CHUNK_SIZE
        self.playback_line.set_xdata(new_position)
        self.axes1.draw_artist(self.playback_line)
        self.canvas.blit(self.axes1.bbox)

# Playback move event (for indicating that a chunk has just been played and so the cursor must be moved):
EVT_PLAYBACK_MOVE = wx.PyEventBinder(wx.NewEventType(), 0)
class PlaybackMoveEvent(wx.PyCommandEvent):
    def __init__(self, eventType=EVT_PLAYBACK_MOVE.evtType[0], id=0):
        wx.PyCommandEvent.__init__(self, eventType, id)

# Callback function for audio playback (called each time the sound card needs "frame_count" more samples):
def audio_callback(in_data, frame_count, time_info, status):
    global playback_counter
    # In case we've run out of samples:
    if playback_counter == len(audio_chunks):
        print "Playback ended."
        # Returns an empty chunk, thus ending playback:
        return ("", pyaudio.paComplete)
    else:
        # Gets the next audio chunk, increments the counter and returns the new chunk:
        new_chunk = audio_chunks[playback_counter]
        main_window.AddPendingEvent(PlaybackMoveEvent())
        playback_counter += 1
        return (new_chunk, pyaudio.paContinue)

# WxGlade default initialization instructions:
if __name__ == "__main__":
    gettext.install("app")
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    main_window = PlayerFrame(None, wx.ID_ANY, "")
    app.SetTopWindow(main_window)
    main_window.Show()
    app.MainLoop()  # UI's main loop. Checks for events and stuff.

    # Final lines (if we're executing here, this means the program is closing):
    audio_stream.close()
    p.terminate()

非常感谢你的帮助和耐心!希望这不仅有助于我,也有助于其他人在WxPython WXAgg后端blitting中挣扎。

1 个答案:

答案 0 :(得分:2)

经过更多的研究,我终于发现解决方案是在从bbox进行有效复制之前调用canvas对象的draw()方法。因此,这里的中间线是答案(其他的仅作为对放置修正的正确位置的参考):

    (...)
    self.Layout()
    self.figure.canvas.draw()   # THIS is the solution.
    self.background = self.figure.canvas.copy_from_bbox(self.axes1.bbox)

但我必须在此补充一点,虽然这可能适用于某些情况,但是任何情节调整大小的情况都可能会再次导致图像损坏。因此,为了解决这个问题,请将您的方法绑定到图形画布的"resize_event",并在方法内强制重绘和新副本:

    self.playback_line = self.axes1.axvline(color="y", animated=True)

    # New line here:
    self.figure.canvas.mpl_connect("resize_event", self.on_resize_canvas)

# New method here:
def on_resize_canvas(self, event):
    self.figure.canvas.draw()
    self.background = self.figure.canvas.copy_from_bbox(self.axes1.bbox)

(...)

你去吧!这个问题耗费了我项目的大部分时间,所以我要与其他人分享解决方案,即使因为这可能是因特网上第一个可用的WxPython,Matplotlib和PyAudio功能音频播放器模板。希望你觉得它很有用!