|
|
@@ -5,7 +5,7 @@ Player Panel — UI for playing files with EQ applied.
|
|
|
import os
|
|
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
|
|
|
QPushButton, QLabel, QFileDialog, QProgressBar,
|
|
|
- QListWidget, QSplitter, QMessageBox, QCheckBox)
|
|
|
+ QListWidget, QSplitter, QMessageBox, QCheckBox, QSlider)
|
|
|
from PyQt6.QtCore import Qt, QTimer, QSettings
|
|
|
from core.file_loader import get_audio_duration
|
|
|
|
|
|
@@ -57,8 +57,14 @@ class PlayerPanel(QWidget):
|
|
|
|
|
|
# --- Top: File Info & Load ---
|
|
|
top_row = QHBoxLayout()
|
|
|
+
|
|
|
+ info_layout = QVBoxLayout()
|
|
|
self.file_lbl = QLabel("No file loaded")
|
|
|
self.file_lbl.setStyleSheet("font-weight: bold; font-size: 12pt;")
|
|
|
+ self.format_lbl = QLabel("")
|
|
|
+ self.format_lbl.setStyleSheet("color: #808090; font-size: 10pt;")
|
|
|
+ info_layout.addWidget(self.file_lbl)
|
|
|
+ info_layout.addWidget(self.format_lbl)
|
|
|
|
|
|
load_btn = QPushButton("Load Files...")
|
|
|
load_btn.clicked.connect(self.load_files)
|
|
|
@@ -66,9 +72,13 @@ class PlayerPanel(QWidget):
|
|
|
load_folder_btn = QPushButton("Load Folder...")
|
|
|
load_folder_btn.clicked.connect(self.load_folder)
|
|
|
|
|
|
- top_row.addWidget(self.file_lbl, 1)
|
|
|
+ self.refresh_btn = QPushButton("Refresh Folder")
|
|
|
+ self.refresh_btn.clicked.connect(self.refresh_folder)
|
|
|
+
|
|
|
+ top_row.addLayout(info_layout, 1)
|
|
|
top_row.addWidget(load_btn)
|
|
|
top_row.addWidget(load_folder_btn)
|
|
|
+ top_row.addWidget(self.refresh_btn)
|
|
|
left_layout.addLayout(top_row)
|
|
|
|
|
|
# --- Middle: Waveform ---
|
|
|
@@ -94,13 +104,25 @@ class PlayerPanel(QWidget):
|
|
|
self.stop_btn = QPushButton("■ Stop")
|
|
|
self.stop_btn.clicked.connect(self._on_stop_clicked)
|
|
|
|
|
|
- self.norm_cb = QCheckBox("Normalize Volume")
|
|
|
+ self.norm_cb = QCheckBox("Normalize")
|
|
|
self.norm_cb.setChecked(self.settings.value("normalize_volume", True, type=bool))
|
|
|
self.norm_cb.clicked.connect(self._on_norm_changed)
|
|
|
|
|
|
+ self.speed_slider = QSlider(Qt.Orientation.Horizontal)
|
|
|
+ self.speed_slider.setRange(90, 110)
|
|
|
+ self.speed_slider.setValue(100)
|
|
|
+ self.speed_slider.setFixedWidth(100)
|
|
|
+ self.speed_slider.valueChanged.connect(self._on_speed_changed)
|
|
|
+
|
|
|
+ self.speed_lbl = QLabel("Speed: 1.00x")
|
|
|
+ self.speed_lbl.setFixedWidth(80)
|
|
|
+
|
|
|
btn_layout.addWidget(self.play_btn)
|
|
|
btn_layout.addWidget(self.stop_btn)
|
|
|
btn_layout.addWidget(self.norm_cb)
|
|
|
+ btn_layout.addWidget(QLabel(" | "))
|
|
|
+ btn_layout.addWidget(self.speed_lbl)
|
|
|
+ btn_layout.addWidget(self.speed_slider)
|
|
|
btn_layout.addStretch()
|
|
|
|
|
|
control_row.addLayout(btn_layout, 1)
|
|
|
@@ -173,6 +195,37 @@ class PlayerPanel(QWidget):
|
|
|
self._update_playlist_ui()
|
|
|
self._load_track(self.playlist[self.current_track_index], auto_play=True)
|
|
|
|
|
|
+ def refresh_folder(self):
|
|
|
+ folder_path = self.settings.value("last_folder", "")
|
|
|
+ if not folder_path or not os.path.isdir(folder_path):
|
|
|
+ QMessageBox.information(self, "Info", "No folder currently loaded or folder no longer exists.")
|
|
|
+ return
|
|
|
+
|
|
|
+ supported = ('.wav', '.mp3', '.opus', '.flac', '.ogg', '.aiff')
|
|
|
+ files = []
|
|
|
+ for f in os.listdir(folder_path):
|
|
|
+ if f.lower().endswith(supported):
|
|
|
+ files.append(os.path.join(folder_path, f))
|
|
|
+
|
|
|
+ if not files:
|
|
|
+ QMessageBox.information(self, "Info", "No supported audio files found in folder.")
|
|
|
+ return
|
|
|
+
|
|
|
+ new_playlist = sorted(files)
|
|
|
+
|
|
|
+ current_file = None
|
|
|
+ if self.playlist and 0 <= self.current_track_index < len(self.playlist):
|
|
|
+ current_file = self.playlist[self.current_track_index]
|
|
|
+
|
|
|
+ self.playlist = new_playlist
|
|
|
+
|
|
|
+ if current_file in self.playlist:
|
|
|
+ self.current_track_index = self.playlist.index(current_file)
|
|
|
+ else:
|
|
|
+ self.current_track_index = -1
|
|
|
+
|
|
|
+ self._update_playlist_ui()
|
|
|
+
|
|
|
def _update_playlist_ui(self):
|
|
|
self.playlist_widget.clear()
|
|
|
total_duration = 0.0
|
|
|
@@ -181,11 +234,21 @@ class PlayerPanel(QWidget):
|
|
|
self.playlist_widget.addItem(os.path.basename(f))
|
|
|
total_duration += get_audio_duration(f)
|
|
|
|
|
|
- dur_m = int(total_duration // 60)
|
|
|
+ total_duration /= self.player.playback_speed
|
|
|
+
|
|
|
+ dur_h = int(total_duration // 3600)
|
|
|
+ dur_m = int((total_duration % 3600) // 60)
|
|
|
dur_s = int(total_duration % 60)
|
|
|
- self.playlist_lbl.setText(f"Playlist (Total: {dur_m:02d}:{dur_s:02d})")
|
|
|
|
|
|
- if self.playlist and self.current_track_index >= 0:
|
|
|
+ if dur_h > 0:
|
|
|
+ time_str = f"{dur_h:02d}:{dur_m:02d}:{dur_s:02d}"
|
|
|
+ else:
|
|
|
+ time_str = f"{dur_m:02d}:{dur_s:02d}"
|
|
|
+
|
|
|
+ self.playlist_lbl.setText(f"Playlist - Total Time: {time_str}")
|
|
|
+ self.playlist_lbl.setStyleSheet("font-weight: bold; color: #4CAF50; font-size: 11pt;")
|
|
|
+
|
|
|
+ if self.playlist and 0 <= self.current_track_index < len(self.playlist):
|
|
|
self.playlist_widget.setCurrentRow(self.current_track_index)
|
|
|
|
|
|
def _on_playlist_double_click(self, item):
|
|
|
@@ -194,6 +257,15 @@ class PlayerPanel(QWidget):
|
|
|
self.current_track_index = idx
|
|
|
self._load_track(self.playlist[self.current_track_index], auto_play=True)
|
|
|
|
|
|
+ def _on_speed_changed(self, value):
|
|
|
+ speed = value / 100.0
|
|
|
+ self.speed_lbl.setText(f"Speed: {speed:.2f}x")
|
|
|
+ self.player.set_speed(speed)
|
|
|
+ self._update_playlist_ui()
|
|
|
+ if self.player.audio_data:
|
|
|
+ pos_sec = self.player.current_frame / self.player.audio_data.sample_rate
|
|
|
+ self._update_time_display(pos_sec)
|
|
|
+
|
|
|
def _on_norm_changed(self):
|
|
|
self.settings.setValue("normalize_volume", self.norm_cb.isChecked())
|
|
|
|
|
|
@@ -206,6 +278,12 @@ class PlayerPanel(QWidget):
|
|
|
prefix = f"[{self.current_track_index + 1}/{len(self.playlist)}] " if self.playlist else ""
|
|
|
self.file_lbl.setText(f"Loaded: {prefix}{filename}")
|
|
|
|
|
|
+ audio = self.player.audio_data
|
|
|
+ if audio:
|
|
|
+ ch_str = "Stereo" if audio.channels == 2 else "Mono"
|
|
|
+ fmt = audio.format_name or "Audio"
|
|
|
+ self.format_lbl.setText(f"{audio.sample_rate} Hz | {ch_str} | {fmt}")
|
|
|
+
|
|
|
self.waveform.load_audio(self.player.audio_data)
|
|
|
self._update_time_display(0)
|
|
|
self.spectrum.set_sample_rate(self.player.device_manager.sample_rate)
|
|
|
@@ -256,12 +334,15 @@ class PlayerPanel(QWidget):
|
|
|
self.waveform.set_position(pos_sec)
|
|
|
self._update_time_display(pos_sec)
|
|
|
|
|
|
- # If playing, we can pull the latest chunk from the player for the spectrum.
|
|
|
- # In a real app we'd pass this via queue, but this is a simple approximation.
|
|
|
- if self.player.is_playing and self.player.current_frame > 1024:
|
|
|
- idx = self.player.current_frame
|
|
|
- chunk = self.player.audio_data.samples[max(0, idx-1024):idx]
|
|
|
- self.spectrum.add_audio_data(chunk)
|
|
|
+ # If playing, pull the latest chunks for the spectrum.
|
|
|
+ if self.player.is_playing:
|
|
|
+ raw = self.player._last_raw_chunk
|
|
|
+ processed = self.player._last_processed_chunk
|
|
|
+ if raw is not None and processed is not None:
|
|
|
+ self.spectrum.add_audio_data(raw, processed)
|
|
|
+ # Consume to avoid adding the same chunk multiple times
|
|
|
+ self.player._last_raw_chunk = None
|
|
|
+ self.player._last_processed_chunk = None
|
|
|
|
|
|
# Update VU meters
|
|
|
l_db, r_db = self.player.vu_levels
|
|
|
@@ -277,12 +358,14 @@ class PlayerPanel(QWidget):
|
|
|
if not self.player.audio_data:
|
|
|
return
|
|
|
|
|
|
- dur_sec = self.player.audio_data.duration
|
|
|
+ speed = self.player.playback_speed
|
|
|
+ pos_sec_actual = pos_sec / speed
|
|
|
+ dur_sec_actual = self.player.audio_data.duration / speed
|
|
|
|
|
|
- pos_m = int(pos_sec // 60)
|
|
|
- pos_s = pos_sec % 60
|
|
|
+ pos_m = int(pos_sec_actual // 60)
|
|
|
+ pos_s = pos_sec_actual % 60
|
|
|
|
|
|
- dur_m = int(dur_sec // 60)
|
|
|
- dur_s = dur_sec % 60
|
|
|
+ dur_m = int(dur_sec_actual // 60)
|
|
|
+ dur_s = dur_sec_actual % 60
|
|
|
|
|
|
self.time_lbl.setText(f"{pos_m:02d}:{pos_s:04.1f} / {dur_m:02d}:{dur_s:04.1f}")
|