Matplotlib: animation for audio player cursor (sliding vertical line) with blitting looks broken on WxPython

♀尐吖头ヾ 提交于 2020-01-22 16:35:23

问题


I am building up a project that requires an audio player for very specific purposes. I'm currently using WxPython, Matplotlib and PyAudio packages. I'm also using Matplotlib's WXAgg backend (backend_wxagg).

The basic idea is very simple: the audio data will be plotted on the main window and simultaneously played through PyAudio while showing playback progress on the plot — which I intend to do with an animated vertical line (horizontally sliding cursor), pretty much the type that you see in Audacity, for instance. I've already tried the generic Matplotlib animation examples, and some others spread through the web, but they are either too slow (no blitting), or rely upon FuncAnimation (an architecture that doesn't fit in my project), or utilize the technique that I'm trying to use (which is not working so far).

Something actually moves on the screen but the overall picture is a mess... A white filled rectangle appears over the plot on my Ubuntu 16 desktop, while a black filled rectangle appears on my Mint laptop. Despite having tried so hard to work on it for days and ALMOST getting it to work, time has come to humbly ask you for help... :/

I'm insisting on the blit() method because as far as I know it (i) allows me to refresh the plotting under custom events (in this case an audio frame consumption) and (ii) has a good performance (which is a concern here due to the large, variable size dataset).

Stripping my project down to the bare minimum, here's the code that, once taken care of, will hopefully allow me to fix my entire application (2000+ lines):

# -*- 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()

Thank you so much for your help and patience! Hopefully this will help not only me, but also someone else struggling against WxPython WXAgg backend blitting.


回答1:


After more research, I finally found out that the solution was to call the draw() method of canvas object before effectively copying from bbox. Thus, the middle line here is the answer (the other ones serve only as a reference to the correct spot to place the fix):

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

But I must add here that, while this might work for some cases, any scenario where your plot gets resized will probably result in a broken image again. So, in order to fix that, bind a method of yours to the "resize_event" of the figure canvas, and inside your method force a redraw and a new copy:

    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)

(...)

And there you go! This problem has consumed a lot of my project's time, so I make it a point to share the solution with everyone else, even because this might be the first functional audio player template available on the Internet with WxPython, Matplotlib and PyAudio. Hope you find it useful!



来源:https://stackoverflow.com/questions/39523011/matplotlib-animation-for-audio-player-cursor-sliding-vertical-line-with-blitt

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!