Python Tkinter 音频播放 GUI play/pause pyaudio 的功能 - 无法从暂停的地方恢复
Python Tkinter audio playback GUI play/pause functionality with pyaudio - can't resume from where paused
我正在构建一个用于处理生态录音的小型音频 GUI。我希望能够播放一个文件,暂停它,然后从我暂停的地方再次播放它。我可以让它播放和暂停(停止),但是当我再次点击播放时它会重新启动音频,而不是从它停止的地方开始播放。
Pyaudio 具有我正在尝试实现的回调函数 ()。这个例子几乎是我想要的,除了这个例子在控制 play/pause 的 while 语句中有 'keyboard.Listener' 行,我需要从 tkinter 实现 play/pause 按钮功能。我还对播放进行了线程化处理,这样 GUI 就不会冻结按钮,这对我来说增加了一些复杂性(我是一名生态学家,在 python 中自学成才,而不是计算机科学家!)。
我已经尝试使用 threading.Event() 作为控制流线程的一种方式,但我认为这只会增加额外的复杂性并让我面临从暂停位置重新启动的相同问题。
最终我还想在暂停时拉出文件的框架 number/time,并在 tkinter 上绘制进度条 canvas/matplot 图 - 我的一部分说 pyaudio 。get_time() 嵌入在回调中可能会对此有所帮助(我认为它是 returns 系统时间)。
下面是一个最小的例子,我可以用我所在的位置获得一个 gui。
import tkinter as tk
from tkinter import ttk
import wave
import pyaudio
import threading
import time
import numpy as np
import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# gui class
class basic_player():
def __init__(self, root):
# BUILD ROOT
self.root = root
root.title('Playback')
self.audio_file = 'C:/Data/Acoustics/Test/5D8CA5E8.WAV'
self.frame_plot()
self.frame_buttons()
# class globals
self.stream_paused = False
def frame_plot(self):
'''Frame for file plot'''
self.frame_plot = ttk.Frame(self.root, height = 100, width = 500)
self.frame_plot.grid(column = 0, row = 0, sticky = 'nsew', columnspan = 2)
self.frame_plot.grid_propagate(False)
# plot file
self.func_plot_fileplot()
def func_plot_fileplot(self):
'''Plot the main audiofile'''
# create figure to contain plot
# get frame size parameters (update frame parameters first)
self.frame_plot.update()
dpi = self.root.winfo_fpixels('1i')
plotwidth = self.frame_plot.winfo_width() / dpi
plotheight = self.frame_plot.winfo_height() / dpi
# create plot
plot_figure_fileplot_main = Figure(figsize = (plotwidth, plotheight),
dpi = dpi, frameon = False, tight_layout = True)
# get data
with wave.open(self.audio_file, mode = 'rb') as wf:
infile_audio_bytes = wf.readframes(wf.getnframes())
data = np.frombuffer(infile_audio_bytes, dtype = np.int16)
# plot x labels
lst_x_ticks = list(range(0, wf.getnframes(), int(wf.getnframes() / 8))) + [wf.getnframes()]
lst_x_label = [str(datetime.timedelta(seconds = int(sample / wf.getframerate()))) for sample in lst_x_ticks]
# add subplot
plot_figure_fileplot = plot_figure_fileplot_main.add_subplot(111)
plot_figure_fileplot.plot(data, linewidth = 0.25)
# adjust subplot visuals
plot_figure_fileplot.set_xmargin(0)
plot_figure_fileplot.yaxis.set_visible(False)
plot_figure_fileplot.spines['top'].set_visible(False)
plot_figure_fileplot.spines['right'].set_visible(False)
plot_figure_fileplot.spines['left'].set_visible(False)
# labels for plot x axis
plot_figure_fileplot.set_xticks(lst_x_ticks) # set x labels to existing to make sure they find the right spot
plot_figure_fileplot.set_xticklabels(lst_x_label, size = 8)
#create tkinter canvas
self.canvas_plot_figure_main = FigureCanvasTkAgg(plot_figure_fileplot_main, master = self.frame_plot)
self.canvas_plot_figure_main.draw()
#place canvas on tkinter window
self.canvas_plot_figure_main.get_tk_widget().grid(sticky = 'nsew')
def frame_buttons(self):
'''The main frame for the initial window'''
frame_buttons = ttk.Frame(self.root, width = 100)
frame_buttons.grid(column = 0, row = 1, sticky = 'nsew')
btn_play = tk.Button(frame_buttons,
text = 'PLAY',
command = self.buttons_command_play,
state = 'normal',
width = 10)
btn_play.grid(column = 0, row = 0, sticky = 'nsew', padx = 10, pady = 10)
btn_pause = tk.Button(frame_buttons,
text = 'PAUSE',
command = self.buttons_command_playback_pause,
state = 'normal',
width = 10)
btn_pause.grid(column = 1, row = 0, sticky = 'nsew', padx = 10, pady = 10)
def buttons_command_play(self):
''' send play audio function to thread '''
self.stream_paused = False
self.stream_thread = threading.Thread(target = self.play_audio)
self.stream_thread.start()
def play_audio(self):
'''Play audio'''
if self.stream_paused: # this doesnt work.
self.stream.start_stream()
else:
# open file
wf = wave.open(self.audio_file, mode = 'rb')
# instantiate pyaudio
self.pyaudio_init = pyaudio.PyAudio()
# define callback
def callback(in_data, frame_count, time_info, status):
data = wf.readframes(frame_count)
return (data, pyaudio.paContinue)
# open stream using callback
self.stream = self.pyaudio_init.open(format=self.pyaudio_init.get_format_from_width(wf.getsampwidth()),
input = False,
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
stream_callback=callback)
self.stream.start_stream()
# start the stream
while self.stream.is_active() and not self.stream_paused:
# this is where the control event needs to work i believe
time.sleep(0.1)
# stop stream
self.stream.stop_stream()
self.stream.close()
wf.close()
def buttons_command_playback_pause(self):
''' Pause the audio '''
if not self.stream_paused:
self.stream_paused = True
else:
pass
## SETUP AND RUN
root = tk.Tk()
basic_player(root)
root.mainloop()
stream_callback
在这里是不必要的。您可以改为创建一个新线程和 运行 循环中的 stream.write()
。
要暂停音频设置一个标志,并在循环中添加一个条件。仅当暂停条件为 False
时写入流
这是一个例子。
import tkinter as tk
import wave
import pyaudio
import threading
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
self.after_id = None
def start_playing(self):
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format =
p.get_format_from_width(wf.getsampwidth()),
channels = wf.getnchannels(),
rate = wf.getframerate(),
output = True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if not self.paused:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing=False
stream.close()
p.terminate()
def pause(self):
self.paused = True
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.after_id is None:
self.update_lbl()
self.paused = False
def stop(self):
self.playing = False
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def update_lbl(self):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
self.after_id = self.current_lbl.after(5, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
这个答案与@Art 的答案相同,但我删除了 self.after_id
变量以稍微简化逻辑:
import tkinter as tk
import threading
import pyaudio
import wave
import time
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
# If you aren't going to use `\`s there is no need for the
# "r" before the start of the string
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
def start_playing(self):
""" # I don't have `pyaudio` so I used this to test my answer:
self.audio_length = 200
while self.playing:
if not self.paused:
self.current_sec += 1
time.sleep(1)
return None
# """
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if self.paused:
time.sleep(0.1)
else:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing = False
stream.close()
p.terminate()
def pause(self):
self.paused = True
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.paused:
self.paused = False
self.update_lbl()
def stop(self):
self.playing = False
def update_lbl(self):
if self.playing and (not self.paused):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
# There is no need to update the label more than 10 times a second.
# It changes once per second anyways.
self.current_lbl.after(100, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
如果您可以将 if self.playing and (not self.paused)
添加到调用 .after
的代码中,则不需要 self.after_id
变量。在这种情况下,它是 update_lbl
方法。
我正在构建一个用于处理生态录音的小型音频 GUI。我希望能够播放一个文件,暂停它,然后从我暂停的地方再次播放它。我可以让它播放和暂停(停止),但是当我再次点击播放时它会重新启动音频,而不是从它停止的地方开始播放。
Pyaudio 具有我正在尝试实现的回调函数 (
最终我还想在暂停时拉出文件的框架 number/time,并在 tkinter 上绘制进度条 canvas/matplot 图 - 我的一部分说 pyaudio 。get_time() 嵌入在回调中可能会对此有所帮助(我认为它是 returns 系统时间)。
下面是一个最小的例子,我可以用我所在的位置获得一个 gui。
import tkinter as tk
from tkinter import ttk
import wave
import pyaudio
import threading
import time
import numpy as np
import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# gui class
class basic_player():
def __init__(self, root):
# BUILD ROOT
self.root = root
root.title('Playback')
self.audio_file = 'C:/Data/Acoustics/Test/5D8CA5E8.WAV'
self.frame_plot()
self.frame_buttons()
# class globals
self.stream_paused = False
def frame_plot(self):
'''Frame for file plot'''
self.frame_plot = ttk.Frame(self.root, height = 100, width = 500)
self.frame_plot.grid(column = 0, row = 0, sticky = 'nsew', columnspan = 2)
self.frame_plot.grid_propagate(False)
# plot file
self.func_plot_fileplot()
def func_plot_fileplot(self):
'''Plot the main audiofile'''
# create figure to contain plot
# get frame size parameters (update frame parameters first)
self.frame_plot.update()
dpi = self.root.winfo_fpixels('1i')
plotwidth = self.frame_plot.winfo_width() / dpi
plotheight = self.frame_plot.winfo_height() / dpi
# create plot
plot_figure_fileplot_main = Figure(figsize = (plotwidth, plotheight),
dpi = dpi, frameon = False, tight_layout = True)
# get data
with wave.open(self.audio_file, mode = 'rb') as wf:
infile_audio_bytes = wf.readframes(wf.getnframes())
data = np.frombuffer(infile_audio_bytes, dtype = np.int16)
# plot x labels
lst_x_ticks = list(range(0, wf.getnframes(), int(wf.getnframes() / 8))) + [wf.getnframes()]
lst_x_label = [str(datetime.timedelta(seconds = int(sample / wf.getframerate()))) for sample in lst_x_ticks]
# add subplot
plot_figure_fileplot = plot_figure_fileplot_main.add_subplot(111)
plot_figure_fileplot.plot(data, linewidth = 0.25)
# adjust subplot visuals
plot_figure_fileplot.set_xmargin(0)
plot_figure_fileplot.yaxis.set_visible(False)
plot_figure_fileplot.spines['top'].set_visible(False)
plot_figure_fileplot.spines['right'].set_visible(False)
plot_figure_fileplot.spines['left'].set_visible(False)
# labels for plot x axis
plot_figure_fileplot.set_xticks(lst_x_ticks) # set x labels to existing to make sure they find the right spot
plot_figure_fileplot.set_xticklabels(lst_x_label, size = 8)
#create tkinter canvas
self.canvas_plot_figure_main = FigureCanvasTkAgg(plot_figure_fileplot_main, master = self.frame_plot)
self.canvas_plot_figure_main.draw()
#place canvas on tkinter window
self.canvas_plot_figure_main.get_tk_widget().grid(sticky = 'nsew')
def frame_buttons(self):
'''The main frame for the initial window'''
frame_buttons = ttk.Frame(self.root, width = 100)
frame_buttons.grid(column = 0, row = 1, sticky = 'nsew')
btn_play = tk.Button(frame_buttons,
text = 'PLAY',
command = self.buttons_command_play,
state = 'normal',
width = 10)
btn_play.grid(column = 0, row = 0, sticky = 'nsew', padx = 10, pady = 10)
btn_pause = tk.Button(frame_buttons,
text = 'PAUSE',
command = self.buttons_command_playback_pause,
state = 'normal',
width = 10)
btn_pause.grid(column = 1, row = 0, sticky = 'nsew', padx = 10, pady = 10)
def buttons_command_play(self):
''' send play audio function to thread '''
self.stream_paused = False
self.stream_thread = threading.Thread(target = self.play_audio)
self.stream_thread.start()
def play_audio(self):
'''Play audio'''
if self.stream_paused: # this doesnt work.
self.stream.start_stream()
else:
# open file
wf = wave.open(self.audio_file, mode = 'rb')
# instantiate pyaudio
self.pyaudio_init = pyaudio.PyAudio()
# define callback
def callback(in_data, frame_count, time_info, status):
data = wf.readframes(frame_count)
return (data, pyaudio.paContinue)
# open stream using callback
self.stream = self.pyaudio_init.open(format=self.pyaudio_init.get_format_from_width(wf.getsampwidth()),
input = False,
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True,
stream_callback=callback)
self.stream.start_stream()
# start the stream
while self.stream.is_active() and not self.stream_paused:
# this is where the control event needs to work i believe
time.sleep(0.1)
# stop stream
self.stream.stop_stream()
self.stream.close()
wf.close()
def buttons_command_playback_pause(self):
''' Pause the audio '''
if not self.stream_paused:
self.stream_paused = True
else:
pass
## SETUP AND RUN
root = tk.Tk()
basic_player(root)
root.mainloop()
stream_callback
在这里是不必要的。您可以改为创建一个新线程和 运行 循环中的 stream.write()
。
要暂停音频设置一个标志,并在循环中添加一个条件。仅当暂停条件为 False
这是一个例子。
import tkinter as tk
import wave
import pyaudio
import threading
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
self.after_id = None
def start_playing(self):
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format =
p.get_format_from_width(wf.getsampwidth()),
channels = wf.getnchannels(),
rate = wf.getframerate(),
output = True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if not self.paused:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing=False
stream.close()
p.terminate()
def pause(self):
self.paused = True
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.after_id is None:
self.update_lbl()
self.paused = False
def stop(self):
self.playing = False
if self.after_id:
self.current_lbl.after_cancel(self.after_id)
self.after_id = None
def update_lbl(self):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
self.after_id = self.current_lbl.after(5, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
这个答案与@Art 的答案相同,但我删除了 self.after_id
变量以稍微简化逻辑:
import tkinter as tk
import threading
import pyaudio
import wave
import time
class SamplePlayer:
def __init__(self, master):
frame = tk.Frame(master=master)
frame.pack(expand=True, fill="both")
self.current_lbl = tk.Label(master=frame, text="0/0")
self.current_lbl.pack()
self.pause_btn = tk.Button(master=frame, text="Pause", command=self.pause)
self.pause_btn.pack()
self.play_btn = tk.Button(master=frame, text="Play", command=self.play)
self.play_btn.pack()
# If you aren't going to use `\`s there is no need for the
# "r" before the start of the string
self.file = r"sample_wavfile.wav"
self.paused = True
self.playing = False
self.audio_length = 0
self.current_sec = 0
def start_playing(self):
""" # I don't have `pyaudio` so I used this to test my answer:
self.audio_length = 200
while self.playing:
if not self.paused:
self.current_sec += 1
time.sleep(1)
return None
# """
p = pyaudio.PyAudio()
chunk = 1024
with wave.open(self.file, "rb") as wf:
self.audio_length = wf.getnframes() / float(wf.getframerate())
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(chunk)
chunk_total = 0
while data != b"" and self.playing:
if self.paused:
time.sleep(0.1)
else:
chunk_total += chunk
stream.write(data)
data = wf.readframes(chunk)
self.current_sec = chunk_total/wf.getframerate()
self.playing = False
stream.close()
p.terminate()
def pause(self):
self.paused = True
def play(self):
if not self.playing:
self.playing = True
threading.Thread(target=self.start_playing, daemon=True).start()
if self.paused:
self.paused = False
self.update_lbl()
def stop(self):
self.playing = False
def update_lbl(self):
if self.playing and (not self.paused):
self.current_lbl.config(text=f"{self.current_sec}/{self.audio_length}")
# There is no need to update the label more than 10 times a second.
# It changes once per second anyways.
self.current_lbl.after(100, self.update_lbl)
def handle_close():
player.stop()
root.destroy()
## SETUP AND RUN
root = tk.Tk()
player = SamplePlayer(root)
root.protocol("WM_DELETE_WINDOW", handle_close)
root.mainloop()
如果您可以将 if self.playing and (not self.paused)
添加到调用 .after
的代码中,则不需要 self.after_id
变量。在这种情况下,它是 update_lbl
方法。