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.