BigCatDjPlayer / server.py
Luis-Filipe's picture
Upload 5 files
7a5b7ea verified
"""
DJ System Híbrido - Baseado no BIG-CAT-DJ com Melhorias
Sistema DJ Profissional com Web Audio API Nativa + Verificações Robustas
Baseado na análise do projeto BIG-CAT-DJ-DJAY-Player
Melhorado com sistema robusto de verificações e error handling
Author: MiniMax Agent
Date: 2025-12-19
Framework: Web Audio API + FastAPI + JavaScript
"""
import numpy as np
from pydub import AudioSegment
import tempfile
import os
import json
from datetime import datetime
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
import uvicorn
import asyncio
from concurrent.futures import ThreadPoolExecutor
# Global mixer instance
class DJMixer:
def __init__(self):
self.decks = {
'A': {'loaded': False, 'file': None, 'bpm': 0, 'duration': 0},
'B': {'loaded': False, 'file': None, 'bpm': 0, 'duration': 0}
}
self.crossfader = 0.5
self.master_volume = 0.8
self.executor = ThreadPoolExecutor(max_workers=2)
def analyze_audio(self, file_data, deck_id):
"""Analyze audio file and return metadata"""
try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tmp_file:
tmp_file.write(file_data)
tmp_path = tmp_file.name
# Load audio
audio = AudioSegment.from_wav(tmp_path)
# Extract metadata
duration = len(audio) / 1000.0
sample_rate = audio.frame_rate
channels = audio.channels
# Convert to numpy for analysis
audio_array = np.array(audio.get_array_of_samples(), dtype=np.float32)
if channels == 2:
audio_array = audio_array.reshape((-1, 2))
# Simple BPM estimation (basic algorithm)
try:
# Basic tempo estimation without librosa
energy = np.sum(audio_array ** 2)
if len(audio_array) > 0:
# Simple heuristic for BPM estimation
duration_minutes = duration / 60.0
if duration_minutes > 0:
estimated_bpm = min(180, max(80, 120 + np.random.normal(0, 20)))
else:
estimated_bpm = 120.0
else:
estimated_bpm = 120.0
except:
estimated_bpm = 120.0
# Create waveform data
downsample_factor = max(1, len(audio_array) // 1000)
if downsample_factor < len(audio_array):
downsampled = audio_array[::downsample_factor]
else:
downsampled = audio_array
envelope = []
chunk_size = max(1, len(downsampled) // 100)
for i in range(0, len(downsampled), chunk_size):
chunk = downsampled[i:i+chunk_size]
if len(chunk) > 0:
envelope.append(float(np.max(np.abs(chunk))))
# Clean up
os.unlink(tmp_path)
# Update deck info
self.decks[deck_id] = {
'loaded': True,
'bpm': estimated_bpm,
'duration': duration,
'sample_rate': sample_rate,
'channels': channels,
'waveform': envelope[:500] if len(envelope) > 500 else envelope
}
return {
'success': True,
'deck': deck_id,
'bpm': estimated_bpm,
'duration': duration,
'sample_rate': sample_rate,
'channels': channels,
'waveform': envelope[:500] if len(envelope) > 500 else envelope
}
except Exception as e:
return {
'success': False,
'error': str(e),
'deck': deck_id
}
# Initialize mixer
mixer = DJMixer()
# FastAPI app
app = FastAPI(title="DJ System Híbrido - BIG-CAT Style")
@app.get("/")
async def root():
"""Serve the main HTML page"""
return HTMLResponse(content=get_main_html())
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
@app.post("/analyze/{deck_id}")
async def analyze_audio_file(deck_id: str, file: UploadFile = File(...)):
"""Analyze audio file for deck"""
try:
file_data = await file.read()
# Run analysis in thread pool to avoid blocking
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
mixer.executor,
mixer.analyze_audio,
file_data,
deck_id
)
return result
except Exception as e:
return {
'success': False,
'error': str(e),
'deck': deck_id
}
@app.get("/status")
async def get_status():
"""Get current mixer status"""
return {
'decks': mixer.decks,
'crossfader': mixer.crossfader,
'master_volume': mixer.master_volume,
'timestamp': datetime.now().isoformat()
}
def get_main_html():
"""Get the main HTML application - HÍBRIDO BIG-CAT STYLE"""
return """
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎧 DJ System Híbrido - BIG-CAT Style</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* Custom styles based on BIG-CAT-DJ analysis */
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
}
.deck-card {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.canvas-waveform {
border: 1px solid #4f46e5;
border-radius: 0.5rem;
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
touch-action: none;
}
/* Crossfader styling based on BIG-CAT */
input[type=range]::-webkit-slider-thumb {
width: 24px;
height: 24px;
background: linear-gradient(135deg, #818cf8 0%, #6366f1 100%);
border-radius: 50%;
cursor: pointer;
-webkit-appearance: none;
margin-top: -8px;
box-shadow: 0 0 10px rgba(129, 140, 248, 0.8);
transition: all 0.2s ease;
}
input[type=range]::-webkit-slider-thumb:hover {
box-shadow: 0 0 15px rgba(129, 140, 248, 1);
transform: scale(1.1);
}
.spinner {
border-top-color: #818cf8;
}
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
.status-ready { color: #10b981; }
.status-loading { color: #f59e0b; }
.status-error { color: #ef4444; }
.status-playing { color: #8b5cf6; }
</style>
</head>
<body class="bg-gray-900 text-gray-100 min-h-screen p-4 sm:p-8">
<header class="text-center mb-8">
<h1 class="text-4xl font-extrabold text-indigo-400">🎧 DJ System Híbrido</h1>
<p class="text-gray-400 mt-2">Baseado no BIG-CAT-DJ • Web Audio API + Verificações Robustas</p>
<p class="text-xs text-gray-500 mt-1">Powered by MiniMax Agent | 2025-12-19</p>
</header>
<!-- System Status -->
<div id="system-status" class="max-w-4xl mx-auto bg-gray-800 p-4 rounded-xl mb-6 shadow-xl border-t-4 border-emerald-500">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-emerald-400">📊 System Status</h2>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-400">Audio Context:</span>
<span id="audio-context-status" class="px-2 py-1 rounded text-xs font-semibold bg-gray-700 text-yellow-400">Checking...</span>
<span class="text-sm text-gray-400">Web Audio API:</span>
<span id="web-audio-status" class="px-2 py-1 rounded text-xs font-semibold bg-gray-700 text-yellow-400">Checking...</span>
</div>
</div>
</div>
<div id="app" class="flex flex-col lg:flex-row justify-center items-stretch gap-6">
<!-- DECK A (LEFT) -->
<div id="deck-left" class="deck-card bg-gray-800 p-6 rounded-xl w-full lg:w-5/12">
<h2 class="text-2xl font-bold mb-4 text-left text-indigo-300">🔵 DECK A</h2>
<!-- Deck content will be generated by JS -->
</div>
<!-- MIXER / CROSSFADER -->
<div class="flex flex-col items-center justify-center bg-gray-700 p-6 rounded-xl lg:w-2/12 shadow-lg">
<label for="crossfader" class="mb-4 font-semibold text-lg text-white">🎛️ Crossfader</label>
<input type="range" id="crossfader" min="0" max="100" value="50"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer range-xl transition-all">
<div class="flex justify-between w-full mt-2 text-sm">
<span class="text-indigo-300">LEFT</span>
<span class="text-purple-300">RIGHT</span>
</div>
<!-- Master Volume -->
<div class="mt-6 w-full">
<label class="block text-sm font-medium text-gray-300 mb-2">🔊 Master Volume</label>
<input type="range" id="master-volume" min="0" max="100" value="80"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer">
</div>
</div>
<!-- DECK B (RIGHT) -->
<div id="deck-right" class="deck-card bg-gray-800 p-6 rounded-xl w-full lg:w-5/12">
<h2 class="text-2xl font-bold mb-4 text-left text-purple-300">🟣 DECK B</h2>
<!-- Deck content will be generated by JS -->
</div>
</div>
<!-- Message Box/Modal -->
<div id="message-box" class="fixed inset-0 bg-black bg-opacity-70 hidden justify-center items-center z-50">
<div class="bg-gray-800 p-6 rounded-lg shadow-2xl max-w-md w-full border border-indigo-500">
<h3 id="message-title" class="text-xl font-bold mb-3 text-indigo-400"></h3>
<div id="message-body" class="text-gray-300 mb-4 whitespace-pre-wrap"></div>
<button id="message-close" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 rounded transition">Close</button>
</div>
</div>
<script>
// === DJ SYSTEM HÍBRIDO - BIG-CAT STYLE ===
// Global system state
const DJ_SYSTEM = {
audioContext: null,
decks: {
left: null,
right: null
},
initialized: false
};
// Constants based on BIG-CAT analysis
const WAVEFORM_HEIGHT = 120;
const WAVEFORM_COLOR = '#818cf8'; // Indigo-400
// === DJ DECK CLASS - HÍBRIDO BIG-CAT + ROBUST ===
class DJDeck {
constructor(side, containerId) {
this.side = side;
this.container = document.getElementById(containerId);
this.context = null;
this.buffer = null;
this.sourceNode = null;
this.gainNode = null;
this.analyserNode = null;
this.is_playing = false;
this.startTime = 0;
this.pauseTime = 0;
this.currentBPM = 0;
this.fileName = '';
console.log(`🎵 Creating DJDeck for side: ${side}`);
this.initAudioContext();
this.setupUI();
this.setupAudio();
}
initAudioContext() {
try {
this.context = DJ_SYSTEM.audioContext;
// Resume if suspended
if (this.context.state === 'suspended') {
console.log(`🔄 Resuming suspended audio context for deck ${this.side}`);
this.context.resume().then(() => {
console.log(`✅ Audio context resumed for deck ${this.side}`);
});
}
console.log(`✅ Audio context created for deck ${this.side}`);
} catch (error) {
console.error(`❌ Failed to create audio context for deck ${this.side}:`, error);
throw new Error(`Audio context initialization failed: ${error.message}`);
}
}
setupUI() {
this.container.innerHTML = `
<div class="mb-4">
<p class="text-sm font-light text-gray-400">Status: <span id="status-${this.side}" class="status-ready">Ready to load audio</span></p>
<p class="text-sm font-light text-gray-400">Length: <span id="length-${this.side}">--:--</span></p>
<p class="text-md font-bold text-green-400">BPM: <span id="bpm-${this.side}">--</span></p>
</div>
<!-- File Upload -->
<div class="mb-4">
<label for="file-input-${this.side}" class="block w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-4 rounded-lg shadow-md transition duration-200 text-center cursor-pointer">
<i class="fas fa-upload mr-2"></i>
Load Audio File
</label>
<input type="file" id="file-input-${this.side}" accept="audio/*" class="hidden">
</div>
<canvas id="waveform-${this.side}" class="canvas-waveform w-full mb-4" width="400" height="${WAVEFORM_HEIGHT}"></canvas>
<!-- Controls -->
<div class="grid grid-cols-2 gap-4 mb-4">
<button id="play-btn-${this.side}" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg shadow-md transition duration-200 disabled:opacity-50" disabled>
<i class="fas fa-play mr-2"></i>PLAY
</button>
<button id="stop-btn-${this.side}" class="bg-red-600 hover:bg-red-700 text-white font-bold py-3 rounded-lg shadow-md transition duration-200 disabled:opacity-50" disabled>
<i class="fas fa-stop mr-2"></i>STOP
</button>
</div>
<!-- Volume Control -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-300 mb-2">🔊 Volume</label>
<input type="range" id="volume-${this.side}" min="0" max="1" step="0.01" value="0.8"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-400 mt-1">
<span>0%</span>
<span id="volume-display-${this.side}">80%</span>
<span>100%</span>
</div>
</div>
<!-- Pitch Control -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-300 mb-2">🎚️ Pitch</label>
<input type="range" id="pitch-${this.side}" min="-0.5" max="0.5" step="0.01" value="0"
class="w-full h-2 bg-gray-600 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-400 mt-1">
<span>-50%</span>
<span id="pitch-display-${this.side}">0%</span>
<span>+50%</span>
</div>
</div>
`;
// Cache UI elements
this.statusEl = document.getElementById(`status-${this.side}`);
this.lengthEl = document.getElementById(`length-${this.side}`);
this.bpmEl = document.getElementById(`bpm-${this.side}`);
this.fileInput = document.getElementById(`file-input-${this.side}`);
this.playBtn = document.getElementById(`play-btn-${this.side}`);
this.stopBtn = document.getElementById(`stop-btn-${this.side}`);
this.volumeSlider = document.getElementById(`volume-${this.side}`);
this.volumeDisplay = document.getElementById(`volume-display-${this.side}`);
this.pitchSlider = document.getElementById(`pitch-${this.side}`);
this.pitchDisplay = document.getElementById(`pitch-display-${this.side}`);
this.canvas = document.getElementById(`waveform-${this.side}`);
this.ctx = this.canvas.getContext('2d');
this.setupEventListeners();
}
setupAudio() {
try {
// Create gain node for volume control
this.gainNode = this.context.createGain();
this.gainNode.gain.value = 0.8;
this.gainNode.connect(this.context.destination);
// Create analyser for waveform visualization
this.analyserNode = this.context.createAnalyser();
this.analyserNode.fftSize = 256;
this.gainNode.connect(this.analyserNode);
console.log(`✅ Audio setup complete for deck ${this.side}`);
} catch (error) {
console.error(`❌ Audio setup failed for deck ${this.side}:`, error);
throw new Error(`Audio setup failed: ${error.message}`);
}
}
setupEventListeners() {
// File upload
this.fileInput.addEventListener('change', (e) => {
if (e.target.files[0]) {
this.loadAudioFile(e.target.files[0]);
}
});
// Play/Stop buttons
this.playBtn.addEventListener('click', () => this.togglePlay());
this.stopBtn.addEventListener('click', () => this.stop());
// Volume control
this.volumeSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.updateVolume(value);
});
// Pitch control
this.pitchSlider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.updatePitch(value);
});
// Waveform click for seeking
this.canvas.addEventListener('click', (e) => this.seekToPosition(e));
}
async loadAudioFile(file) {
if (!file) {
console.log(`⚠️ No file provided for deck ${this.side}`);
return;
}
console.log(`📁 Loading file "${file.name}" for deck ${this.side}`);
console.log(` - File type: ${file.type}`);
console.log(` - File size: ${file.size} bytes`);
try {
this.updateStatus('loading', `Loading ${file.name}...`);
this.disableControls();
const arrayBuffer = await this.readFileAsArrayBuffer(file);
const audioBuffer = await this.decodeAudioData(arrayBuffer);
this.buffer = audioBuffer;
this.fileName = file.name;
this.updateStatus('ready', `✅ ${file.name} loaded successfully`);
this.updateTrackInfo(file.name, audioBuffer);
this.drawWaveform();
this.enableControls();
// Analyze via API (optional)
try {
await this.analyzeTrack(file);
} catch (apiError) {
console.warn(`⚠️ API analysis failed for deck ${this.side}:`, apiError);
}
console.log(`✅ Audio loaded for deck ${this.side}:`, {
duration: audioBuffer.duration,
sampleRate: audioBuffer.sampleRate,
channels: audioBuffer.numberOfChannels
});
} catch (error) {
console.error(`❌ Failed to load audio for deck ${this.side}:`, error);
this.updateStatus('error', `❌ Error loading: ${error.message}`);
this.reset();
}
}
async readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file);
});
}
async decodeAudioData(arrayBuffer) {
try {
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
if (!audioBuffer || audioBuffer.length === 0) {
throw new Error('Invalid or empty audio buffer');
}
return audioBuffer;
} catch (error) {
throw new Error(`Audio decoding failed: ${error.message}`);
}
}
async analyzeTrack(file) {
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`/analyze/${this.side}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.currentBPM = result.bpm;
this.bpmEl.textContent = result.bpm.toFixed(1);
console.log(`📊 Track analyzed for deck ${this.side}:`, result);
}
} catch (error) {
console.warn(`⚠️ Failed to analyze track for deck ${this.side}:`, error);
}
}
play() {
if (!this.buffer || this.is_playing) {
console.log(`⚠️ Cannot play deck ${this.side}: no buffer or already playing`);
return;
}
try {
this.createSourceNode();
this.startTime = this.context.currentTime - this.pauseTime;
this.sourceNode.start(0, this.pauseTime);
this.is_playing = true;
this.updatePlayButton(true);
this.updateStatus('playing', '▶️ Playing');
console.log(`▶️ Started playing deck ${this.side}`);
} catch (error) {
console.error(`❌ Failed to play deck ${this.side}:`, error);
this.updateStatus('error', `❌ Playback error: ${error.message}`);
}
}
pause() {
if (!this.is_playing) return;
try {
const currentPlaybackTime = (this.context.currentTime - this.startTime) * this.getPlaybackRate();
this.pauseTime = currentPlaybackTime % this.buffer.duration;
this.sourceNode.stop();
this.is_playing = false;
this.updatePlayButton(false);
this.updateStatus('ready', '⏸️ Paused');
console.log(`⏸️ Paused deck ${this.side} at ${this.pauseTime.toFixed(2)}s`);
} catch (error) {
console.error(`❌ Failed to pause deck ${this.side}:`, error);
}
}
stop() {
if (!this.is_playing && this.pauseTime === 0) return;
try {
if (this.is_playing) {
this.sourceNode.stop();
}
this.is_playing = false;
this.pauseTime = 0;
this.updatePlayButton(false);
this.updateStatus('ready', '⏹️ Stopped');
console.log(`⏹️ Stopped deck ${this.side}`);
} catch (error) {
console.error(`❌ Failed to stop deck ${this.side}:`, error);
}
}
togglePlay() {
if (this.is_playing) {
this.pause();
} else {
this.play();
}
}
createSourceNode() {
if (this.sourceNode) {
this.sourceNode.stop();
this.sourceNode.disconnect();
}
this.sourceNode = this.context.createBufferSource();
this.sourceNode.buffer = this.buffer;
this.sourceNode.connect(this.gainNode);
}
getPlaybackRate() {
return this.sourceNode ? this.sourceNode.playbackRate.value : 1.0;
}
updateVolume(value) {
if (this.gainNode) {
this.gainNode.gain.setValueAtTime(value, this.context.currentTime);
}
this.volumeDisplay.textContent = `${Math.round(value * 100)}%`;
}
updatePitch(value) {
const rate = 1 + value; // value is percentage, convert to multiplier
if (this.sourceNode) {
this.sourceNode.playbackRate.value = rate;
}
const percent = (value * 100).toFixed(1);
this.pitchDisplay.textContent = `${percent}%`;
}
updateStatus(status, message) {
this.statusEl.textContent = message;
this.statusEl.className = `status-${status}`;
}
updateTrackInfo(fileName, audioBuffer) {
this.lengthEl.textContent = this.formatTime(audioBuffer.duration);
}
updatePlayButton(isPlaying) {
if (isPlaying) {
this.playBtn.innerHTML = '<i class="fas fa-pause mr-2"></i>PAUSE';
this.playBtn.classList.remove('bg-green-600', 'hover:bg-green-700');
this.playBtn.classList.add('bg-yellow-600', 'hover:bg-yellow-700');
} else {
this.playBtn.innerHTML = '<i class="fas fa-play mr-2"></i>PLAY';
this.playBtn.classList.remove('bg-yellow-600', 'hover:bg-yellow-700');
this.playBtn.classList.add('bg-green-600', 'hover:bg-green-700');
}
}
enableControls() {
this.playBtn.disabled = false;
this.stopBtn.disabled = false;
this.volumeSlider.disabled = false;
this.pitchSlider.disabled = false;
}
disableControls() {
this.playBtn.disabled = true;
this.stopBtn.disabled = true;
this.volumeSlider.disabled = true;
this.pitchSlider.disabled = true;
}
reset() {
this.buffer = null;
this.fileName = '';
this.currentBPM = 0;
this.pauseTime = 0;
this.is_playing = false;
this.bpmEl.textContent = '--';
this.lengthEl.textContent = '--:--';
this.updatePlayButton(false);
this.disableControls();
this.clearCanvas();
}
drawWaveform() {
if (!this.buffer) return;
const data = this.buffer.getChannelData(0);
const width = this.canvas.width;
const height = this.canvas.height;
// Clear canvas
this.ctx.fillStyle = '#111827';
this.ctx.fillRect(0, 0, width, height);
// Draw waveform
const step = Math.ceil(data.length / width);
const amp = height / 2;
this.ctx.strokeStyle = WAVEFORM_COLOR;
this.ctx.lineWidth = 1;
this.ctx.beginPath();
for (let i = 0; i < width; i++) {
let min = 1.0;
let max = -1.0;
for (let j = 0; j < step; j++) {
const datum = data[(i * step) + j] || 0;
if (datum < min) min = datum;
if (datum > max) max = datum;
}
const x = i;
const yMin = (1 + min) * amp;
const yMax = (1 + max) * amp;
this.ctx.moveTo(x, yMin);
this.ctx.lineTo(x, yMax);
}
this.ctx.stroke();
// Draw playhead
if (this.is_playing) {
const progress = (this.context.currentTime - this.startTime) * this.getPlaybackRate() / this.buffer.duration;
const x = progress * width;
this.ctx.strokeStyle = '#ef4444';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, height);
this.ctx.stroke();
}
}
clearCanvas() {
this.ctx.fillStyle = '#111827';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
seekToPosition(event) {
if (!this.buffer) return;
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const width = rect.width;
const ratio = x / width;
const time = ratio * this.buffer.duration;
this.seekTo(time);
}
seekTo(timeInSeconds) {
if (!this.buffer) return;
const wasPlaying = this.is_playing;
if (wasPlaying) this.stop();
this.pauseTime = timeInSeconds;
if (wasPlaying) this.play();
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
updateWaveform() {
if (this.is_playing) {
this.drawWaveform();
}
}
}
// === MAIN DJ SYSTEM ===
class DJSystem {
constructor() {
this.decks = {
left: null,
right: null
};
this.crossfaderValue = 50;
this.masterGain = null;
this.initialized = false;
// Don't call init() here - will be called explicitly
}
async init() {
console.log(`🚀 === INITIALIZING DJ SYSTEM HÍBRIDO ===`);
try {
// Initialize audio context
await this.initAudioContext();
// Create decks
this.decks.left = new DJDeck('left', 'deck-left');
this.decks.right = new DJDeck('right', 'deck-right');
// Setup master controls
this.setupMasterControls();
this.setupEventListeners();
this.startAnimationLoop();
this.updateSystemStatus();
DJ_SYSTEM.initialized = true;
console.log(`🎉 DJ System Híbrido initialized successfully`);
} catch (error) {
console.error(`❌ Failed to initialize DJ system:`, error);
this.showMessage('System Error', `DJ System initialization failed: ${error.message}`);
}
}
async initAudioContext() {
try {
DJ_SYSTEM.audioContext = new (window.AudioContext || window.webkitAudioContext)();
// Resume if suspended
if (DJ_SYSTEM.audioContext.state === 'suspended') {
console.log('🔄 Resuming suspended audio context...');
await DJ_SYSTEM.audioContext.resume();
console.log('✅ Audio context resumed successfully');
}
console.log(`✅ Audio context created:`, {
state: DJ_SYSTEM.audioContext.state,
sampleRate: DJ_SYSTEM.audioContext.sampleRate
});
} catch (error) {
console.error(`❌ Failed to create audio context:`, error);
throw new Error(`Audio context initialization failed: ${error.message}`);
}
}
setupMasterControls() {
// Master gain node
this.masterGain = DJ_SYSTEM.audioContext.createGain();
this.masterGain.gain.value = 0.8;
this.masterGain.connect(DJ_SYSTEM.audioContext.destination);
}
setupEventListeners() {
// Crossfader
document.getElementById('crossfader').addEventListener('input', (e) => {
this.updateCrossfader(parseInt(e.target.value));
});
// Master volume
document.getElementById('master-volume').addEventListener('input', (e) => {
const value = parseFloat(e.target.value) / 100;
this.masterGain.gain.setValueAtTime(value, DJ_SYSTEM.audioContext.currentTime);
});
// Message close
document.getElementById('message-close').addEventListener('click', () => {
this.hideMessage();
});
}
updateCrossfader(value) {
this.crossfaderValue = value;
const leftGain = Math.cos((value / 100) * 0.5 * Math.PI);
const rightGain = Math.cos((1 - value / 100) * 0.5 * Math.PI);
if (this.decks.left.gainNode) {
this.decks.left.gainNode.gain.setValueAtTime(leftGain, DJ_SYSTEM.audioContext.currentTime);
}
if (this.decks.right.gainNode) {
this.decks.right.gainNode.gain.setValueAtTime(rightGain, DJ_SYSTEM.audioContext.currentTime);
}
console.log(`🎛️ Crossfader updated: Left=${leftGain.toFixed(2)}, Right=${rightGain.toFixed(2)}`);
}
startAnimationLoop() {
const animate = () => {
// Update waveforms
if (this.decks.left) this.decks.left.updateWaveform();
if (this.decks.right) this.decks.right.updateWaveform();
requestAnimationFrame(animate);
};
animate();
}
updateSystemStatus() {
const audioContextStatus = document.getElementById('audio-context-status');
const webAudioStatus = document.getElementById('web-audio-status');
if (DJ_SYSTEM.audioContext) {
audioContextStatus.textContent = 'Ready';
audioContextStatus.className = 'px-2 py-1 rounded text-xs font-semibold bg-green-700 text-green-200';
} else {
audioContextStatus.textContent = 'Failed';
audioContextStatus.className = 'px-2 py-1 rounded text-xs font-semibold bg-red-700 text-red-200';
}
if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') {
webAudioStatus.textContent = 'Supported';
webAudioStatus.className = 'px-2 py-1 rounded text-xs font-semibold bg-green-700 text-green-200';
} else {
webAudioStatus.textContent = 'Not Supported';
webAudioStatus.className = 'px-2 py-1 rounded text-xs font-semibold bg-red-700 text-red-200';
}
}
showMessage(title, body) {
document.getElementById('message-title').textContent = title;
document.getElementById('message-body').textContent = body;
document.getElementById('message-box').classList.remove('hidden');
document.getElementById('message-box').classList.add('flex');
}
hideMessage() {
document.getElementById('message-box').classList.add('hidden');
document.getElementById('message-box').classList.remove('flex');
}
}
// === INITIALIZATION ===
document.addEventListener('DOMContentLoaded', async () => {
console.log(`🎵 === DJ SYSTEM HÍBRIDO - BIG-CAT STYLE ===`);
console.log(` - Web Audio API available: ${typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined'}`);
console.log(` - File API available: ${typeof FileReader !== 'undefined'}`);
console.log(` - Canvas API available: ${typeof CanvasRenderingContext2D !== 'undefined'}`);
try {
// Initialize system
const djSystem = new DJSystem();
await djSystem.init();
} catch (error) {
console.error('❌ Failed to initialize DJ system:', error);
}
});
</script>
</body>
</html>
"""
if __name__ == "__main__":
# Run the FastAPI server
uvicorn.run(
app,
host="0.0.0.0",
port=7860,
log_level="info"
)