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 方法。