p3_gui_player.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import tkinter as tk
  2. from tkinter import filedialog, messagebox
  3. import threading
  4. import time
  5. import opuslib
  6. import struct
  7. import numpy as np
  8. import sounddevice as sd
  9. import os
  10. def play_p3_file(input_file, stop_event=None, pause_event=None):
  11. """
  12. 播放p3格式的音频文件
  13. p3格式: [1字节类型, 1字节保留, 2字节长度, Opus数据]
  14. """
  15. # 初始化Opus解码器
  16. sample_rate = 16000 # 采样率固定为16000Hz
  17. channels = 1 # 单声道
  18. decoder = opuslib.Decoder(sample_rate, channels)
  19. # 帧大小 (60ms)
  20. frame_size = int(sample_rate * 60 / 1000)
  21. # 打开音频流
  22. stream = sd.OutputStream(
  23. samplerate=sample_rate,
  24. channels=channels,
  25. dtype='int16'
  26. )
  27. stream.start()
  28. try:
  29. with open(input_file, 'rb') as f:
  30. print(f"正在播放: {input_file}")
  31. while True:
  32. if stop_event and stop_event.is_set():
  33. break
  34. if pause_event and pause_event.is_set():
  35. time.sleep(0.1)
  36. continue
  37. # 读取头部 (4字节)
  38. header = f.read(4)
  39. if not header or len(header) < 4:
  40. break
  41. # 解析头部
  42. packet_type, reserved, data_len = struct.unpack('>BBH', header)
  43. # 读取Opus数据
  44. opus_data = f.read(data_len)
  45. if not opus_data or len(opus_data) < data_len:
  46. break
  47. # 解码Opus数据
  48. pcm_data = decoder.decode(opus_data, frame_size)
  49. # 将字节转换为numpy数组
  50. audio_array = np.frombuffer(pcm_data, dtype=np.int16)
  51. # 播放音频
  52. stream.write(audio_array)
  53. except KeyboardInterrupt:
  54. print("\n播放已停止")
  55. finally:
  56. stream.stop()
  57. stream.close()
  58. print("播放完成")
  59. class P3PlayerApp:
  60. def __init__(self, root):
  61. self.root = root
  62. self.root.title("P3 文件简易播放器")
  63. self.root.geometry("500x400")
  64. self.playlist = []
  65. self.current_index = 0
  66. self.is_playing = False
  67. self.is_paused = False
  68. self.stop_event = threading.Event()
  69. self.pause_event = threading.Event()
  70. self.loop_playback = tk.BooleanVar(value=False) # 循环播放复选框的状态
  71. # 创建界面组件
  72. self.create_widgets()
  73. def create_widgets(self):
  74. # 播放列表
  75. self.playlist_label = tk.Label(self.root, text="播放列表:")
  76. self.playlist_label.pack(pady=5)
  77. self.playlist_frame = tk.Frame(self.root)
  78. self.playlist_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
  79. self.playlist_listbox = tk.Listbox(self.playlist_frame, selectmode=tk.SINGLE)
  80. self.playlist_listbox.pack(fill=tk.BOTH, expand=True)
  81. # 复选框和移除按钮
  82. self.checkbox_frame = tk.Frame(self.root)
  83. self.checkbox_frame.pack(pady=5)
  84. self.remove_button = tk.Button(self.checkbox_frame, text="移除文件", command=self.remove_files)
  85. self.remove_button.pack(side=tk.LEFT, padx=5)
  86. # 循环播放复选框
  87. self.loop_checkbox = tk.Checkbutton(self.checkbox_frame, text="循环播放", variable=self.loop_playback)
  88. self.loop_checkbox.pack(side=tk.LEFT, padx=5)
  89. # 控制按钮
  90. self.control_frame = tk.Frame(self.root)
  91. self.control_frame.pack(pady=10)
  92. self.add_button = tk.Button(self.control_frame, text="添加文件", command=self.add_file)
  93. self.add_button.grid(row=0, column=0, padx=5)
  94. self.play_button = tk.Button(self.control_frame, text="播放", command=self.play)
  95. self.play_button.grid(row=0, column=1, padx=5)
  96. self.pause_button = tk.Button(self.control_frame, text="暂停", command=self.pause)
  97. self.pause_button.grid(row=0, column=2, padx=5)
  98. self.stop_button = tk.Button(self.control_frame, text="停止", command=self.stop)
  99. self.stop_button.grid(row=0, column=3, padx=5)
  100. # 状态标签
  101. self.status_label = tk.Label(self.root, text="未在播放", fg="blue")
  102. self.status_label.pack(pady=10)
  103. def add_file(self):
  104. files = filedialog.askopenfilenames(filetypes=[("P3 文件", "*.p3")])
  105. if files:
  106. self.playlist.extend(files)
  107. self.update_playlist()
  108. def update_playlist(self):
  109. self.playlist_listbox.delete(0, tk.END)
  110. for file in self.playlist:
  111. self.playlist_listbox.insert(tk.END, os.path.basename(file)) # 仅显示文件名
  112. def update_status(self, status_text, color="blue"):
  113. """更新状态标签的内容"""
  114. self.status_label.config(text=status_text, fg=color)
  115. def play(self):
  116. if not self.playlist:
  117. messagebox.showwarning("警告", "播放列表为空!")
  118. return
  119. if self.is_paused:
  120. self.is_paused = False
  121. self.pause_event.clear()
  122. self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green")
  123. return
  124. if self.is_playing:
  125. return
  126. self.is_playing = True
  127. self.stop_event.clear()
  128. self.pause_event.clear()
  129. self.current_index = self.playlist_listbox.curselection()[0] if self.playlist_listbox.curselection() else 0
  130. self.play_thread = threading.Thread(target=self.play_audio, daemon=True)
  131. self.play_thread.start()
  132. self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green")
  133. def play_audio(self):
  134. while True:
  135. if self.stop_event.is_set():
  136. break
  137. if self.pause_event.is_set():
  138. time.sleep(0.1)
  139. continue
  140. # 检查当前索引是否有效
  141. if self.current_index >= len(self.playlist):
  142. if self.loop_playback.get(): # 如果勾选了循环播放
  143. self.current_index = 0 # 回到第一首
  144. else:
  145. break # 否则停止播放
  146. file = self.playlist[self.current_index]
  147. self.playlist_listbox.selection_clear(0, tk.END)
  148. self.playlist_listbox.selection_set(self.current_index)
  149. self.playlist_listbox.activate(self.current_index)
  150. self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green")
  151. play_p3_file(file, self.stop_event, self.pause_event)
  152. if self.stop_event.is_set():
  153. break
  154. if not self.loop_playback.get(): # 如果没有勾选循环播放
  155. break # 播放完当前文件后停止
  156. self.current_index += 1
  157. if self.current_index >= len(self.playlist):
  158. if self.loop_playback.get(): # 如果勾选了循环播放
  159. self.current_index = 0 # 回到第一首
  160. self.is_playing = False
  161. self.is_paused = False
  162. self.update_status("播放已停止", "red")
  163. def pause(self):
  164. if self.is_playing:
  165. self.is_paused = not self.is_paused
  166. if self.is_paused:
  167. self.pause_event.set()
  168. self.update_status("播放已暂停", "orange")
  169. else:
  170. self.pause_event.clear()
  171. self.update_status(f"正在播放:{os.path.basename(self.playlist[self.current_index])}", "green")
  172. def stop(self):
  173. if self.is_playing or self.is_paused:
  174. self.is_playing = False
  175. self.is_paused = False
  176. self.stop_event.set()
  177. self.pause_event.clear()
  178. self.update_status("播放已停止", "red")
  179. def remove_files(self):
  180. selected_indices = self.playlist_listbox.curselection()
  181. if not selected_indices:
  182. messagebox.showwarning("警告", "请先选择要移除的文件!")
  183. return
  184. for index in reversed(selected_indices):
  185. self.playlist.pop(index)
  186. self.update_playlist()
  187. if __name__ == "__main__":
  188. root = tk.Tk()
  189. app = P3PlayerApp(root)
  190. root.mainloop()