python - Matplotlib:音频播放器光标(滑动垂直线)的动画,在 WxPython 上,位 block 传输看起来损坏

标签 python animation matplotlib wxpython blit

我正在构建一个项目,需要一个用于非常特定目的的音频播放器。我目前正在使用 WxPython、Matplotlib 和 PyAudio 包。我还使用 Matplotlib 的 WXAgg 后端 (backend_wxagg)。

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

屏幕上确实有东西在移动,但整体画面一团糟...在我的 Ubuntu 16 桌面上,绘图上方出现了一个白色实心矩形,而 <我的 Mint 笔记本电脑上出现strong>黑色填充矩形。尽管已经努力工作好几天了,而且几乎要让它工作了,但现在是时候谦虚地向你寻求帮助了……:/

我坚持使用 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 的人。

最佳答案

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

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

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

    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 提供的功能性音频播放器模板。希望您觉得它有用!

关于python - Matplotlib:音频播放器光标(滑动垂直线)的动画,在 WxPython 上,位 block 传输看起来损坏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39523011/

相关文章:

python - 使用嵌套模式更新对象的最佳方式

css - 如何使 `react-beautiful-dnd` 不触发 `react-transition-group` 动画?

python - 如何在 matplotlib 中删除环形图的偏离中心部分?

python - 如何每次都得到一个随机列表?

python - 如何在 numpy 中对二维数组进行 bin?

python - 如何加快动画速度?

ios - xcode图像位置x y实时更新

python - 仅从控制台运行时 Matplotlib pdf 保存失败

python - 为什么我得到 UserWarning : Matplotlib is currently using ps, 这是一个非 GUI 后端,所以无法显示图形

python - 如何知道ui窗体中的哪个qwidget在pyqt中获得了焦点