Add loop/repeat and queue features for both decks
This commit is contained in:
parent
ee26294106
commit
de973b2a0e
151
techdj_qt.py
151
techdj_qt.py
|
|
@ -54,7 +54,9 @@ class AudioEngine:
|
|||
'cues': {},
|
||||
'loop_start': None,
|
||||
'loop_end': None,
|
||||
'loop_active': False
|
||||
'loop_active': False,
|
||||
'repeat': False,
|
||||
'queue': [],
|
||||
},
|
||||
'B': {
|
||||
'audio_data': None,
|
||||
|
|
@ -70,7 +72,9 @@ class AudioEngine:
|
|||
'cues': {},
|
||||
'loop_start': None,
|
||||
'loop_end': None,
|
||||
'loop_active': False
|
||||
'loop_active': False,
|
||||
'repeat': False,
|
||||
'queue': [],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +189,16 @@ class AudioEngine:
|
|||
|
||||
# Auto-stop at end
|
||||
if deck['position'] >= len(deck['audio_data']):
|
||||
deck['playing'] = False
|
||||
if deck['repeat']:
|
||||
# Loop current track
|
||||
deck['position'] = 0
|
||||
elif len(deck['queue']) > 0:
|
||||
# Mark that we need to load next track
|
||||
# Can't load here (wrong thread), UI will handle it
|
||||
deck['playing'] = False
|
||||
deck['needs_next_track'] = True
|
||||
else:
|
||||
deck['playing'] = False
|
||||
|
||||
output = output * self.master_volume
|
||||
outdata[:] = output
|
||||
|
|
@ -264,6 +277,39 @@ class AudioEngine:
|
|||
def set_filter(self, deck_id, filter_type, value):
|
||||
with self.lock:
|
||||
self.decks[deck_id]['filters'][filter_type] = value
|
||||
|
||||
def set_repeat(self, deck_id, enabled):
|
||||
"""Toggle repeat/loop for a deck"""
|
||||
with self.lock:
|
||||
self.decks[deck_id]['repeat'] = enabled
|
||||
|
||||
def add_to_queue(self, deck_id, filepath):
|
||||
"""Add track to deck's queue"""
|
||||
with self.lock:
|
||||
self.decks[deck_id]['queue'].append(filepath)
|
||||
|
||||
def remove_from_queue(self, deck_id, index):
|
||||
"""Remove track from queue by index"""
|
||||
with self.lock:
|
||||
if 0 <= index < len(self.decks[deck_id]['queue']):
|
||||
self.decks[deck_id]['queue'].pop(index)
|
||||
|
||||
def clear_queue(self, deck_id):
|
||||
"""Clear all tracks from queue"""
|
||||
with self.lock:
|
||||
self.decks[deck_id]['queue'].clear()
|
||||
|
||||
def get_queue(self, deck_id):
|
||||
"""Get current queue (returns a copy)"""
|
||||
with self.lock:
|
||||
return list(self.decks[deck_id]['queue'])
|
||||
|
||||
def pop_next_from_queue(self, deck_id):
|
||||
"""Get and remove next track from queue"""
|
||||
with self.lock:
|
||||
if len(self.decks[deck_id]['queue']) > 0:
|
||||
return self.decks[deck_id]['queue'].pop(0)
|
||||
return None
|
||||
|
||||
|
||||
class DownloadThread(QThread):
|
||||
|
|
@ -854,6 +900,11 @@ class DeckWidget(QWidget):
|
|||
reset_btn.clicked.connect(self.reset_deck)
|
||||
transport.addWidget(reset_btn)
|
||||
|
||||
self.loop_btn = NeonButton("🔁 LOOP")
|
||||
self.loop_btn.setCheckable(True)
|
||||
self.loop_btn.clicked.connect(self.toggle_loop)
|
||||
transport.addWidget(self.loop_btn)
|
||||
|
||||
layout.addLayout(transport)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
|
@ -962,11 +1013,51 @@ class DeckWidget(QWidget):
|
|||
|
||||
print(f"🔄 Deck {self.deck_id} reset to defaults")
|
||||
|
||||
def toggle_loop(self):
|
||||
"""Toggle loop/repeat for this deck"""
|
||||
is_looping = self.loop_btn.isChecked()
|
||||
self.audio_engine.set_repeat(self.deck_id, is_looping)
|
||||
|
||||
if is_looping:
|
||||
self.loop_btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background: rgba({self.color.red()}, {self.color.green()}, {self.color.blue()}, 0.3);
|
||||
border: 2px solid rgb({self.color.red()}, {self.color.green()}, {self.color.blue()});
|
||||
color: rgb({self.color.red()}, {self.color.green()}, {self.color.blue()});
|
||||
font-family: 'Orbitron';
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 6px;
|
||||
}}
|
||||
""")
|
||||
print(f"🔁 Deck {self.deck_id} loop enabled")
|
||||
else:
|
||||
self.loop_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid #666;
|
||||
color: #888;
|
||||
font-family: 'Orbitron';
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
print(f"⏹️ Deck {self.deck_id} loop disabled")
|
||||
|
||||
def update_display(self):
|
||||
deck = self.audio_engine.decks[self.deck_id]
|
||||
position = self.audio_engine.get_position(self.deck_id)
|
||||
duration = deck['duration']
|
||||
|
||||
# Check if we need to load next track from queue
|
||||
if deck.get('needs_next_track', False):
|
||||
deck['needs_next_track'] = False
|
||||
next_track = self.audio_engine.pop_next_from_queue(self.deck_id)
|
||||
if next_track:
|
||||
print(f"📋 Auto-loading next track from queue: {os.path.basename(next_track)}")
|
||||
self.load_track(next_track)
|
||||
self.play()
|
||||
|
||||
pos_min = int(position // 60)
|
||||
pos_sec = int(position % 60)
|
||||
dur_min = int(duration // 60)
|
||||
|
|
@ -1794,14 +1885,23 @@ class TechDJMainWindow(QMainWindow):
|
|||
layout = QVBoxLayout()
|
||||
layout.addWidget(QLabel(f"Load '{track['title']}' to:"))
|
||||
|
||||
btn_a = NeonButton(f"Deck A", PRIMARY_CYAN)
|
||||
btn_a = NeonButton(f"▶ Play on Deck A", PRIMARY_CYAN)
|
||||
btn_a.clicked.connect(lambda: self.load_to_deck('A', track, dialog))
|
||||
layout.addWidget(btn_a)
|
||||
|
||||
btn_b = NeonButton(f"Deck B", SECONDARY_MAGENTA)
|
||||
btn_b = NeonButton(f"▶ Play on Deck B", SECONDARY_MAGENTA)
|
||||
btn_b.clicked.connect(lambda: self.load_to_deck('B', track, dialog))
|
||||
layout.addWidget(btn_b)
|
||||
|
||||
# Add to queue buttons
|
||||
queue_a = NeonButton(f"📋 Add to Queue A", PRIMARY_CYAN)
|
||||
queue_a.clicked.connect(lambda: self.add_to_queue('A', track, dialog))
|
||||
layout.addWidget(queue_a)
|
||||
|
||||
queue_b = NeonButton(f"📋 Add to Queue B", SECONDARY_MAGENTA)
|
||||
queue_b.clicked.connect(lambda: self.add_to_queue('B', track, dialog))
|
||||
layout.addWidget(queue_b)
|
||||
|
||||
dialog.setLayout(layout)
|
||||
dialog.exec_()
|
||||
|
||||
|
|
@ -1853,6 +1953,47 @@ class TechDJMainWindow(QMainWindow):
|
|||
else:
|
||||
QMessageBox.warning(self, "Download Error", "Failed to download track")
|
||||
|
||||
def add_to_queue(self, deck_id, track, dialog=None):
|
||||
"""Add track to deck's queue"""
|
||||
if dialog:
|
||||
dialog.accept()
|
||||
|
||||
# Determine file path
|
||||
if self.library_mode == 'local':
|
||||
filepath = track['file']
|
||||
else:
|
||||
filename = track['file'].split('/')[-1]
|
||||
cache_path = self.cache_dir / filename
|
||||
|
||||
if cache_path.exists():
|
||||
filepath = str(cache_path)
|
||||
else:
|
||||
# Download to cache first
|
||||
url = f"{self.server_url}/{track['file']}"
|
||||
print(f"⬇️ Downloading for queue: {filename}")
|
||||
|
||||
thread = DownloadThread(url, str(cache_path))
|
||||
thread.finished.connect(lambda path, success: self.on_queue_download_finished(deck_id, path, success))
|
||||
thread.start()
|
||||
self.download_threads[filename] = thread
|
||||
return
|
||||
|
||||
# Add to queue
|
||||
self.audio_engine.add_to_queue(deck_id, filepath)
|
||||
queue_len = len(self.audio_engine.get_queue(deck_id))
|
||||
print(f"📋 Added to Deck {deck_id} queue: {track['title']} (Queue: {queue_len})")
|
||||
QMessageBox.information(self, "Added to Queue",
|
||||
f"Added '{track['title']}' to Deck {deck_id} queue\n\nQueue length: {queue_len}")
|
||||
|
||||
def on_queue_download_finished(self, deck_id, filepath, success):
|
||||
"""Handle download completion for queued tracks"""
|
||||
if success:
|
||||
self.audio_engine.add_to_queue(deck_id, filepath)
|
||||
queue_len = len(self.audio_engine.get_queue(deck_id))
|
||||
print(f"✅ Downloaded and queued: {os.path.basename(filepath)} (Queue: {queue_len})")
|
||||
else:
|
||||
print(f"❌ Failed to download for queue: {os.path.basename(filepath)}")
|
||||
|
||||
def on_crossfader_change(self, value):
|
||||
self.audio_engine.set_crossfader(value / 100.0)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue