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': {},
|
'cues': {},
|
||||||
'loop_start': None,
|
'loop_start': None,
|
||||||
'loop_end': None,
|
'loop_end': None,
|
||||||
'loop_active': False
|
'loop_active': False,
|
||||||
|
'repeat': False,
|
||||||
|
'queue': [],
|
||||||
},
|
},
|
||||||
'B': {
|
'B': {
|
||||||
'audio_data': None,
|
'audio_data': None,
|
||||||
|
|
@ -70,7 +72,9 @@ class AudioEngine:
|
||||||
'cues': {},
|
'cues': {},
|
||||||
'loop_start': None,
|
'loop_start': None,
|
||||||
'loop_end': None,
|
'loop_end': None,
|
||||||
'loop_active': False
|
'loop_active': False,
|
||||||
|
'repeat': False,
|
||||||
|
'queue': [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,7 +189,16 @@ class AudioEngine:
|
||||||
|
|
||||||
# Auto-stop at end
|
# Auto-stop at end
|
||||||
if deck['position'] >= len(deck['audio_data']):
|
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
|
output = output * self.master_volume
|
||||||
outdata[:] = output
|
outdata[:] = output
|
||||||
|
|
@ -264,6 +277,39 @@ class AudioEngine:
|
||||||
def set_filter(self, deck_id, filter_type, value):
|
def set_filter(self, deck_id, filter_type, value):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.decks[deck_id]['filters'][filter_type] = value
|
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):
|
class DownloadThread(QThread):
|
||||||
|
|
@ -854,6 +900,11 @@ class DeckWidget(QWidget):
|
||||||
reset_btn.clicked.connect(self.reset_deck)
|
reset_btn.clicked.connect(self.reset_deck)
|
||||||
transport.addWidget(reset_btn)
|
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)
|
layout.addLayout(transport)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
@ -962,11 +1013,51 @@ class DeckWidget(QWidget):
|
||||||
|
|
||||||
print(f"🔄 Deck {self.deck_id} reset to defaults")
|
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):
|
def update_display(self):
|
||||||
deck = self.audio_engine.decks[self.deck_id]
|
deck = self.audio_engine.decks[self.deck_id]
|
||||||
position = self.audio_engine.get_position(self.deck_id)
|
position = self.audio_engine.get_position(self.deck_id)
|
||||||
duration = deck['duration']
|
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_min = int(position // 60)
|
||||||
pos_sec = int(position % 60)
|
pos_sec = int(position % 60)
|
||||||
dur_min = int(duration // 60)
|
dur_min = int(duration // 60)
|
||||||
|
|
@ -1794,14 +1885,23 @@ class TechDJMainWindow(QMainWindow):
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.addWidget(QLabel(f"Load '{track['title']}' to:"))
|
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))
|
btn_a.clicked.connect(lambda: self.load_to_deck('A', track, dialog))
|
||||||
layout.addWidget(btn_a)
|
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))
|
btn_b.clicked.connect(lambda: self.load_to_deck('B', track, dialog))
|
||||||
layout.addWidget(btn_b)
|
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.setLayout(layout)
|
||||||
dialog.exec_()
|
dialog.exec_()
|
||||||
|
|
||||||
|
|
@ -1853,6 +1953,47 @@ class TechDJMainWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(self, "Download Error", "Failed to download track")
|
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):
|
def on_crossfader_change(self, value):
|
||||||
self.audio_engine.set_crossfader(value / 100.0)
|
self.audio_engine.set_crossfader(value / 100.0)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue