我正在构建一个项目,需要一个用于非常特定目的的音频播放器。我目前正在使用 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/