Mobile UX improvements: Remove emojis, convert to British English, fix queue navigation and floating buttons

- Removed all emojis from UI (replaced with text labels)
- Converted all American English to British English (initialise, optimise, visualiser)
- Fixed mobile queue section visibility in portrait/landscape modes
- Added proper CSS rules for show-queue-A and show-queue-B classes
- Repositioned floating action buttons to prevent overlap with mobile tabs
- Added responsive button sizing for mobile (50px) and small screens (45px)
- Stacked buttons vertically on screens ≤480px
- Disabled all body::before border effects that were blocking UI
- Fixed crossfader width: 70% in portrait, 100% in landscape
- Removed unnecessary .md files (COMPARISON.md, PYQT5_FEATURES.md, QUICKSTART.md, README_PYQT5.md)
- Updated README.md with British English and removed emojis
This commit is contained in:
ComputerTech 2026-02-02 02:37:56 +00:00
parent c2085291c0
commit 6246b26925
8 changed files with 328 additions and 973 deletions

View File

@ -1,190 +0,0 @@
# TechDJ: Web vs Native Comparison
## Memory Usage Analysis
### Chrome Web Panel (~400MB)
```
Chrome Process Breakdown:
├── Browser Process: ~100MB
├── Renderer Process: ~150MB
│ ├── V8 JavaScript Engine: ~50MB
│ ├── Blink Rendering: ~40MB
│ ├── DOM + CSS: ~30MB
│ └── Web Audio Buffers: ~30MB
├── GPU Process: ~80MB
├── Audio Buffers (2 decks): ~100MB
│ └── Decoded PCM audio in memory
└── Network/Extensions: ~70MB
```
### PyQt5 Native App (~120-150MB)
```
PyQt5 Process Breakdown:
├── Python Runtime: ~30MB
├── PyQt5 Framework: ~40MB
├── Audio Engine (sounddevice): ~20MB
├── Cached Audio (streaming): ~30MB
│ └── Only active chunks in memory
├── UI Rendering: ~15MB
└── Libraries (numpy, etc.): ~15MB
```
## Performance Comparison
| Metric | Chrome Web | PyQt5 Native | Improvement |
|--------|------------|--------------|-------------|
| **RAM Usage** | ~400MB | ~120-150MB | **62% less** |
| **CPU Usage (idle)** | ~5-8% | ~1-2% | **75% less** |
| **CPU Usage (mixing)** | ~15-25% | ~8-12% | **50% less** |
| **Audio Latency** | 50-100ms | <10ms | **90% better** |
| **Startup Time** | 3-5s | 1-2s | **60% faster** |
| **Battery Impact** | High | Low | **~40% longer** |
## Feature Parity
| Feature | Web Panel | PyQt5 Native |
|---------|-----------|--------------|
| Dual Decks | ✅ | ✅ |
| Crossfader | ✅ | ✅ |
| Waveform Display | ✅ | ✅ |
| Hot Cues | ✅ | ✅ |
| Speed/Pitch Control | ✅ | ✅ |
| Volume Control | ✅ | ✅ |
| EQ (3-band) | ✅ | 🚧 (planned) |
| Filters (LP/HP) | ✅ | 🚧 (planned) |
| Loop Controls | ✅ | 🚧 (planned) |
| Auto-Loop | ✅ | 🚧 (planned) |
| Library Search | ✅ | ✅ |
| Mobile Support | ✅ | ❌ |
| Remote Access | ✅ | ❌ |
| Offline Mode | ❌ | ✅ (cached) |
| Broadcast Streaming | ✅ | 🚧 (planned) |
## Use Case Recommendations
### Use Chrome Web Panel When:
- 🌐 DJing remotely (different computer)
- 📱 Using mobile device (phone/tablet)
- 👥 Multiple DJs need access
- 🎧 Streaming to web listeners
- 💻 No installation permissions
- 🔄 Frequent updates/testing
### Use PyQt5 Native When:
- 💻 DJing on your laptop
- 🔋 Battery life is important
- ⚡ Low latency is critical
- 💾 RAM is limited (<8GB)
- 🎵 Offline DJing (cached songs)
- 🎮 Using MIDI controllers (future)
## Technical Details
### Audio Processing Architecture
**Web Panel (Web Audio API):**
```javascript
// Loads entire song into memory
audioCtx.decodeAudioData(arrayBuffer, (buffer) => {
// buffer contains full PCM data (~50MB for 5min song)
deck.localBuffer = buffer;
});
// Creates new source for each play
const source = audioCtx.createBufferSource();
source.buffer = deck.localBuffer; // Full song in RAM
source.connect(filters);
```
**PyQt5 Native (sounddevice):**
```python
# Streams audio in small chunks
def audio_callback(outdata, frames, time_info, status):
# Only processes 2048 frames at a time (~46ms)
chunk = audio_data[position:position+frames]
outdata[:] = chunk # Minimal memory usage
position += frames
```
### Memory Efficiency Example
**5-minute song (320kbps MP3):**
- File size: ~12MB (compressed)
- Chrome Web Audio: ~50MB (decoded PCM in RAM)
- PyQt5 Native: ~0.1MB (streaming chunks)
**With 2 decks loaded:**
- Chrome: ~100MB just for audio buffers
- PyQt5: ~0.2MB for active chunks
## Hybrid Setup (Best of Both Worlds)
You can run **both** simultaneously:
```
┌─────────────────────┐
│ PyQt5 Native App │ (Your laptop - low RAM)
│ (Local DJing) │
└─────────────────────┘
│ Broadcasts to
┌─────────────────────┐
│ Flask Server │ (Relay server)
│ (5000/5001) │
└─────────────────────┘
│ Streams to
┌─────────────────────┐
│ Web Listeners │ (Audience - any device)
│ (Mobile/Browser) │
└─────────────────────┘
```
**Benefits:**
- DJ uses native app (low memory, low latency)
- Listeners use web interface (easy access)
- Best performance for both use cases
## Installation Size Comparison
| Component | Size |
|-----------|------|
| Chrome Browser | ~200MB |
| PyQt5 + Dependencies | ~150MB |
| **Total for Web** | ~200MB |
| **Total for Native** | ~150MB |
## Conclusion
**For your laptop:** PyQt5 Native is the clear winner
- ✅ 62% less RAM usage (400MB → 150MB)
- ✅ 75% less CPU usage
- ✅ 90% lower audio latency
- ✅ Better battery life
- ✅ Works offline with cached songs
**Keep the web panel for:**
- Mobile DJing
- Remote sessions
- Listener streaming
- Quick access without installation
## Next Steps
1. **Try the PyQt5 app:**
```bash
./launch_qt.sh
```
2. **Compare memory usage:**
```bash
# Chrome
ps aux | grep chrome | awk '{sum+=$6} END {print sum/1024 " MB"}'
# PyQt5
ps aux | grep techdj_qt | awk '{print $6/1024 " MB"}'
```
3. **Test both side-by-side** and see the difference!

View File

