Reputation: 370
I am building a small audio GUI for processing ecological recordings. I want to be able to play a file, and pause it, and play it again from where I paused it. I can get it to play, and pause(stop), but when I hit play again it restarts the audio, not picks up from where it left off. Pyaudio has the callback function which is what I am trying to implement (see here for working example). That example is pretty much what I want, except where this example has the 'keyboard.Listener' line in the while statement controlling the play/pause, I need to implement the play/pause button functionality from tkinter. I have also threaded the playback so that the GUI does not freeze the buttons, which has added some more complexity for me (I am an ecologist self taught in python, not a computer scientist!). I have played around with threading.Event() for this as a way to control the stream thread, but I think that will just add additional complexity and leave me at the same problem of restarting from pause location.
Eventually I'd also like to pull out the frame number/time of file when paused, and also plot a progress bar on the tkinter canvas/matplot Figure - part of me says the pyaudio .get_time() embedded within the callback may be able to help with this (i think it returns system time).
Below is a minimum example I could make to get a gui working with where Im at.
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()
Upvotes: 1
Views: 1832
Reputation: 7680
This answer is the same as @Art's answer but I removed the self.after_id
variable to simplify the logic a bit:
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()
There is no need for a self.after_id
variable if you can just add if self.playing and (not self.paused)
to the code that calls .after
. In this case it's the update_lbl
method.
Upvotes: 2
Reputation: 3089
stream_callback
is unnecessary here. You could instead create a new thread and run the stream.write()
in a loop.
To pause the audio set a flag, and add a condition in the loop. write the stream only if the pause condition is False
Here is an example.
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()
Upvotes: 2