Spaces:
Build error
Build error
| """ | |
| 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") | |
| async def root(): | |
| """Serve the main HTML page""" | |
| return HTMLResponse(content=get_main_html()) | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return {"status": "healthy", "timestamp": datetime.now().isoformat()} | |
| 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 | |
| } | |
| 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" | |
| ) |