@ -1,233 +0,0 @@
# TechDJ PyQt5 - Complete Feature List
## ✨ Now a Perfect Replica of the Web DJ Panel!
### 🎨 Visual Features (Matching Web Panel)
#### **Main Layout**
- ✅ 3-column grid (Library | Deck A | Deck B)
- ✅ Crossfader spanning both decks at bottom
- ✅ Exact color scheme (#0a0a12 background, #00f3ff cyan, #bc13fe magenta)
- ✅ Neon glow effects
- ✅ Orbitron and Rajdhani fonts
#### **Deck Components**
- ✅ Animated vinyl disks (rotating when playing)
- ✅ Waveform display with playhead
- ✅ Hot cues (4 per deck) with glow
- ✅ Loop controls (IN/OUT/EXIT)
- ✅ Volume faders
- ✅ 3-band EQ (HI/MID/LO) with vertical sliders
- ✅ Filters (Low-pass/High-pass)
- ✅ Speed/pitch control with bend buttons
- ✅ Transport buttons (PLAY/PAUSE/SYNC/RESET)
- ✅ Time display with current/total time
- ✅ Track name display
#### **Library**
- ✅ Cyan border and glow
- ✅ Search/filter box
- ✅ Track list with hover effects
- ✅ Refresh button
- ✅ Double-click to load
#### **Crossfader**
- ✅ Gradient (cyan → gray → magenta)
- ✅ Large handle
- ✅ "A" and "B" labels
- ✅ Metallic styling
---
### 🆕 NEW: Floating Action Buttons (Bottom-Right)
Just like the web panel, now has 4 floating buttons:
1. **📡 Streaming Button**
- Opens streaming panel
- Magenta neon glow
- Tooltip: "Live Streaming"
2. **⚙️ Settings Button**
- Opens settings panel
- Magenta neon glow
- Tooltip: "Settings"
3. **📁 Upload Button**
- Upload MP3 files to server
- File dialog integration
- Tooltip: "Upload MP3"
4. **⌨️ Keyboard Button**
- Keyboard shortcuts reference
- Tooltip: "Keyboard Shortcuts"
---
### 🆕 NEW: Streaming Panel
**Features:**
- ✅ Start/Stop broadcast button (red → green when live)
- ✅ Broadcast status indicator
- ✅ Listener count display (👂 with big number)
- ✅ Stream URL with copy button
- ✅ Auto-start on play checkbox
- ✅ Quality selector (128k/96k/64k/48k/32k)
- ✅ Connection to Flask server via Socket.IO
- ✅ Cyan border matching web panel
**Functionality:**
- Connects to Flask server on port 5000
- Sends broadcast start/stop commands
- Configurable bitrate
- Real-time listener count (when implemented)
---
### 🆕 NEW: Settings Panel
**Features:**
- ✅ Repeat Deck A/B checkboxes
- ✅ Auto-Crossfade checkbox
- ✅ Shuffle Library checkbox
- ✅ Quantize checkbox
- ✅ Auto-play next checkbox
- ✅ **✨ Glow Deck A (Cyan)** checkbox
- ✅ **✨ Glow Deck B (Magenta)** checkbox
- ✅ **✨ Glow Intensity** slider (1-100)
- ✅ Magenta border matching web panel
**Neon Glow Effects:**
- Toggle cyan glow for Deck A
- Toggle magenta glow for Deck B
- Adjustable intensity (1-100%)
- Real-time visual feedback
- Matches web panel's glow feature exactly!
---
### 🎯 Feature Comparison
| Feature | Web Panel | PyQt5 Native | Status |
|---------|-----------|--------------|--------|
| **UI & Layout** |
| Dual Decks | ✅ | ✅ | ✅ Complete |
| Crossfader | ✅ | ✅ | ✅ Complete |
| Library | ✅ | ✅ | ✅ Complete |
| Waveforms | ✅ | ✅ | ✅ Complete |
| Vinyl Disks | ✅ | ✅ | ✅ Complete |
| Neon Colors | ✅ | ✅ | ✅ Complete |
| **Controls** |
| Hot Cues | ✅ | ✅ | ✅ Complete |
| Loop Controls | ✅ | ✅ | ✅ Complete |
| Volume | ✅ | ✅ | ✅ Complete |
| EQ (3-band) | ✅ | ✅ | ✅ Complete |
| Filters | ✅ | ✅ | ✅ Complete |
| Speed/Pitch | ✅ | ✅ | ✅ Complete |
| **Features** |
| Streaming Panel | ✅ | ✅ | ✅ **NEW!** |
| Settings Panel | ✅ | ✅ | ✅ **NEW!** |
| Glow Effects | ✅ | ✅ | ✅ **NEW!** |
| Broadcast | ✅ | ✅ | ✅ **NEW!** |
| Upload Files | ✅ | ✅ | ✅ **NEW!** |
| Floating Buttons | ✅ | ✅ | ✅ **NEW!** |
| **Performance** |
| RAM Usage | ~400MB | ~150MB | 62% less |
| CPU Usage | High | Low | 75% less |
| Latency | 50-100ms | <10ms | 90% better |
---
### 🎮 How to Use
#### **Open Streaming Panel:**
1. Click 📡 button (bottom-right)
2. Configure quality
3. Click "START BROADCAST"
4. Share URL with listeners
#### **Enable Glow Effects:**
1. Click ⚙️ button (bottom-right)
2. Check "✨ Glow Deck A (Cyan)"
3. Check "✨ Glow Deck B (Magenta)"
4. Adjust intensity slider
5. Watch your decks glow! ✨
#### **Upload Songs:**
1. Click 📁 button (bottom-right)
2. Select MP3 file
3. File uploads to server
4. Library refreshes automatically
---
### 🚀 Launch the App
```bash
./launch_qt.sh
```
Or directly:
```bash
python3 techdj_qt.py
```
---
### 📊 Memory Usage
**Before (Web Panel):**
- Chrome: ~400MB RAM
- Multiple processes
- High CPU usage
**After (PyQt5 Native):**
- PyQt5: ~150MB RAM
- Single process
- Low CPU usage
- **62% memory savings!**
---
### ✨ Visual Highlights
1. **Neon Aesthetic** - Exact cyan/magenta colors from web panel
2. **Animated Vinyl** - Smooth rotation when playing
3. **Glow Effects** - Adjustable intensity, just like web version
4. **Floating Buttons** - Bottom-right corner, matching web layout
5. **Panels** - Slide-out streaming and settings panels
6. **Professional Look** - Pixel-perfect replica of web design
---
### 🎉 What's New in This Update
✅ Added floating action buttons (📡⚙️📁⌨️)
✅ Added streaming panel with broadcast controls
✅ Added settings panel with all options
✅ Added **neon glow effects** (Deck A cyan, Deck B magenta)
✅ Added glow intensity slider
✅ Added file upload functionality
✅ Added Socket.IO integration for broadcasting
✅ Added listener count display
✅ Added stream URL with copy button
✅ Added quality selector
✅ Made it a **perfect visual replica** of the web panel!
---
### 🔮 Coming Soon
- [ ] VU meters with real-time visualization
- [ ] Keyboard shortcuts panel
- [ ] BPM detection and sync
- [ ] Auto-mix functionality
- [ ] Effects (reverb, delay, etc.)
- [ ] Recording/export
- [ ] MIDI controller support
---
**Now you have a complete, pixel-perfect PyQt5 replica of TechDJ with all the features, including broadcast and neon glow effects!** 🎧✨
**Memory usage: ~150MB (vs ~400MB in Chrome) - 62% savings!**

View File

@ -1,216 +0,0 @@
# TechDJ - Quick Start Guide
## 🎧 You Now Have TWO DJ Applications!
### 1⃣ Web DJ Panel (Chrome) - ~400MB RAM
**Best for:** Mobile, remote DJing, streaming to listeners
**Start:**
```bash
python3 server.py
# Open http://localhost:5000 in Chrome
```
### 2⃣ PyQt5 Native App - ~150MB RAM ⚡
**Best for:** Laptop DJing, low memory, better performance
**Start:**
```bash
./launch_qt.sh
```
---
## 📊 Quick Comparison
| Feature | Web Panel | Native App |
|---------|-----------|------------|
| **RAM** | ~400MB | ~150MB (62% less!) |
| **Latency** | 50-100ms | <10ms (90% better!) |
| **Mobile** | ✅ Yes | ❌ No |
| **Offline** | ❌ No | ✅ Yes (cached) |
| **Installation** | None | Requires setup |
---
## 🚀 First Time Setup (Native App)
1. **Install system dependencies:**
```bash
# Ubuntu/Debian
sudo apt-get install portaudio19-dev python3-pyqt5 python3-pip
# Fedora
sudo dnf install portaudio-devel python3-qt5 python3-pip
# Arch
sudo pacman -S portaudio python-pyqt5 python-pip
```
2. **Install Python packages:**
```bash
pip3 install --user -r requirements.txt
```
3. **Launch:**
```bash
./launch_qt.sh
```
---
## 📁 File Structure
```
techdj/
├── server.py # Flask server (required for both)
├── index.html # Web DJ panel
├── script.js # Web DJ logic
├── style.css # Web DJ styling
├── techdj_qt.py # PyQt5 native app ⭐ NEW
├── launch_qt.sh # Easy launcher ⭐ NEW
├── compare_memory.py # Memory comparison tool ⭐ NEW
├── README_PYQT5.md # Native app docs ⭐ NEW
├── COMPARISON.md # Detailed comparison ⭐ NEW
└── music/ # Your MP3 library
```
---
## 🎵 How the Native App Works
1. **Fetches library** from Flask server (`http://localhost:5000/library.json`)
2. **Downloads songs** to local cache (`~/.techdj_cache/`)
3. **Processes audio locally** using sounddevice (efficient!)
4. **Caches songs** for instant loading next time
**Memory savings:**
- Chrome loads entire song into RAM (~50MB per 5min song)
- PyQt5 streams in small chunks (~0.1MB at a time)
- **Result: 99% less audio memory usage!**
---
## 🔧 Troubleshooting
### Native app won't start?
```bash
# Check dependencies
python3 -c "import PyQt5; import sounddevice; import soundfile; print('✅ All good!')"
# Check audio devices
python3 -c "import sounddevice as sd; print(sd.query_devices())"
```
### Can't connect to server?
```bash
# Make sure server is running
curl http://localhost:5000/library.json
# Start server if needed
python3 server.py
```
### Audio crackling/glitches?
- Increase buffer size in `techdj_qt.py` (line 56):
```python
blocksize=4096 # Increase from 2048
```
---
## 📈 Compare Memory Usage
Run both versions and check the difference:
```bash
python3 compare_memory.py
```
Example output:
```
TechDJ Memory Usage Comparison
============================================================
🌐 Chrome (Web Panel):
Total Memory: 387.2 MB
Processes: 8
💻 PyQt5 Native App:
Total Memory: 142.5 MB
Processes: 1
============================================================
📊 Comparison:
Memory Saved: 244.7 MB (63.2%)
Chrome: ████████████████████████████████████████ 387MB
PyQt5: ███████████████ 143MB
✅ PyQt5 uses 63% less memory!
============================================================
```
---
## 🎯 Recommended Setup
**For best results, use BOTH:**
1. **DJ on your laptop** with PyQt5 native app (low memory, fast)
2. **Keep web panel** for mobile/remote access
3. **Listeners** connect to web interface (port 5001)
```
You (Laptop) Server Audience
┌──────────┐ ┌──────┐ ┌──────────┐
│ PyQt5 │────────▶│Flask │────────▶│ Web │
│ Native │ control │5000/ │ stream │Listeners │
│ ~150MB │ │5001 │ │ Mobile │
└──────────┘ └──────┘ └──────────┘
```
---
## 🎉 What's Next?
### Try it out:
```bash
# Terminal 1: Start server
python3 server.py
# Terminal 2: Launch native app
./launch_qt.sh
# Terminal 3: Compare memory
python3 compare_memory.py
```
### Future enhancements for native app:
- [ ] EQ and filters
- [ ] Loop controls
- [ ] Broadcast to web listeners
- [ ] BPM detection
- [ ] MIDI controller support
- [ ] Effects (reverb, delay)
---
## 📚 Documentation
- **Native App Guide:** `README_PYQT5.md`
- **Detailed Comparison:** `COMPARISON.md`
- **Web Panel:** Original `README.md`
---
## 💡 Tips
1. **Cache management:** Songs download once, then load instantly
2. **Offline DJing:** Works without internet after songs are cached
3. **Battery life:** Native app uses ~40% less battery than Chrome
4. **Latency:** Native app has <10ms latency vs 50-100ms in browser
---
**Enjoy your lightweight, native DJ experience! 🎧⚡**

View File

@ -129,7 +129,7 @@ You should see output like:
### DJ workflow
1. Open the DJ Panel: `http://localhost:5000`
2. Click **INITIALIZE SYSTEM**
2. Click **INITIALISE SYSTEM**
3. Load/play music
- Upload MP3s (folder/upload button)
- Or download from URLs (paste into deck input / download controls)
@ -140,7 +140,7 @@ You should see output like:
TechDJ can relay live streams from other DJ servers:
1. Open the DJ Panel: `http://localhost:5000`
2. Click the streaming panel (📡 LIVE STREAM)
2. Click the streaming panel (LIVE STREAM)
3. In the "Remote Stream Relay" section, paste a remote stream URL (e.g., `http://remote.dj/stream.mp3`)
4. Click **START RELAY**
5. Your listeners will receive the relayed stream
@ -249,7 +249,7 @@ Example Cloudflare page rule:
- Crossfader isnt fully on the silent side
- Volumes arent at 0
- Check `http://<DJ_MACHINE_IP>:5001/stream_debug` and see if `transcoder_bytes_out` increases
### Spectrum visualizer not showing
### Spectrum visualiser not showing
- Ensure the listener page is loaded and audio is enabled.
- Check browser console for errors related to Web Audio API.

View File

@ -1,143 +0,0 @@
# TechDJ PyQt5 - Native DJ Application
A lightweight native alternative to the web-based TechDJ panel, built with PyQt5.
## Features
**Lightweight** - Uses ~120-150MB RAM (vs ~400MB for Chrome)
**Local Audio Processing** - Downloads songs from Flask server and processes locally
**Low Latency** - Instant response to controls, perfect for live DJing
**Full DJ Features**:
- Dual decks with independent playback
- Crossfader mixing
- Waveform display
- Hot cues (4 per deck)
- Speed/pitch control
- Volume control per deck
- Real-time audio visualization
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
Note: On Linux, you may also need to install PortAudio:
```bash
# Ubuntu/Debian
sudo apt-get install portaudio19-dev python3-pyqt5
# Fedora
sudo dnf install portaudio-devel python3-qt5
# Arch
sudo pacman -S portaudio python-pyqt5
```
## Usage
1. Start the Flask server (in one terminal):
```bash
python server.py
```
2. Launch the PyQt5 DJ app (in another terminal):
```bash
python techdj_qt.py
```
## How It Works
1. **Library Management**: Fetches song list from Flask server at `http://localhost:5000/library.json`
2. **Song Download**: Downloads MP3 files to local cache (`~/.techdj_cache/`) on first use
3. **Local Playback**: Uses `sounddevice` for efficient real-time audio processing
4. **Caching**: Songs are cached locally for instant loading on subsequent uses
## Architecture
```
┌─────────────────────┐
│ PyQt5 Native App │ (~150MB RAM)
│ │
│ • Fetches library │
│ • Downloads & caches│
│ • Local audio mix │
│ • Real-time DSP │
└─────────────────────┘
│ HTTP (library + downloads)
┌─────────────────────┐
│ Flask Server │
│ (port 5000/5001) │
│ │
│ • Serves library │
│ • Serves MP3 files │
│ • Handles streaming │
└─────────────────────┘
```
## Memory Comparison
| Version | RAM Usage | CPU Usage | Latency |
|---------|-----------|-----------|---------|
| **Chrome Web Panel** | ~400MB | High | ~50-100ms |
| **PyQt5 Native** | ~120-150MB | Low | <10ms |
## Keyboard Shortcuts
- **Space**: Play/Pause active deck
- **1-4**: Hot cues for Deck A
- **5-8**: Hot cues for Deck B
- **Q/W**: Volume Deck A/B
- **A/S**: Speed Deck A/B
## Cache Management
Songs are cached in `~/.techdj_cache/`. To clear the cache:
```bash
rm -rf ~/.techdj_cache/
```
## Troubleshooting
### Audio Issues
If you get audio errors, check your audio devices:
```python
import sounddevice as sd
print(sd.query_devices())
```
### Connection Issues
Make sure the Flask server is running on port 5000:
```bash
curl http://localhost:5000/library.json
```
## Comparison with Web Panel
| Feature | Web Panel | PyQt5 Native |
|---------|-----------|--------------|
| Memory Usage | ~400MB | ~150MB |
| Installation | None (browser) | Requires Python + deps |
| Mobile Support | Excellent | None |
| Remote Access | Easy | Requires VPN/SSH |
| Audio Latency | Higher | Lower |
| Offline Mode | No | Yes (cached songs) |
| Battery Impact | Higher | Lower |
## Future Enhancements
- [ ] Broadcast to Flask server for web listeners
- [ ] BPM detection
- [ ] Auto-sync between decks
- [ ] Effects (reverb, delay, etc.)
- [ ] Recording/export
- [ ] MIDI controller support
- [ ] Playlist management
## License
Same as TechDJ main project

View File

@ -11,13 +11,13 @@
<body>
<div id="start-overlay">
<h1 class="overlay-title">TECHDJ PROTOCOL</h1>
<button id="start-btn" onclick="initSystem()">INITIALIZE SYSTEM</button>
<button id="start-btn" onclick="initSystem()">INITIALISE SYSTEM</button>
<p style="color:#666; margin-top:20px; font-family:'Rajdhani'">v2.0 // NEON CORE</p>
</div>
<!-- Landscape Orientation Prompt -->
<div class="landscape-prompt" id="landscape-prompt">
<div class="rotate-icon">📱→🔄</div>
<div class="rotate-icon">ROTATE</div>
<h2>OPTIMAL ORIENTATION</h2>
<p>For the best DJ experience, please rotate your device to <strong>landscape mode</strong> (sideways).</p>
<p style="font-size: 0.9rem; color: #888;">Both decks and the crossfader will be visible simultaneously.</p>
@ -33,19 +33,27 @@
<!-- Mobile Tabs -->
<nav class="mobile-tabs">
<button class="tab-btn active" onclick="switchTab('library')">
<span class="tab-icon">📁</span>
<span class="tab-icon"></span>
<span>LIBRARY</span>
</button>
<button class="tab-btn" onclick="switchTab('deck-A')">
<span class="tab-icon">💿</span>
<span class="tab-icon"></span>
<span>DECK A</span>
</button>
<button class="tab-btn" onclick="switchTab('queue-A')">
<span class="tab-icon"></span>
<span>QUEUE A</span>
</button>
<button class="tab-btn" onclick="switchTab('deck-B')">
<span class="tab-icon">💿</span>
<span class="tab-icon"></span>
<span>DECK B</span>
</button>
<button class="tab-btn" onclick="switchTab('queue-B')">
<span class="tab-icon"></span>
<span>QUEUE B</span>
</button>
<button class="tab-btn fullscreen-btn" onclick="toggleFullScreen()" id="fullscreen-toggle">
<span class="tab-icon">📺</span>
<span class="tab-icon"></span>
<span>FULL</span>
</button>
</nav>
@ -53,8 +61,8 @@
<!-- 1. LEFT: LIBRARY -->
<section class="library-section">
<div class="lib-header">
<input type="text" id="lib-search" placeholder="🔍 FILTER LIBRARY..." onkeyup="filterLibrary()">
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">🔄</button>
<input type="text" id="lib-search" placeholder="FILTER LIBRARY..." onkeyup="filterLibrary()">
<button class="refresh-btn" onclick="refreshLibrary()" title="Refresh Library">REFRESH</button>
</div>
@ -175,19 +183,8 @@
<button class="big-btn play-btn" onclick="playDeck('A')">PLAY</button>
<button class="big-btn pause-btn" onclick="pauseDeck('A')">PAUSE</button>
<button class="big-btn sync-btn" onclick="syncDecks('A')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('A')" title="Reset all settings to default">🔄
RESET</button>
</div>
<!-- QUEUE for Deck A -->
<div class="queue-panel" id="queue-panel-A">
<div class="queue-header">
<span class="queue-title">📋 QUEUE A</span>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">🗑️</button>
</div>
<div class="queue-list" id="queue-list-A">
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
</div>
<button class="big-btn reset-btn" onclick="resetDeck('A')"
title="Reset all settings to default">RESET</button>
</div>
</div>
@ -302,22 +299,32 @@
<button class="big-btn play-btn" onclick="playDeck('B')">PLAY</button>
<button class="big-btn pause-btn" onclick="pauseDeck('B')">PAUSE</button>
<button class="big-btn sync-btn" onclick="syncDecks('B')">SYNC</button>
<button class="big-btn reset-btn" onclick="resetDeck('B')" title="Reset all settings to default">🔄
RESET</button>
</div>
<!-- QUEUE for Deck B -->
<div class="queue-panel" id="queue-panel-B">
<div class="queue-header">
<span class="queue-title">📋 QUEUE B</span>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">🗑️</button>
</div>
<div class="queue-list" id="queue-list-B">
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
</div>
<button class="big-btn reset-btn" onclick="resetDeck('B')"
title="Reset all settings to default">RESET</button>
</div>
</div>
<!-- STANDALONE QUEUE SECTIONS FOR MOBILE -->
<section class="queue-section" id="queue-A">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE A</h2>
<button class="queue-clear-btn" onclick="clearQueue('A')" title="Clear queue">Clear All</button>
</div>
<div class="queue-list" id="queue-list-A">
<div class="queue-empty">Drop tracks here or click "Queue to A" in library</div>
</div>
</section>
<section class="queue-section" id="queue-B">
<div class="queue-page-header">
<h2 class="queue-page-title">QUEUE B</h2>
<button class="queue-clear-btn" onclick="clearQueue('B')" title="Clear queue">Clear All</button>
</div>
<div class="queue-list" id="queue-list-B">
<div class="queue-empty">Drop tracks here or click "Queue to B" in library</div>
</div>
</section>
<!-- 4. BOTTOM: CROSSFADER -->
<div class="mixer-section">
<input type="range" class="xfader" id="crossfader" min="0" max="100" value="50"
@ -329,13 +336,13 @@
<!-- Live Streaming Panel -->
<div class="streaming-panel" id="streaming-panel">
<div class="streaming-header">
<span>📡 LIVE STREAM</span>
<button class="close-streaming" onclick="toggleStreamingPanel()"></button>
<span>LIVE STREAM</span>
<button class="close-streaming" onclick="toggleStreamingPanel()">X</button>
</div>
<div class="streaming-content">
<div class="broadcast-controls">
<button class="broadcast-btn" id="broadcast-btn" onclick="toggleBroadcast()">
<span class="broadcast-icon">🔴</span>
<span class="broadcast-icon"></span>
<span id="broadcast-text">START BROADCAST</span>
</button>
<div class="broadcast-status" id="broadcast-status">Offline</div>
@ -343,7 +350,7 @@
<div class="listener-info">
<div class="listener-count">
<span class="count-icon">👂</span>
<span class="count-icon"></span>
<span id="listener-count">0</span>
<span class="count-label">Listeners</span>
</div>
@ -353,7 +360,7 @@
<label>Share this URL:</label>
<div class="url-copy-group">
<input type="text" id="stream-url" readonly value="http://localhost:5001">
<button onclick="copyStreamUrl(event)" class="copy-btn">📋</button>
<button onclick="copyStreamUrl(event)" class="copy-btn">COPY</button>
</div>
</div>
@ -382,7 +389,7 @@
<!-- Listener Mode (Hidden by default) -->
<div class="listener-mode" id="listener-mode" style="display: none;">
<div class="listener-header">
<h1>🎧 TECHDJ LIVE</h1>
<h1>TECHDJ LIVE</h1>
<div class="live-indicator">
<span class="pulse-dot"></span>
<span>LIVE</span>
@ -396,13 +403,13 @@
<!-- Enable Audio Button (shown when autoplay is blocked) -->
<button class="enable-audio-btn" id="enable-audio-btn" style="display: none;"
onclick="enableListenerAudio()">
<span class="audio-icon">🔊</span>
<span class="audio-icon"></span>
<span class="audio-text">ENABLE AUDIO</span>
<span class="audio-subtitle">Click to start listening</span>
</button>
<div class="volume-control">
<label>🔊 Volume</label>
<label>Volume</label>
<input type="range" id="listener-volume" min="0" max="100" value="80"
oninput="setListenerVolume(this.value)">
</div>
@ -415,45 +422,46 @@
<!-- Settings Panel -->
<div class="settings-panel" id="settings-panel">
<div class="settings-header">
<span>⚙️ SETTINGS</span>
<button class="close-settings" onclick="toggleSettings()"></button>
<span>SETTINGS</span>
<button class="close-settings" onclick="toggleSettings()">X</button>
</div>
<div class="settings-content">
<div class="setting-item"><label><input type="checkbox" id="repeat-A"
onchange="toggleRepeat('A', this.checked)">🔁 Repeat Deck A</label></div>
onchange="toggleRepeat('A', this.checked)">Repeat Deck A</label></div>
<div class="setting-item"><label><input type="checkbox" id="repeat-B"
onchange="toggleRepeat('B', this.checked)"><EFBFBD><EFBFBD> Repeat Deck B</label></div>
onchange="toggleRepeat('B', this.checked)">Repeat Deck B</label></div>
<div class="setting-item"><label><input type="checkbox" id="auto-mix"
onchange="toggleAutoMix(this.checked)">🎛️ Auto-Crossfade</label></div>
onchange="toggleAutoMix(this.checked)">Auto-Crossfade</label></div>
<div class="setting-item"><label><input type="checkbox" id="shuffle-mode"
onchange="toggleShuffle(this.checked)">🔀 Shuffle Library</label></div>
onchange="toggleShuffle(this.checked)">Shuffle Library</label></div>
<div class="setting-item"><label><input type="checkbox" id="quantize"
onchange="toggleQuantize(this.checked)">📐 Quantize</label></div>
onchange="toggleQuantize(this.checked)">Quantise</label></div>
<div class="setting-item"><label><input type="checkbox" id="auto-play" checked
onchange="toggleAutoPlay(this.checked)">▶️ Auto-play next</label></div>
onchange="toggleAutoPlay(this.checked)">Auto-play next</label></div>
<div class="setting-item"><label><input type="checkbox" id="glow-A"
onchange="updateManualGlow('A', this.checked)">Glow Deck A (Cyan)</label></div>
onchange="updateManualGlow('A', this.checked)">Glow Deck A (Cyan)</label></div>
<div class="setting-item"><label><input type="checkbox" id="glow-B"
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
onchange="updateManualGlow('B', this.checked)">Glow Deck B (Magenta)</label></div>
<div class="setting-item" style="flex-direction: column; align-items: flex-start;">
<label>Glow Intensity</label>
<label>Glow Intensity</label>
<input type="range" id="glow-intensity" min="1" max="100" value="30" style="width: 100%;"
oninput="updateGlowIntensity(this.value)">
</div>
<div class="setting-item">
<button class="btn-primary" onclick="openKeyboardSettings()"
style="width: 100%; padding: 12px; margin-top: 10px;">
⌨️ CUSTOM KEYBOARD MAPS
CUSTOM KEYBOARD MAPS
</button>
</div>
</div>
</div>
<button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">⌨️</button>
<button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">📡</button>
<button class="upload-btn" onclick="document.getElementById('file-upload').click()" title="Upload MP3">📁</button>
<button class="keyboard-btn" onclick="openKeyboardSettings()" title="Keyboard Shortcuts (H)">KB</button>
<button class="streaming-btn" onclick="toggleStreamingPanel()" title="Live Streaming">STREAM</button>
<button class="upload-btn" onclick="document.getElementById('file-upload').click()"
title="Upload MP3">UPLOAD</button>
<input type="file" id="file-upload" accept="audio/mp3,audio/mpeg" multiple style="display:none"
onchange="handleFileUpload(event)">
<button class="settings-btn" onclick="toggleSettings()">⚙️</button>
<button class="settings-btn" onclick="toggleSettings()">SET</button>
</body>
</html>

130
script.js
View File

@ -253,7 +253,7 @@ function initSystem() {
});
});
// Initialize mobile view
// Initialise mobile view
if (window.innerWidth <= 1024) {
switchTab('library');
}
@ -290,7 +290,7 @@ function animateVUMeters() {
const barCount = 32;
const barWidth = width / barCount;
// Initialize smoothed values if needed
// Initialise smoothed values if needed
if (!vuMeterState[id].smoothedValues.length) {
vuMeterState[id].smoothedValues = new Array(barCount).fill(0);
vuMeterState[id].peakValues = new Array(barCount).fill(0);
@ -355,10 +355,10 @@ function toggleDeck(id) {
function switchTab(tabId) {
const container = document.querySelector('.app-container');
const buttons = document.querySelectorAll('.tab-btn');
const sections = document.querySelectorAll('.library-section, .deck');
const sections = document.querySelectorAll('.library-section, .deck, .queue-section');
// Remove all tab and active classes
container.classList.remove('show-library', 'show-deck-A', 'show-deck-B');
container.classList.remove('show-library', 'show-deck-A', 'show-deck-B', 'show-queue-A', 'show-queue-B');
buttons.forEach(btn => btn.classList.remove('active'));
sections.forEach(sec => sec.classList.remove('active'));
@ -458,7 +458,7 @@ function handleSwipe() {
}
}
// Waveform Generation (Optimized for Speed)
// Waveform Generation (Optimised for Speed)
function generateWaveformData(buffer) {
const rawData = buffer.getChannelData(0);
const samples = 1000;
@ -542,7 +542,7 @@ function drawWaveform(id) {
}
}
// BPM Detection (Optimized: Only check middle 60 seconds for speed)
// BPM Detection (Optimised: Only check middle 60 seconds for speed)
function detectBPM(buffer) {
const sampleRate = buffer.sampleRate;
const duration = buffer.duration;
@ -702,7 +702,7 @@ function pauseDeck(id) {
// Browser-side audio mode (original code)
if (decks[id].type === 'local' && decks[id].localSource && decks[id].playing) {
if (!audioCtx) {
console.warn(`[Deck ${id}] Cannot calculate pause position - audioCtx not initialized`);
console.warn(`[Deck ${id}] Cannot calculate pause position - audioCtx not initialised`);
decks[id].playing = false;
} else {
const playbackRate = decks[id].localSource.playbackRate.value;
@ -1174,13 +1174,13 @@ function renderLibrary(songs) {
// QUEUE buttons
const queueA = document.createElement('button');
queueA.className = 'load-btn queue-btn-a';
queueA.textContent = '📋 Q-A';
queueA.textContent = 'Q-A';
queueA.title = 'Add to Queue A';
queueA.addEventListener('click', () => addToQueue('A', t.file, t.title));
const queueB = document.createElement('button');
queueB.className = 'load-btn queue-btn-b';
queueB.textContent = '📋 Q-B';
queueB.textContent = 'Q-B';
queueB.title = 'Add to Queue B';
queueB.addEventListener('click', () => addToQueue('B', t.file, t.title));
@ -1256,7 +1256,7 @@ async function loadFromServer(id, url, title) {
if (!socket) initSocket();
socket.emit('audio_load_track', { deck: id, filename: filename });
console.log(`[Deck ${id}] 📡 Load command sent to server: ${filename}`);
console.log(`[Deck ${id}] Load command sent to server: ${filename}`);
// We DON'T return here anymore. We continue below to load for the UI.
}
@ -1318,7 +1318,7 @@ async function loadFromServer(id, url, title) {
// AUTO-RESUME for broadcast continuity
if (wasPlaying && wasBroadcasting) {
console.log(`[Deck ${id}] 🎵 Auto-resuming playback to maintain broadcast stream`);
console.log(`[Deck ${id}] Auto-resuming playback to maintain broadcast stream`);
// Small delay to ensure buffer is fully ready
setTimeout(() => {
playDeck(id);
@ -1378,7 +1378,7 @@ async function handleFileUpload(event) {
const files = event.target.files;
if (!files || files.length === 0) return;
console.log(`📁 Uploading ${files.length} file(s)...`);
console.log(`Uploading ${files.length} file(s)...`);
for (let file of files) {
if (!file.type.match('audio/mpeg') && !file.name.endsWith('.mp3')) {
@ -1410,7 +1410,7 @@ async function handleFileUpload(event) {
}
// Refresh library
console.log('🔄 Refreshing library...');
console.log('Refreshing library...');
await loadLibrary();
alert(`${files.length} file(s) uploaded successfully!`);
@ -1461,7 +1461,7 @@ window.addEventListener('DOMContentLoaded', () => {
if (prompt) prompt.classList.add('dismissed');
}
// Initialize glow intensity
// Initialise glow intensity
updateGlowIntensity(settings.glowIntensity);
const glowAToggle = document.getElementById('glow-A');
if (glowAToggle) glowAToggle.checked = settings.glowA;
@ -1570,7 +1570,7 @@ function getMp3FallbackUrl() {
return `${window.location.origin}/stream.mp3`;
}
// Initialize SocketIO connection
// Initialise SocketIO connection
function initSocket() {
if (socket) return socket;
@ -1623,7 +1623,7 @@ function initSocket() {
});
socket.on('broadcast_started', () => {
console.log('🎙️ Broadcast started notification received');
console.log('Broadcast started notification received');
// Update relay UI if it's a relay
const relayStatus = document.getElementById('relay-status');
if (relayStatus && relayStatus.textContent.includes('Connecting')) {
@ -1645,12 +1645,12 @@ function initSocket() {
});
socket.on('deck_status', (data) => {
console.log(`📡 Server: Deck ${data.deck_id} status update:`, data);
console.log(`Server: Deck ${data.deck_id} status update:`, data);
// This is handled by a single status update too, but helpful for immediate feedback
});
socket.on('error', (data) => {
console.error('📡 Server error:', data.message);
console.error('Server error:', data.message);
alert(`SERVER ERROR: ${data.message}`);
// Reset relay UI on error
document.getElementById('start-relay-btn').style.display = 'inline-block';
@ -1681,7 +1681,7 @@ function updateUIFromMixerStatus(status) {
// Update loaded track if changed
if (deckStatus.filename && (!decks[id].currentFile || decks[id].currentFile !== deckStatus.filename)) {
console.log(`📡 Server synced: Deck ${id} is playing ${deckStatus.filename}`);
console.log(`Server synced: Deck ${id} is playing ${deckStatus.filename}`);
decks[id].currentFile = deckStatus.filename;
decks[id].duration = deckStatus.duration;
@ -1725,7 +1725,7 @@ function toggleStreamingPanel() {
const panel = document.getElementById('streaming-panel');
panel.classList.toggle('active');
// Initialize socket when panel is opened
// Initialise socket when panel is opened
if (panel.classList.contains('active') && !socket) {
initSocket();
}
@ -1734,7 +1734,7 @@ function toggleStreamingPanel() {
// Toggle broadcast
function toggleBroadcast() {
if (!audioCtx) {
alert('Please initialize the system first (click INITIALIZE SYSTEM)');
alert('Please initialise the system first (click INITIALIZE SYSTEM)');
return;
}
@ -1751,10 +1751,10 @@ function toggleBroadcast() {
// Start broadcasting
function startBroadcast() {
try {
console.log('🎙️ Starting broadcast...');
console.log('Starting broadcast...');
if (!audioCtx) {
alert('Please initialize the system first!');
alert('Please initialise the system first!');
return;
}
@ -1779,7 +1779,7 @@ function startBroadcast() {
// Check if any audio is playing
const anyPlaying = decks.A.playing || decks.B.playing;
if (!anyPlaying) {
console.warn('⚠️ WARNING: No decks are currently playing! Start playing a track for audio to stream.');
console.warn('WARNING: No decks are currently playing! Start playing a track for audio to stream.');
}
// Create MediaStreamDestination to capture audio output
@ -1820,7 +1820,7 @@ function startBroadcast() {
// Get selected quality from dropdown
const qualitySelect = document.getElementById('stream-quality');
const selectedBitrate = parseInt(qualitySelect.value) * 1000; // Convert kbps to bps
console.log(`🎚️ Starting broadcast at ${qualitySelect.value}kbps`);
console.log(`Starting broadcast at ${qualitySelect.value}kbps`);
const preferredTypes = [
// Prefer MP4/AAC when available (broad device support)
@ -1843,7 +1843,7 @@ function startBroadcast() {
}
currentStreamMimeType = chosenType;
console.log(`🎛️ Using broadcast mimeType: ${currentStreamMimeType}`);
console.log(`Using broadcast mimeType: ${currentStreamMimeType}`);
mediaRecorder = new MediaRecorder(stream, {
mimeType: currentStreamMimeType,
@ -1861,7 +1861,7 @@ function startBroadcast() {
// Warn if chunks are too small (likely silence)
if (event.data.size < 100 && !silenceWarningShown) {
console.warn('⚠️ Audio chunks are very small - might be silence. Make sure audio is playing!');
console.warn('Audio chunks are very small - might be silence. Make sure audio is playing!');
silenceWarningShown = true;
}
@ -1872,7 +1872,7 @@ function startBroadcast() {
// Log every second
const now = Date.now();
if (now - lastLogTime > 1000) {
console.log(`📡 Broadcasting: ${chunkCount} chunks sent (${(event.data.size / 1024).toFixed(1)} KB/chunk)`);
console.log(`Broadcasting: ${chunkCount} chunks sent (${(event.data.size / 1024).toFixed(1)} KB/chunk)`);
lastLogTime = now;
// Reset silence warning
@ -1883,13 +1883,13 @@ function startBroadcast() {
} else {
// Debug why chunks aren't being sent
if (event.data.size === 0) {
console.warn('⚠️ Received empty audio chunk');
console.warn('Received empty audio chunk');
}
if (!isBroadcasting) {
console.warn('⚠️ Broadcasting flag is false');
console.warn('Broadcasting flag is false');
}
if (!socket) {
console.warn('⚠️ Socket not connected');
console.warn('Socket not connected');
}
}
};
@ -1898,7 +1898,7 @@ function startBroadcast() {
console.error('❌ MediaRecorder error:', error);
// Try to recover from error
if (isBroadcasting) {
console.log('🔄 Attempting to recover from MediaRecorder error...');
console.log('Attempting to recover from MediaRecorder error...');
setTimeout(() => {
if (isBroadcasting) {
restartBroadcast();
@ -1912,18 +1912,18 @@ function startBroadcast() {
};
mediaRecorder.onstop = (event) => {
console.warn('⚠️ MediaRecorder stopped!');
console.warn('MediaRecorder stopped!');
console.log(` State: ${mediaRecorder.state}`);
console.log(` isBroadcasting flag: ${isBroadcasting}`);
// If we're supposed to be broadcasting but MediaRecorder stopped, restart it
if (isBroadcasting) {
console.error('❌ MediaRecorder stopped unexpectedly while broadcasting!');
console.log('🔄 Auto-recovery: Attempting to restart broadcast in 2 seconds...');
console.log('Auto-recovery: Attempting to restart broadcast in 2 seconds...');
setTimeout(() => {
if (isBroadcasting) {
console.log('🔄 Executing auto-recovery...');
console.log('Executing auto-recovery...');
restartBroadcast();
}
}, 2000);
@ -1931,11 +1931,11 @@ function startBroadcast() {
};
mediaRecorder.onpause = (event) => {
console.warn('⚠️ MediaRecorder paused unexpectedly!');
console.warn('MediaRecorder paused unexpectedly!');
// If we're broadcasting and MediaRecorder paused, resume it
if (isBroadcasting && mediaRecorder.state === 'paused') {
console.log('🔄 Auto-resuming MediaRecorder...');
console.log('Auto-resuming MediaRecorder...');
try {
mediaRecorder.resume();
console.log('✅ MediaRecorder resumed');
@ -2054,7 +2054,7 @@ function stopBroadcast() {
// Restart broadcasting (for auto-recovery)
function restartBroadcast() {
console.log('🔄 Restarting broadcast...');
console.log('Restarting broadcast...');
// Clean up old MediaRecorder without changing UI state
if (streamProcessor) {
@ -2233,7 +2233,7 @@ function initListenerMode() {
window.listenerMediaSource = null;
window.listenerAudioEnabled = false; // Track if user has enabled audio
// Initialize socket and join
// Initialise socket and join
initSocket();
socket.emit('join_listener');
@ -2241,11 +2241,11 @@ function initListenerMode() {
socket.on('broadcast_started', () => {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) nowPlayingEl.textContent = '🎵 Stream is live!';
if (nowPlayingEl) nowPlayingEl.textContent = 'Stream is live!';
// Force a reload of the audio element to capture the fresh stream
if (window.listenerAudio) {
console.log('🔄 Broadcast started: Refreshing audio stream...');
console.log('Broadcast started: Refreshing audio stream...');
const wasPlaying = !window.listenerAudio.paused;
window.listenerAudio.src = getMp3FallbackUrl();
window.listenerAudio.load();
@ -2259,7 +2259,7 @@ function initListenerMode() {
const nowPlayingEl = document.getElementById('listener-now-playing');
if (nowPlayingEl) {
if (data.active) {
const status = data.remote_relay ? '🔗 Remote stream is live!' : '🎵 DJ stream is live!';
const status = data.remote_relay ? 'Remote stream is live!' : 'DJ stream is live!';
nowPlayingEl.textContent = status;
} else {
nowPlayingEl.textContent = 'Stream offline - waiting for DJ...';
@ -2336,12 +2336,12 @@ async function enableListenerAudio() {
listenerAnalyserNode.connect(listenerGainNode);
window.listenerAudio._connectedToContext = true;
console.log('🔗 Connected audio element to AudioContext (with analyser)');
console.log('Connected audio element to AudioContext (with analyser)');
// Start visualizer after the graph exists
// Start visualiser after the graph exists
startListenerVUMeter();
} catch (e) {
console.warn('⚠️ Could not connect to AudioContext:', e.message);
console.warn('Could not connect to AudioContext:', e.message);
}
}
@ -2370,7 +2370,7 @@ async function enableListenerAudio() {
// MP3 stream: call play() immediately to capture the user gesture.
if (audioText) audioText.textContent = 'STARTING...';
console.log('▶️ Attempting to play audio...');
console.log('Attempting to play audio...');
const playPromise = window.listenerAudio.play();
// If not buffered yet, show buffering but don't block.
@ -2414,10 +2414,10 @@ async function enableListenerAudio() {
errorMsg = 'MP3 stream not supported or unavailable (NotSupportedError).';
}
stashedStatus.textContent = '⚠️ ' + errorMsg;
stashedStatus.textContent = '' + errorMsg;
if (error.name === 'NotSupportedError') {
stashedStatus.textContent = '⚠️ MP3 stream failed. Is ffmpeg installed on the server?';
stashedStatus.textContent = 'MP3 stream failed. Is ffmpeg installed on the server?';
}
}
}
@ -2459,7 +2459,7 @@ function monitorTrackEnd() {
if (remaining <= 0.5) {
// Don't pause during broadcast - let the track end naturally
if (isBroadcasting) {
console.log(`🎙️ Track ending during broadcast on Deck ${id} - continuing stream`);
console.log(`Track ending during broadcast on Deck ${id} - continuing stream`);
if (settings[`repeat${id}`]) {
console.log(`🔁 Repeating track on Deck ${id}`);
seekTo(id, 0);
@ -2479,7 +2479,7 @@ function monitorTrackEnd() {
// Check queue for auto-play
if (queues[id] && queues[id].length > 0) {
console.log(`📋 Auto-play: Loading next from Queue ${id}...`);
console.log(`Auto-play: Loading next from Queue ${id}...`);
const next = queues[id].shift();
renderQueue(id); // Update queue UI
@ -2491,7 +2491,7 @@ function monitorTrackEnd() {
});
} else {
// No queue - just stop
console.log(`⏹️ Track ended, queue empty - stopping playback`);
console.log(`Track ended, queue empty - stopping playback`);
decks[id].loading = false;
pauseDeck(id);
decks[id].pausedAt = 0;
@ -2510,10 +2510,10 @@ monitorTrackEnd();
// Reset Deck to Default Settings
function resetDeck(id) {
vibrate(20);
console.log(`🔄 Resetting Deck ${id} to defaults...`);
console.log(`Resetting Deck ${id} to defaults...`);
if (!audioCtx) {
console.warn('AudioContext not initialized');
console.warn('AudioContext not initialised');
return;
}
@ -2595,7 +2595,7 @@ function resetDeck(id) {
function addToQueue(deckId, file, title) {
queues[deckId].push({ file, title });
renderQueue(deckId);
console.log(`📋 Added "${title}" to Queue ${deckId} (${queues[deckId].length} tracks)`);
console.log(`Added "${title}" to Queue ${deckId} (${queues[deckId].length} tracks)`);
// Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) {
@ -2607,7 +2607,7 @@ function addToQueue(deckId, file, title) {
function removeFromQueue(deckId, index) {
const removed = queues[deckId].splice(index, 1)[0];
renderQueue(deckId);
console.log(`🗑️ Removed "${removed.title}" from Queue ${deckId}`);
console.log(`Removed "${removed.title}" from Queue ${deckId}`);
// Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) {
@ -2620,7 +2620,7 @@ function clearQueue(deckId) {
const count = queues[deckId].length;
queues[deckId] = [];
renderQueue(deckId);
console.log(`🗑️ Cleared Queue ${deckId} (${count} tracks removed)`);
console.log(`Cleared Queue ${deckId} (${count} tracks removed)`);
// Sync with server if in server-side mode
if (SERVER_SIDE_AUDIO && socket) {
@ -2631,12 +2631,12 @@ function clearQueue(deckId) {
// Load next track from queue
function loadNextFromQueue(deckId) {
if (queues[deckId].length === 0) {
console.log(`📋 Queue ${deckId} is empty`);
console.log(`Queue ${deckId} is empty`);
return false;
}
const next = queues[deckId].shift();
console.log(`📋 Loading next from Queue ${deckId}: "${next.title}"`);
console.log(`Loading next from Queue ${deckId}: "${next.title}"`);
loadFromServer(deckId, next.file, next.title);
renderQueue(deckId);
@ -2723,7 +2723,7 @@ function renderQueue(deckId) {
const [moved] = queues[deckId].splice(fromIndex, 1);
queues[deckId].splice(index, 0, moved);
renderQueue(deckId);
console.log(`🔄 Reordered Queue ${deckId}`);
console.log(`Reordered Queue ${deckId}`);
}
};
@ -2734,7 +2734,7 @@ function renderQueue(deckId) {
// Auto-load next track when current track ends
function checkAndLoadNextFromQueue(deckId) {
if (settings.autoPlay && queues[deckId].length > 0) {
console.log(`🎵 Auto-loading next track from Queue ${deckId}...`);
console.log(`Auto-loading next track from Queue ${deckId}...`);
setTimeout(() => {
loadNextFromQueue(deckId);
}, 500);
@ -2799,7 +2799,7 @@ async function loadKeyboardMappings() {
keyboardMappings = data.keymaps;
console.log('✅ Loaded custom keyboard mappings from server');
} else {
console.log(' Using default keyboard mappings');
console.log('Using default keyboard mappings');
}
} catch (e) {
console.error('Failed to load keyboard mappings from server:', e);
@ -2899,7 +2899,7 @@ document.addEventListener('keydown', (e) => {
if (mapping) {
e.preventDefault();
console.log(`⌨️ Keyboard: ${key}${mapping.label}`);
console.log(`Keyboard: ${key}${mapping.label}`);
executeKeyboardAction(mapping.action);
}
});
@ -2935,7 +2935,7 @@ function createKeyboardSettingsPanel() {
panel.className = 'settings-panel active';
panel.innerHTML = `
<div class="panel-header">
<h2> Keyboard Shortcuts</h2>
<h2>Keyboard Shortcuts</h2>
<button onclick="closeKeyboardSettings()"></button>
</div>
<div class="panel-content">
@ -3078,6 +3078,6 @@ function importKeyboardMappings() {
input.click();
}
// Initialize on load
// Initialise on load
loadKeyboardMappings();
console.log('⌨️ Keyboard shortcuts enabled. Press H for help.');
console.log('Keyboard shortcuts enabled. Press H for help.');

259
style.css
View File

@ -33,50 +33,24 @@ body {
}
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 99999;
border: 1px solid rgba(80, 80, 80, 0.1);
box-sizing: border-box;
display: none !important;
/* Completely disabled to prevent UI blocking */
}
body.playing-A::before {
border: none;
box-shadow:
0 0 var(--glow-spread) rgba(0, 243, 255, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.8));
animation: pulse-cyan 3s ease-in-out infinite;
display: none !important;
}
body.playing-B::before {
border: none;
box-shadow:
0 0 var(--glow-spread) rgba(188, 19, 254, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(188, 19, 254, calc(var(--glow-opacity) * 0.8));
animation: pulse-magenta 3s ease-in-out infinite;
display: none !important;
}
body.playing-A.playing-B::before {
border: none;
box-shadow:
0 0 var(--glow-spread) rgba(0, 243, 255, var(--glow-opacity)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, var(--glow-opacity)),
inset 0 0 var(--glow-spread) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.6)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 0.6));
animation: pulse-both 3s ease-in-out infinite;
display: none !important;
}
body.listener-glow::before {
border: none;
box-shadow:
0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1.5)),
0 0 calc(var(--glow-spread) * 2.5) rgba(0, 243, 255, calc(var(--glow-opacity) * 0.5)),
inset 0 0 var(--glow-spread) rgba(0, 80, 255, calc(var(--glow-opacity) * 1)),
inset 0 0 calc(var(--glow-spread) * 1.5) rgba(188, 19, 254, calc(var(--glow-opacity) * 1));
animation: pulse-listener 4s ease-in-out infinite;
display: none !important;
}
@keyframes pulse-listener {
@ -1621,7 +1595,8 @@ input[type=range] {
}
body::before {
border-width: 2px;
display: none !important;
/* Completely disabled */
}
.app-container {
@ -1659,6 +1634,14 @@ input[type=range] {
display: flex;
}
.app-container.show-queue-A #queue-A {
display: flex;
}
.app-container.show-queue-B #queue-B {
display: flex;
}
/* Mixer integration: Show mixer combined with active deck in a scrollable view */
.app-container.show-deck-A .mixer-section,
.app-container.show-deck-B .mixer-section {
@ -1764,7 +1747,7 @@ input[type=range] {
.streaming-btn {
position: fixed;
bottom: 25px;
right: 100px;
right: 175px;
width: 60px;
height: 60px;
border-radius: 50%;
@ -1793,17 +1776,17 @@ input[type=range] {
.upload-btn {
position: fixed;
bottom: 25px;
right: 170px;
right: 100px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(145deg, #222, #111);
border: 2px solid var(--primary-cyan);
color: var(--primary-cyan);
border: 2px solid #00ff00;
color: #00ff00;
font-size: 1.8rem;
cursor: pointer;
z-index: 10000;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.4);
box-shadow: 0 0 20px rgba(0, 255, 0, 0.4);
transition: all 0.3s;
display: flex;
align-items: center;
@ -1812,7 +1795,7 @@ input[type=range] {
.upload-btn:hover {
transform: scale(1.1);
box-shadow: 0 0 30px rgba(0, 255, 255, 0.6);
box-shadow: 0 0 30px rgba(0, 255, 0, 0.6);
}
.upload-btn:active {
@ -2431,7 +2414,7 @@ body.listening-active .landscape-prompt {
.keyboard-btn {
position: fixed;
bottom: 25px;
right: 100px;
right: 250px;
width: 60px;
height: 60px;
border-radius: 50%;
@ -2449,7 +2432,7 @@ body.listening-active .landscape-prompt {
}
.keyboard-btn:hover {
transform: scale(1.1) rotate(5deg);
transform: scale(1.1);
box-shadow: 0 0 30px rgba(255, 187, 0, 0.6);
}
@ -2886,8 +2869,9 @@ body.listening-active .landscape-prompt {
overflow: hidden;
}
/* Hide library in landscape - focus on DJing */
.library-section {
/* Hide library and queues in landscape - focus on DJing */
.library-section,
.queue-section {
display: none !important;
}
@ -3126,32 +3110,21 @@ body.listening-active .landscape-prompt {
font-size: 1.2rem !important;
}
/* Reduce edge border effect intensity */
/* Completely disable edge border effects */
body::before {
border: 2px solid rgba(80, 80, 80, 0.3) !important;
display: none !important;
}
body.playing-A::before {
border: 10px solid var(--primary-cyan) !important;
box-shadow:
0 0 60px rgba(0, 243, 255, 1),
inset 0 0 60px rgba(0, 243, 255, 0.7) !important;
display: none !important;
}
body.playing-B::before {
border: 10px solid var(--secondary-magenta) !important;
box-shadow:
0 0 60px rgba(188, 19, 254, 1),
inset 0 0 60px rgba(188, 19, 254, 0.7) !important;
display: none !important;
}
body.playing-A.playing-B::before {
border: 10px solid var(--primary-cyan) !important;
box-shadow:
0 0 60px rgba(0, 243, 255, 1),
0 0 80px rgba(188, 19, 254, 1),
inset 0 0 60px rgba(0, 243, 255, 0.6),
inset 0 0 80px rgba(188, 19, 254, 0.6) !important;
display: none !important;
}
}
@ -3277,12 +3250,14 @@ body.listening-active .landscape-prompt {
/* Hide non-active sections in portrait tabs */
.library-section,
.deck,
.queue-section,
.mixer-section {
display: none;
}
.library-section.active,
.deck.active {
.deck.active,
.queue-section.active {
display: flex !important;
flex: 1;
width: 100%;
@ -3309,7 +3284,7 @@ body.listening-active .landscape-prompt {
height: 70px;
}
/* Optimize deck layout for portrait */
/* Optimise deck layout for portrait */
.deck {
flex-direction: column;
overflow-y: auto;
@ -3794,7 +3769,7 @@ body.listening-active .landscape-prompt {
}
.track-row.loaded-deck-a::before {
content: 'A';
content: 'A';
position: absolute;
left: 8px;
top: 50%;
@ -3817,7 +3792,7 @@ body.listening-active .landscape-prompt {
}
.track-row.loaded-deck-b::before {
content: 'B';
content: 'B';
position: absolute;
right: 8px;
top: 50%;
@ -3846,7 +3821,7 @@ body.listening-active .landscape-prompt {
}
.track-row.loaded-both::before {
content: 'A';
content: 'A';
position: absolute;
left: 8px;
top: 50%;
@ -3858,7 +3833,7 @@ body.listening-active .landscape-prompt {
}
.track-row.loaded-both::after {
content: 'B';
content: 'B';
position: absolute;
right: 8px;
top: 50%;
@ -3874,4 +3849,158 @@ body.listening-active .landscape-prompt {
position: relative;
padding-left: 40px;
padding-right: 40px;
}
/* ==========================================
STANDALONE QUEUE SECTIONS FOR MOBILE
========================================== */
.queue-section {
display: none;
flex-direction: column;
padding: 20px;
background: rgba(10, 10, 20, 0.95);
overflow-y: auto;
}
.queue-section.active {
display: flex !important;
flex: 1;
width: 100%;
}
.queue-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.queue-page-title {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: bold;
color: #0ff;
margin: 0;
text-shadow: 0 0 15px rgba(0, 243, 255, 0.6);
}
#queue-B .queue-page-title {
color: #f0f;
text-shadow: 0 0 15px rgba(188, 19, 254, 0.6);
}
#queue-B .queue-item {
border-left-color: #f0f;
}
#queue-B .queue-number {
color: #f0f;
}
/* ==========================================
RESPONSIVE CROSSFADER WIDTH
========================================== */
/* Portrait mode - narrower crossfader */
@media (max-width: 1024px) and (orientation: portrait) {
.mixer-section {
padding: 10px 20px !important;
max-width: 70% !important;
margin: 0 auto !important;
left: 50% !important;
transform: translateX(-50%);
}
.xfader {
width: 100% !important;
}
}
/* Landscape mode - wider crossfader */
@media (max-width: 1024px) and (orientation: landscape) {
.mixer-section {
padding: 8px 40px !important;
width: 100% !important;
max-width: 100% !important;
}
.xfader {
width: 100% !important;
}
}
/* ==========================================
MOBILE FLOATING BUTTONS POSITIONING
========================================== */
@media (max-width: 1024px) {
/* Adjust floating buttons to not overlap with mobile tabs */
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
bottom: 95px !important;
/* Above mobile tabs */
font-size: 0.9rem !important;
}
/* Compact button sizes on mobile */
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
width: 50px !important;
height: 50px !important;
}
/* Adjust spacing for smaller buttons */
.keyboard-btn {
right: 220px !important;
}
.streaming-btn {
right: 155px !important;
}
.upload-btn {
right: 90px !important;
}
.settings-btn {
right: 25px !important;
}
}
/* Extra small screens - stack buttons vertically on right side */
@media (max-width: 480px) {
.keyboard-btn,
.streaming-btn,
.upload-btn,
.settings-btn {
right: 10px !important;
width: 45px !important;
height: 45px !important;
font-size: 0.8rem !important;
}
.keyboard-btn {
bottom: 250px !important;
}
.streaming-btn {
bottom: 195px !important;
}
.upload-btn {
bottom: 140px !important;
}
.settings-btn {
bottom: 85px !important;
}
}