// ═══════════════════════════════════════════════════════════ // COMPLETE UPDATED BACKEND SERVER // Version 15.0 - Added "Slowed" Effect + Improved Bass Boost // ═══════════════════════════════════════════════════════════ const express = require('express'); const multer = require('multer'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const { exec } = require('child_process'); const util = require('util'); const execPromise = util.promisify(exec); const app = express(); const PORT = process.env.PORT || 7860; app.use(cors()); app.use(express.json({ limit: '10mb' })); const uploadsDir = path.join(__dirname, 'uploads'); const outputsDir = path.join(__dirname, 'outputs'); if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true }); if (!fs.existsSync(outputsDir)) fs.mkdirSync(outputsDir, { recursive: true }); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`) }); const upload = multer({ storage: storage, limits: { fileSize: 50 * 1024 * 1024 } }); const mashupStorage = new Map(); const INSTRUMENTS = { sad_piano: { name: "Sad Piano", mood: "sad", command: "aevalsrc='0.04*sin(2*PI*(220+3*sin(0.05*t))*t)*exp(-0.12*mod(t,4.5))':d=12:s=44100,aecho=0.6:0.8:500:0.3,lowpass=f=3500" }, melancholic_violin: { name: "Melancholic Violin", mood: "sad", command: "aevalsrc='0.04*sin(2*PI*330*t)*sin(2*PI*0.25*t)*exp(-0.08*mod(t,5.5))':d=12:s=44100,vibrato=f=0.12:d=0.4,aecho=0.5:0.75:450:0.25" }, romantic_piano: { name: "Romantic Piano", mood: "romantic", command: "aevalsrc='0.04*(sin(2*PI*330*t)+sin(2*PI*415*t))*exp(-0.1*mod(t,4))':d=12:s=44100,aecho=0.5:0.7:400:0.3,chorus=0.25:0.5:30:0.2:0.12:2" }, bittersweet_piano: { name: "Bittersweet Piano", mood: "sad_romantic", command: "aevalsrc='0.04*(sin(2*PI*247*t)+sin(2*PI*330*t))*exp(-0.12*mod(t,4.5))*(1+0.2*sin(0.4*t))':d=12:s=44100,aecho=0.6:0.8:450:0.3,vibrato=f=0.1:d=0.35" }, dreamy_pad: { name: "Dreamy Pad", mood: "ambient", command: "aevalsrc='0.04*(sin(2*PI*165*t)+sin(2*PI*220*t)+sin(2*PI*330*t))':d=12:s=44100,aphaser=in_gain=0.3:out_gain=0.6:delay=2.5:decay=0.35:speed=0.2,aecho=0.4:0.6:350:0.25" }, lofi_guitar: { name: "LoFi Guitar", mood: "chill", command: "aevalsrc='0.04*sin(2*PI*196*t)*exp(-0.25*mod(t,3.5))*(1+0.1*random(0))':d=12:s=44100,chorus=0.4:0.6:35:0.22:0.13:2,lowpass=f=4200" }, bright_synth: { name: "Bright Synth", mood: "upbeat", command: "aevalsrc='0.04*(sin(2*PI*440*t)+sin(2*PI*554*t)+sin(2*PI*659*t))':d=12:s=44100,chorus=0.5:0.7:45:0.32:0.23:2,highpass=f=220" } }; // ✅ UPDATED EFFECTS - Added "slowed" + Improved "bass_boosted" const EFFECTS = { lofi: { name: "LoFi Hip Hop", description: "Vintage warmth with jazzy chill vibes (No Instruments)", baseCommand: `asetrate=44100*0.90,aresample=44100,bass=g=5:f=85:width_type=h:width=100,treble=g=-3:f=8000,equalizer=f=200:t=q:width=1.5:g=-2,equalizer=f=1000:t=q:width=2:g=2,acompressor=threshold=-20dB:ratio=3:attack=10:release=120:makeup=3,vibrato=f=0.2:d=0.4,highpass=f=70,lowpass=f=4500,volume=1.3`, instrumentVolume: 0, instrumentMoods: ["chill", "sad_romantic", "ambient"] }, // ✅ NEW: Simple Slowed Effect (just makes song slower, no reverb) slowed: { name: "Slowed", description: "Just slowed down - pure slow tempo (No instruments, no reverb)", baseCommand: `atempo=0.80,asetrate=44100*0.90,aresample=44100,equalizer=f=800:t=q:width=2:g=1.5,highpass=f=35,lowpass=f=14000,volume=1.15`, instrumentVolume: 0, instrumentMoods: [] }, slowed_reverb: { name: "Slowed + Reverb", description: "Simple & authentic - like YouTube/TikTok (clean vocals, no instruments)", baseCommand: `atempo=0.85,asetrate=44100*0.92,aresample=44100,equalizer=f=1000:t=q:width=2:g=2,equalizer=f=2500:t=q:width=2.5:g=2.5,equalizer=f=8000:t=q:width=4:g=-3,aecho=0.8:0.88:400:0.35,aecho=0.7:0.78:600:0.25,highpass=f=40,lowpass=f=12000,volume=1.2`, instrumentVolume: 0, instrumentMoods: [] }, nightcore: { name: "Nightcore", description: "High energy with bright sparkling sounds (With Instruments)", baseCommand: `atempo=1.30,asetrate=44100*1.18,aresample=44100,treble=g=6:f=5000:width_type=h:width=3000,bass=g=-3:f=100,equalizer=f=2000:t=q:width=2:g=2.5,equalizer=f=6000:t=q:width=3:g=4,equalizer=f=10000:t=q:width=4:g=5,acompressor=threshold=-18dB:ratio=3:attack=3:release=30:makeup=4,highpass=f=120,volume=1.4`, instrumentVolume: 0.04, instrumentMoods: ["upbeat", "romantic"] }, vaporwave: { name: "Vaporwave", description: "Retro nostalgic with dreamy sad vibes (With Instruments)", baseCommand: `asetrate=44100*0.82,aresample=44100,equalizer=f=300:t=q:width=2:g=4,equalizer=f=1500:t=q:width=3:g=-2,equalizer=f=6000:t=q:width=4:g=-4,aphaser=in_gain=0.5:out_gain=0.75:delay=4:decay=0.5:speed=0.2,chorus=0.6:0.85:55:0.4:0.25:2,vibrato=f=0.12:d=0.5,aecho=0.5:0.65:180:0.35,acompressor=threshold=-20dB:ratio=2.5:attack=12:release=150:makeup=2,highpass=f=70,lowpass=f=5500,volume=1.2`, instrumentVolume: 0.005, instrumentMoods: ["sad_romantic", "ambient", "sad"] }, "8d_audio": { name: "8D Audio", description: "Immersive 360° with ambient dreamy layers (With Instruments)", baseCommand: `apulsator=hz=0.08:mode=sine:width=0.9,extrastereo=m=3:c=1,aecho=0.7:0.85:100:0.35,haas=level_in=1:level_out=1.1:side_gain=0.9:middle_source=mid,bass=g=3:f=85,acompressor=threshold=-18dB:ratio=2:attack=10:release=100:makeup=2,highpass=f=60,volume=1.2`, instrumentVolume: 0.05, instrumentMoods: ["ambient", "romantic", "sad_romantic"] }, // ✅ IMPROVED: Bass Boosted (smooth transitions, no noise, dynamic pulsing) bass_boosted: { name: "Bass Boosted", description: "Smooth heavy bass with pulsing rhythm (Clean, no distortion)", baseCommand: `bass=g=16:f=50:width_type=o:width=1.8,bass=g=12:f=90:width_type=h:width=100,equalizer=f=35:t=q:width=1:g=10,equalizer=f=70:t=q:width=1.5:g=8,equalizer=f=130:t=q:width=2:g=6,equalizer=f=450:t=q:width=2:g=-2.5,equalizer=f=3500:t=q:width=3.5:g=-4.5,acompressor=threshold=-24dB:ratio=6:attack=4:release=60:makeup=6,apulsator=hz=0.18:mode=sine:width=0.6,highpass=f=25,alimiter=limit=0.92:attack=6:release=65,volume=1.45`, instrumentVolume: 0.05, instrumentMoods: ["ambient", "chill"] }, ambient: { name: "Ambient Chill", description: "Ethereal meditation with sad peaceful vibes (With Instruments)", baseCommand: `asetrate=44100*0.94,aresample=44100,aecho=0.6:0.8:200:0.4,aecho=0.5:0.7:400:0.3,chorus=0.6:0.85:50:0.4:0.25:2,aphaser=in_gain=0.5:out_gain=0.8:delay=3:decay=0.4:speed=0.2,equalizer=f=1500:t=q:width=3:g=-2,equalizer=f=6000:t=q:width=4:g=-4,acompressor=threshold=-18dB:ratio=2:attack=15:release=200:makeup=3,highpass=f=50,lowpass=f=9000,volume=1.5`, instrumentVolume: 0.06, instrumentMoods: ["ambient", "sad", "sad_romantic"] } }; async function analyzeAudioFeatures(filePath) { try { console.log(' 🔍 Analyzing audio features...'); const bpm = await detectBPMAdvanced(filePath); const spectrum = await analyzeFrequencySpectrum(filePath); const rhythm = await analyzeRhythmPattern(filePath); const loudness = await analyzeLoudness(filePath); return { bpm, spectrum, rhythm, loudness }; } catch (error) { console.error(' ⚠️ Audio analysis error:', error.message); return null; } } async function detectBPMAdvanced(filePath) { try { const { stdout } = await execPromise( `ffmpeg -i "${filePath}" -t 30 -af "asetnsamples=2048,astats=metadata=1:reset=1,ametadata=print:key=lavfi.astats.Overall.RMS_level:file=-" -f null - 2>&1 | grep "pts_time" | wc -l` ); const beatCount = parseInt(stdout.trim()) || 0; let bpm = Math.round((beatCount / 30) * 60); if (bpm < 60) bpm = bpm * 2; if (bpm > 180) bpm = bpm / 2; if (bpm < 60) bpm = 120; if (bpm > 200) bpm = 140; console.log(` ✅ BPM detected: ${bpm}`); return bpm; } catch (error) { console.log(' ⚠️ BPM detection failed, using default 120'); return 120; } } async function analyzeFrequencySpectrum(filePath) { try { const bands = { subBass: { low: 20, high: 60 }, bass: { low: 60, high: 250 }, lowMid: { low: 250, high: 500 }, mid: { low: 500, high: 2000 }, highMid: { low: 2000, high: 4000 }, high: { low: 4000, high: 8000 } }; const spectrum = {}; for (const [name, range] of Object.entries(bands)) { const { stdout } = await execPromise( `ffmpeg -i "${filePath}" -t 10 -af "bandpass=f=${(range.low + range.high) / 2}:width_type=h:w=${range.high - range.low},volumedetect" -f null - 2>&1 | grep "mean_volume" || echo "mean_volume: -30.0 dB"` ); const match = stdout.match(/mean_volume:\s*([-\d.]+)/); spectrum[name] = match ? parseFloat(match[1]) : -30.0; } console.log(` ✅ Spectrum: Bass=${spectrum.bass.toFixed(1)}dB, Mid=${spectrum.mid.toFixed(1)}dB`); return spectrum; } catch (error) { console.log(' ⚠️ Spectrum analysis failed'); return { subBass: -25, bass: -20, lowMid: -18, mid: -15, highMid: -18, high: -20 }; } } async function analyzeRhythmPattern(filePath) { try { const { stdout } = await execPromise( `ffmpeg -i "${filePath}" -t 20 -af "highpass=f=80,lowpass=f=250,compand=attacks=0.3:decays=0.8:points=-80/-80|-45/-15|-27/-9|0/-7|20/-7,volume=2" -f null - 2>&1 | grep "size=" || echo "size=0kB"` ); const sizeMatch = stdout.match(/size=\s*(\d+)kB/); const percussiveEnergy = sizeMatch ? parseInt(sizeMatch[1]) : 0; const isHighlyRhythmic = percussiveEnergy > 500; const isModerateRhythm = percussiveEnergy > 200; console.log(` ✅ Rhythm: ${isHighlyRhythmic ? 'High' : isModerateRhythm ? 'Moderate' : 'Low'}`); return { percussiveEnergy, isHighlyRhythmic, isModerateRhythm }; } catch (error) { console.log(' ⚠️ Rhythm analysis failed'); return { percussiveEnergy: 250, isHighlyRhythmic: false, isModerateRhythm: true }; } } async function analyzeLoudness(filePath) { try { const { stdout } = await execPromise( `ffmpeg -i "${filePath}" -t 15 -af "ebur128=framelog=verbose" -f null - 2>&1 | grep "I:" | tail -1 || echo "I: -20.0 LUFS"` ); const match = stdout.match(/I:\s*([-\d.]+)/); const lufs = match ? parseFloat(match[1]) : -20.0; console.log(` ✅ Loudness: ${lufs.toFixed(1)} LUFS`); return lufs; } catch (error) { console.log(' ⚠️ Loudness analysis failed'); return -20.0; } } async function detectGenre(filePath) { try { console.log(' 🎭 Starting genre detection...'); const features = await analyzeAudioFeatures(filePath); if (!features) { return getDefaultGenreInfo(); } const { bpm, spectrum, rhythm, loudness } = features; const scores = { hiphop: 0, pop: 0, rock: 0, edm: 0, metal: 0, electronic: 0, rnb: 0, country: 0 }; if (bpm >= 70 && bpm <= 110) scores.hiphop += 30; if (spectrum.bass > -15) scores.hiphop += 25; if (spectrum.subBass > -20) scores.hiphop += 20; if (rhythm.isHighlyRhythmic) scores.hiphop += 25; if (bpm >= 100 && bpm <= 130) scores.pop += 30; if (spectrum.mid > -12) scores.pop += 30; if (Math.abs(spectrum.bass - spectrum.mid) < 8) scores.pop += 20; if (loudness > -12) scores.pop += 20; if (bpm >= 110 && bpm <= 150) scores.rock += 25; if (spectrum.highMid > -15) scores.rock += 30; if (spectrum.mid > -15) scores.rock += 20; if (rhythm.isModerateRhythm) scores.rock += 25; if (bpm >= 120 && bpm <= 140) scores.edm += 30; if (bpm >= 140 && bpm <= 180) scores.edm += 20; if (spectrum.bass > -12) scores.edm += 30; if (rhythm.isHighlyRhythmic) scores.edm += 30; if (spectrum.subBass > -15) scores.edm += 10; if (bpm >= 140 && bpm <= 200) scores.metal += 35; if (spectrum.highMid > -10) scores.metal += 25; if (spectrum.high > -15) scores.metal += 20; if (loudness > -10) scores.metal += 20; if (bpm >= 100 && bpm <= 130) scores.electronic += 20; if (spectrum.high > -15) scores.electronic += 25; if (rhythm.isHighlyRhythmic) scores.electronic += 25; if (spectrum.bass > -15) scores.electronic += 20; if (bpm >= 70 && bpm <= 100) scores.rnb += 30; if (spectrum.mid > -10) scores.rnb += 35; if (spectrum.bass > -18) scores.rnb += 20; if (!rhythm.isHighlyRhythmic && rhythm.isModerateRhythm) scores.rnb += 15; if (bpm >= 90 && bpm <= 120) scores.country += 25; if (spectrum.mid > -12) scores.country += 25; if (spectrum.highMid > -18) scores.country += 25; if (spectrum.bass < -20) scores.country += 15; let detectedGenre = 'pop'; let maxScore = 0; for (const [genre, score] of Object.entries(scores)) { if (score > maxScore) { maxScore = score; detectedGenre = genre; } } let confidence = 'low'; if (maxScore >= 80) confidence = 'high'; else if (maxScore >= 60) confidence = 'medium'; console.log(` ✅ DETECTED: ${detectedGenre.toUpperCase()} (${confidence} confidence, score: ${maxScore})`); return { genre: detectedGenre, confidence, bpm, bassLevel: parseFloat(spectrum.bass.toFixed(1)), characteristics: getGenreCharacteristics(detectedGenre), debug: { scores, maxScore, spectrum: { bass: spectrum.bass.toFixed(1), mid: spectrum.mid.toFixed(1), high: spectrum.high.toFixed(1) }, rhythm: rhythm.isHighlyRhythmic ? 'high' : rhythm.isModerateRhythm ? 'moderate' : 'low' } }; } catch (error) { console.error(' ❌ Genre detection error:', error); return getDefaultGenreInfo(); } } function getDefaultGenreInfo() { return { genre: 'pop', confidence: 'low', bpm: 120, bassLevel: -20, characteristics: 'Unable to analyze - using default', debug: { error: 'Analysis failed' } }; } function getGenreCharacteristics(genre) { const characteristics = { pop: 'Catchy melodies, balanced mix, radio-friendly', rock: 'Guitar-driven, energetic, band-focused', hiphop: 'Heavy bass, rap vocals, rhythmic beats', edm: 'Electronic beats, drops, high energy', metal: 'Aggressive, distorted, very fast tempo', electronic: 'Synthetic sounds, experimental, varied', rnb: 'Smooth vocals, soul influence, groove-based', country: 'Acoustic guitar, storytelling, folk roots' }; return characteristics[genre] || 'Unknown style'; } function checkGenreCompatibility(genre1Info, genre2Info) { const compatibilityMatrix = { pop: { pop: 95, rock: 70, hiphop: 65, edm: 75, metal: 30, electronic: 60, rnb: 70, country: 55 }, rock: { pop: 70, rock: 95, hiphop: 45, edm: 50, metal: 85, electronic: 55, rnb: 50, country: 60 }, hiphop: { pop: 65, rock: 45, hiphop: 95, edm: 70, metal: 25, electronic: 65, rnb: 80, country: 35 }, edm: { pop: 75, rock: 50, hiphop: 70, edm: 95, metal: 40, electronic: 85, rnb: 60, country: 40 }, metal: { pop: 30, rock: 85, hiphop: 25, edm: 40, metal: 95, electronic: 45, rnb: 25, country: 30 }, electronic: { pop: 60, rock: 55, hiphop: 65, edm: 85, metal: 45, electronic: 95, rnb: 55, country: 45 }, rnb: { pop: 70, rock: 50, hiphop: 80, edm: 60, metal: 25, electronic: 55, rnb: 95, country: 50 }, country: { pop: 55, rock: 60, hiphop: 35, edm: 40, metal: 30, electronic: 45, rnb: 50, country: 95 } }; const score = compatibilityMatrix[genre1Info.genre]?.[genre2Info.genre] || 50; const bpmDiff = Math.abs(genre1Info.bpm - genre2Info.bpm); let adjustedScore = score; if (bpmDiff > 40) adjustedScore -= 20; else if (bpmDiff > 20) adjustedScore -= 10; adjustedScore = Math.max(0, Math.min(100, adjustedScore)); let status, message, recommendation; if (adjustedScore >= 80) { status = 'excellent'; message = '✅ Excellent match! These songs will blend perfectly.'; recommendation = 'proceed'; } else if (adjustedScore >= 60) { status = 'good'; message = '✅ Good compatibility. Mashup will sound professional.'; recommendation = 'proceed'; } else if (adjustedScore >= 40) { status = 'risky'; message = '⚠️ Moderate compatibility. Result may sound experimental.'; recommendation = 'caution'; } else { status = 'poor'; message = '❌ Poor compatibility. Strongly recommend changing one song.'; recommendation = 'change_songs'; } return { score: Math.round(adjustedScore), status, message, recommendation, bpmDifference: bpmDiff, details: { genreMatch: score >= 70 ? 'compatible' : score >= 50 ? 'moderate' : 'incompatible', tempoMatch: bpmDiff <= 10 ? 'excellent' : bpmDiff <= 20 ? 'good' : 'poor' } }; } function getSmartInstruments(effectType, count = 2) { const effect = EFFECTS[effectType]; if (!effect || !effect.instrumentMoods) { const instrumentKeys = Object.keys(INSTRUMENTS); return instrumentKeys.sort(() => Math.random() - 0.5).slice(0, count); } const matchingInstruments = Object.keys(INSTRUMENTS).filter(key => effect.instrumentMoods.includes(INSTRUMENTS[key].mood) ); return matchingInstruments.sort(() => Math.random() - 0.5).slice(0, count); } async function getAudioDuration(filePath) { try { const { stdout } = await execPromise( `ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filePath}"` ); return parseFloat(stdout.trim()); } catch (error) { console.error('Duration detection error:', error); return 180; } } function generateSections(duration) { const sectionLength = 15; const sections = []; for (let start = 0; start < duration; start += sectionLength) { const end = Math.min(start + sectionLength, duration); const index = sections.length; sections.push({ index, type: index === 0 ? 'intro' : index === 1 ? 'verse' : index % 2 === 0 ? 'chorus' : 'verse', startTime: parseFloat(start.toFixed(2)), endTime: parseFloat(end.toFixed(2)), duration: parseFloat((end - start).toFixed(2)) }); } return sections; } function parseCrossfadeDuration(transitions) { if (!transitions) return 3.0; if (transitions === 'smooth') return 3.0; if (transitions === 'none') return 0; const match = transitions.match(/(\d+\.?\d*)/); if (match) { const duration = parseFloat(match[1]); return Math.max(0, Math.min(10, duration)); } return 3.0; } async function matchTempo(inputPath, targetBPM, currentBPM, identifier) { try { if (Math.abs(targetBPM - currentBPM) < 3) { console.log(` ⏭️ Tempo match skipped (${currentBPM} ≈ ${targetBPM})`); return inputPath; } const ratio = targetBPM / currentBPM; const outputPath = path.join(outputsDir, `tempo-${identifier}.wav`); console.log(` 🎵 Matching tempo: ${currentBPM} → ${targetBPM} BPM (ratio: ${ratio.toFixed(3)})`); let tempoFilter = ''; if (ratio <= 0.5) { tempoFilter = 'atempo=0.5,atempo=' + (ratio / 0.5).toFixed(3); } else if (ratio >= 2.0) { tempoFilter = 'atempo=2.0,atempo=' + (ratio / 2.0).toFixed(3); } else { tempoFilter = `atempo=${ratio.toFixed(3)}`; } await execPromise( `ffmpeg -i "${inputPath}" -af "${tempoFilter}" -ar 44100 -y "${outputPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); if (!fs.existsSync(outputPath)) { throw new Error('Tempo matched file not created'); } console.log(` ✅ Tempo matched successfully`); return outputPath; } catch (error) { console.error(` ⚠️ Tempo matching failed: ${error.message}`); return inputPath; } } // API ENDPOINTS app.get('/', (req, res) => { res.json({ status: 'online', message: '🎵 Unified Mashup API v15.0 - NEW SLOWED EFFECT', version: '15.0', features: [ '🎭 Smart Genre Detection (8 genres)', '🎬 CapCut-style Timeline Editor', '🎨 8 Audio Effects (NEW: Pure Slowed)', '🔀 Natural Crossfade', '🎹 Smart Instrument Mixing', '🎵 BPM Detection & Tempo Matching', '🎯 Genre Compatibility Check', '📊 Frequency Spectrum Analysis', '✂️ Precise Clip Trimming', '🔊 Volume Control per Clip', '🔥 NEW: Pure Slowed Effect (just slow tempo)', '🎚️ IMPROVED: Clean Bass Boost (smooth pulsing)', '❌ Zero Quality Loss' ], total_effects: Object.keys(EFFECTS).length, total_instruments: Object.keys(INSTRUMENTS).length, note: 'NEW: Slowed effect added! Bass boost improved for smooth transitions!' }); }); app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), activeSessions: mashupStorage.size }); }); app.get('/effects', (req, res) => { res.json({ total: Object.keys(EFFECTS).length, effects: Object.keys(EFFECTS).map(key => ({ id: key, name: EFFECTS[key].name, description: EFFECTS[key].description, moods: EFFECTS[key].instrumentMoods || [], hasInstruments: EFFECTS[key].instrumentVolume > 0, isNew: key === 'slowed', isImproved: key === 'bass_boosted' })) }); }); app.get('/instruments', (req, res) => { res.json({ total: Object.keys(INSTRUMENTS).length, instruments: Object.keys(INSTRUMENTS).map(key => ({ id: key, name: INSTRUMENTS[key].name, mood: INSTRUMENTS[key].mood })) }); }); app.post('/process', upload.single('audio'), async (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, error: 'No audio file uploaded' }); } const effectType = req.body.effect || 'lofi'; const addInstruments = req.body.instruments !== 'false'; if (!EFFECTS[effectType]) { fs.unlinkSync(req.file.path); return res.status(400).json({ success: false, error: `Unknown effect: ${effectType}`, available_effects: Object.keys(EFFECTS) }); } const inputPath = req.file.path; const outputFilename = `${effectType}-${Date.now()}.mp3`; const outputPath = path.join(outputsDir, outputFilename); const effect = EFFECTS[effectType]; console.log(`\n🎵 Processing: ${effect.name}`); console.log(`📁 File: ${req.file.originalname}`); let ffmpegCommand; let usedInstruments = []; let analysisDetails = null; // ✅ Handle "slowed" effect (NEW) if (effectType === 'slowed') { console.log('\n🐌 Applying PURE SLOWED effect...'); console.log(' Speed: 80% (perfect slow)'); console.log(' Pitch: 90% (deeper tone)'); console.log(' NO REVERB, NO INSTRUMENTS'); console.log(' Just pure slowed tempo'); ffmpegCommand = `ffmpeg -i "${inputPath}" -threads 0 -af "${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -y "${outputPath}"`; analysisDetails = { description: 'Pure slowed tempo', speed: '80% (slower than slowed+reverb)', pitch: '90% (deeper voice)', reverb: 'NONE', instruments: 'NONE', quality: 'Simple & Clean - Just Slow' }; } // ✅ Handle "slowed_reverb" effect else if (effectType === 'slowed_reverb') { console.log('\n🔥 Applying SIMPLE Slowed + Reverb (YouTube/TikTok style)...'); console.log(' Speed: 85% (perfect slow)'); console.log(' Pitch: 92% (deep voice)'); console.log(' Vocals: Enhanced at 1000Hz & 2500Hz'); console.log(' Reverb: Clean and subtle (2 layers)'); console.log(' NO INSTRUMENTS, NO EXTRA SOUNDS'); ffmpegCommand = `ffmpeg -i "${inputPath}" -threads 0 -af "${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -y "${outputPath}"`; analysisDetails = { description: 'Simple authentic slowed+reverb', speed: '85% (not too slow)', pitch: '92% (deep feel)', vocals: 'Crystal clear (boosted at 1000Hz & 2500Hz)', reverb: 'Subtle (2 layers - 400ms & 600ms)', instruments: 'NONE', quality: 'YouTube/TikTok style - Clean & Professional' }; } // ✅ Handle "bass_boosted" effect (IMPROVED) else if (effectType === 'bass_boosted') { console.log('\n🔊 Applying IMPROVED BASS BOOST...'); console.log(' Bass: Heavy but clean (16dB @ 50Hz)'); console.log(' Pulsing: Smooth rhythm (0.18Hz sine wave)'); console.log(' Compression: Dynamic & artifact-free'); console.log(' NO NOISE, NO DISTORTION'); if (addInstruments && effect.instrumentVolume > 0) { const selectedKeys = getSmartInstruments(effectType, 2); usedInstruments = selectedKeys.map(key => ({ id: key, name: INSTRUMENTS[key].name, mood: INSTRUMENTS[key].mood })); console.log(`🎹 Instruments: ${usedInstruments.map(i => i.name).join(', ')}`); const timestamp = Date.now(); const inst1Path = path.join(outputsDir, `inst1-${timestamp}.wav`); const inst2Path = path.join(outputsDir, `inst2-${timestamp}.wav`); await execPromise(`ffmpeg -f lavfi -i "${INSTRUMENTS[selectedKeys[0]].command}" -t 12 -ar 44100 -y "${inst1Path}"`); await execPromise(`ffmpeg -f lavfi -i "${INSTRUMENTS[selectedKeys[1]].command}" -t 12 -ar 44100 -y "${inst2Path}"`); const vol = effect.instrumentVolume; ffmpegCommand = `ffmpeg -i "${inputPath}" -stream_loop -1 -i "${inst1Path}" -stream_loop -1 -i "${inst2Path}" -filter_complex "[0:a]volume=3.5[main];[1:a]volume=${vol}[i1];[2:a]volume=${vol}[i2];[main][i1][i2]amix=inputs=3:duration=first:dropout_transition=2:weights=15 0.5 0.5,${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -threads 0 -y "${outputPath}"`; setTimeout(() => { [inst1Path, inst2Path].forEach(p => { if (fs.existsSync(p)) fs.unlinkSync(p); }); }, 5000); } else { console.log('⏭️ Instruments skipped for this effect'); ffmpegCommand = `ffmpeg -i "${inputPath}" -threads 0 -af "${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -y "${outputPath}"`; } analysisDetails = { description: 'Smooth heavy bass with pulsing', bass: 'Deep & powerful (16dB @ 50Hz, 12dB @ 90Hz)', pulsing: 'Smooth sine wave rhythm (0.18Hz)', compression: 'Dynamic (6:1 ratio, clean transitions)', quality: 'No noise, no distortion - Pure bass' }; } // ✅ Handle other effects else { if (addInstruments && effect.instrumentVolume > 0) { const selectedKeys = getSmartInstruments(effectType, 2); usedInstruments = selectedKeys.map(key => ({ id: key, name: INSTRUMENTS[key].name, mood: INSTRUMENTS[key].mood })); console.log(`🎹 Instruments: ${usedInstruments.map(i => i.name).join(', ')}`); const timestamp = Date.now(); const inst1Path = path.join(outputsDir, `inst1-${timestamp}.wav`); const inst2Path = path.join(outputsDir, `inst2-${timestamp}.wav`); await execPromise(`ffmpeg -f lavfi -i "${INSTRUMENTS[selectedKeys[0]].command}" -t 12 -ar 44100 -y "${inst1Path}"`); await execPromise(`ffmpeg -f lavfi -i "${INSTRUMENTS[selectedKeys[1]].command}" -t 12 -ar 44100 -y "${inst2Path}"`); const vol = effect.instrumentVolume; ffmpegCommand = `ffmpeg -i "${inputPath}" -stream_loop -1 -i "${inst1Path}" -stream_loop -1 -i "${inst2Path}" -filter_complex "[0:a]volume=3.5[main];[1:a]volume=${vol}[i1];[2:a]volume=${vol}[i2];[main][i1][i2]amix=inputs=3:duration=first:dropout_transition=2:weights=15 0.5 0.5,${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -threads 0 -y "${outputPath}"`; setTimeout(() => { [inst1Path, inst2Path].forEach(p => { if (fs.existsSync(p)) fs.unlinkSync(p); }); }, 5000); } else { console.log('⏭️ Instruments skipped for this effect'); ffmpegCommand = `ffmpeg -i "${inputPath}" -threads 0 -af "${effect.baseCommand}" -ar 44100 -b:a 192k -preset ultrafast -y "${outputPath}"`; } } console.log('\n⚙️ Processing audio...'); const startTime = Date.now(); await execPromise(ffmpegCommand, { maxBuffer: 50 * 1024 * 1024 }); const processingTime = ((Date.now() - startTime) / 1000).toFixed(2); console.log(`✅ Processing complete in ${processingTime}s`); fs.unlinkSync(inputPath); const stats = fs.statSync(outputPath); const response = { success: true, message: `Processed with ${effect.name}`, downloadUrl: `/download/${outputFilename}`, filename: outputFilename, effect: { id: effectType, name: effect.name, description: effect.description, moods: effect.instrumentMoods || [], hasInstruments: effect.instrumentVolume > 0, isNew: effectType === 'slowed', isImproved: effectType === 'bass_boosted' }, instruments: usedInstruments, output: { size_kb: (stats.size / 1024).toFixed(2), size_mb: (stats.size / 1024 / 1024).toFixed(2) }, processing_time_seconds: parseFloat(processingTime) }; if (analysisDetails) { response.analysis = analysisDetails; } console.log('\n═══════════════════════════════════════════════════════════'); console.log('✅ SUCCESS - File ready for download'); console.log('═══════════════════════════════════════════════════════════\n'); res.json(response); } catch (error) { console.error('\n❌ Processing error:', error); if (req.file && fs.existsSync(req.file.path)) { fs.unlinkSync(req.file.path); } res.status(500).json({ success: false, error: error.message, details: 'Check server logs for more information' }); } }); app.post('/mashup/upload', upload.array('songs', 2), async (req, res) => { try { if (!req.files || req.files.length !== 2) { return res.status(400).json({ success: false, error: 'Please upload exactly 2 songs' }); } const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const songs = req.files.map((file, index) => ({ id: `song${index + 1}`, path: file.path, filename: file.originalname, size: (file.size / 1024 / 1024).toFixed(2) + ' MB' })); mashupStorage.set(sessionId, { songs, createdAt: new Date(), analyzed: false, status: 'uploaded' }); console.log(`✅ Session created: ${sessionId}`); res.json({ success: true, sessionId, songs: songs.map(s => ({ id: s.id, filename: s.filename, size: s.size })), message: 'Songs uploaded successfully. Next: analyze songs.' }); } catch (error) { console.error('Upload error:', error); res.status(500).json({ success: false, error: error.message }); } }); app.post('/mashup/analyze/:sessionId', async (req, res) => { try { const { sessionId } = req.params; const session = mashupStorage.get(sessionId); if (!session) { return res.status(404).json({ success: false, error: 'Session not found' }); } console.log('\n═══════════════════════════════════════════════════════════'); console.log(`🔬 ANALYZING SESSION: ${sessionId}`); console.log('═══════════════════════════════════════════════════════════'); const analyzedSongs = []; const genreInfos = []; for (const song of session.songs) { const timestamp = Date.now() + Math.random() * 1000; console.log(`\n📀 Processing ${song.id}: ${song.filename}`); const duration = await getAudioDuration(song.path); console.log(` ⏱️ Duration: ${duration.toFixed(2)}s`); const genreInfo = await detectGenre(song.path); genreInfos.push(genreInfo); console.log(` 🎤 Extracting vocals...`); const vocalsPath = path.join(outputsDir, `vocals-${song.id}-${timestamp}.wav`); await execPromise( `ffmpeg -i "${song.path}" -af "pan=mono|c0=0.5*c0+0.5*c1,highpass=f=250,lowpass=f=4000,equalizer=f=1000:t=q:width=2:g=3,volume=1.3" -ar 44100 -y "${vocalsPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); console.log(' ✅ Vocals extracted'); console.log(' 🎸 Extracting instruments...'); const instrumentsPath = path.join(outputsDir, `instruments-${song.id}-${timestamp}.wav`); await execPromise( `ffmpeg -i "${song.path}" -af "extrastereo=m=2.8,equalizer=f=1000:t=q:width=3:g=-4,highpass=f=40,lowpass=f=15000,volume=1.1" -ar 44100 -y "${instrumentsPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); console.log(' ✅ Instruments extracted'); const sections = generateSections(duration); analyzedSongs.push({ id: song.id, filename: song.filename, duration: parseFloat(duration.toFixed(2)), bpm: genreInfo.bpm, genre: genreInfo.genre, genreConfidence: genreInfo.confidence, bassLevel: genreInfo.bassLevel, genreCharacteristics: genreInfo.characteristics, vocals: vocalsPath, instruments: instrumentsPath, sections }); } const avgBPM = Math.round((genreInfos[0].bpm + genreInfos[1].bpm) / 2); const compatibility = checkGenreCompatibility(genreInfos[0], genreInfos[1]); console.log(`\n🎯 Compatibility: ${compatibility.score}% (${compatibility.status})`); session.analyzed = true; session.analyzedData = analyzedSongs; session.targetBPM = avgBPM; session.compatibility = compatibility; session.genreInfos = genreInfos; session.status = 'analyzed'; mashupStorage.set(sessionId, session); console.log('\n═══════════════════════════════════════════════════════════'); console.log('✅ ANALYSIS COMPLETE'); console.log('═══════════════════════════════════════════════════════════\n'); res.json({ success: true, message: 'Songs analyzed successfully', targetBPM: avgBPM, compatibility: compatibility, songs: analyzedSongs.map(s => ({ id: s.id, filename: s.filename, duration: s.duration, bpm: s.bpm, genre: s.genre, genreConfidence: s.genreConfidence, genreCharacteristics: s.genreCharacteristics, sections: s.sections })), warnings: compatibility.recommendation === 'change_songs' ? [{ type: 'genre_mismatch', severity: 'high', message: compatibility.message, suggestion: `Song 1 is ${genreInfos[0].genre.toUpperCase()} (${genreInfos[0].bpm} BPM), Song 2 is ${genreInfos[1].genre.toUpperCase()} (${genreInfos[1].bpm} BPM). Consider uploading songs from similar genres.` }] : compatibility.recommendation === 'caution' ? [{ type: 'moderate_compatibility', severity: 'medium', message: compatibility.message, suggestion: 'Mashup will work but may sound experimental.' }] : [], nextStep: 'create' }); } catch (error) { console.error('\n❌ Analysis error:', error); res.status(500).json({ success: false, error: error.message }); } }); app.post('/mashup/create', async (req, res) => { const tempFiles = []; try { const { session_id, sessionId, arrangement, theme, transitions, forceCreate } = req.body; const actualSessionId = session_id || sessionId; console.log('\n🎨 CREATE MASHUP REQUEST'); console.log('Session ID received (snake_case):', session_id); console.log('Session ID received (camelCase):', sessionId); console.log('Using Session ID:', actualSessionId); console.log('Clips in arrangement:', arrangement?.length); if (!actualSessionId) { console.error('❌ No session ID provided!'); return res.status(400).json({ success: false, error: 'Session ID is required', received: { session_id, sessionId } }); } if (!arrangement || !Array.isArray(arrangement) || arrangement.length === 0) { return res.status(400).json({ success: false, error: 'Arrangement array is required' }); } const session = mashupStorage.get(actualSessionId); if (!session) { console.error(`❌ Session not found: ${actualSessionId}`); console.log('Available sessions:', Array.from(mashupStorage.keys())); return res.status(404).json({ success: false, error: 'Session not found', sessionId: actualSessionId }); } if (!session.analyzed || !session.analyzedData) { return res.status(400).json({ success: false, error: 'Session not analyzed. Call /mashup/analyze first.' }); } if (session.compatibility && session.compatibility.recommendation === 'change_songs' && !forceCreate) { console.warn('⚠️ Songs not compatible - rejecting'); return res.status(400).json({ success: false, error: 'Songs are not compatible', compatibility: session.compatibility, message: session.compatibility.message, suggestion: 'Upload different songs or set forceCreate: true' }); } const crossfadeDuration = parseCrossfadeDuration(transitions); const timestamp = Date.now(); const segmentPaths = []; console.log(`\n🎨 Creating mashup: ${arrangement.length} clips`); console.log(` Target BPM: ${session.targetBPM}`); console.log(` Crossfade: ${crossfadeDuration}s`); console.log(` Theme: ${theme || 'none'}`); for (let i = 0; i < arrangement.length; i++) { const section = arrangement[i]; const song = session.analyzedData.find(s => s.id === section.songId); if (!song) { console.warn(`⚠️ Skipping clip ${i}: song not found`); continue; } console.log(`\n📍 Clip ${i + 1}/${arrangement.length}: ${song.id}`); const vocalsMatched = await matchTempo( song.vocals, session.targetBPM, song.bpm, `${timestamp}-${i}-vocals` ); const instrumentsMatched = await matchTempo( song.instruments, session.targetBPM, song.bpm, `${timestamp}-${i}-inst` ); tempFiles.push(vocalsMatched, instrumentsMatched); const segmentPath = path.join(outputsDir, `segment-${timestamp}-${i}.wav`); let inputs = []; let filterParts = []; if (section.parts.includes('vocals')) { inputs.push(`-i "${vocalsMatched}"`); filterParts.push('[0:a]'); } if (section.parts.includes('instruments')) { const idx = inputs.length; inputs.push(`-i "${instrumentsMatched}"`); filterParts.push(`[${idx}:a]`); } if (inputs.length === 0) continue; const volume = section.volume || 1.0; let filterComplex = ''; if (filterParts.length > 1) { filterComplex = `${filterParts.join('')}amix=inputs=${filterParts.length}:duration=longest:normalize=0,atrim=${section.startTime}:${section.endTime},asetpts=PTS-STARTPTS,volume=${volume}`; } else { filterComplex = `${filterParts[0]}atrim=${section.startTime}:${section.endTime},asetpts=PTS-STARTPTS,volume=${volume}`; } await execPromise( `ffmpeg ${inputs.join(' ')} -filter_complex "${filterComplex}" -ar 44100 -y "${segmentPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); segmentPaths.push(segmentPath); tempFiles.push(segmentPath); console.log(` ✅ Segment created`); } if (segmentPaths.length === 0) { throw new Error('No segments created'); } console.log(`\n🔗 Joining ${segmentPaths.length} segments...`); let finalAudioPath = segmentPaths[0]; if (crossfadeDuration > 0 && segmentPaths.length > 1) { for (let i = 1; i < segmentPaths.length; i++) { const outputPath = path.join(outputsDir, `crossfade-${timestamp}-${i}.wav`); await execPromise( `ffmpeg -i "${finalAudioPath}" -i "${segmentPaths[i]}" -filter_complex "[0:a][1:a]acrossfade=d=${crossfadeDuration}:o=1:c1=tri:c2=tri" -ar 44100 -ac 2 -preset ultrafast -threads 0 -y "${outputPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); finalAudioPath = outputPath; tempFiles.push(outputPath); } } const outputFilename = `mashup-${timestamp}.mp3`; const finalPath = path.join(outputsDir, outputFilename); let masteringFilter = `equalizer=f=80:t=q:width=1.5:g=1.5,equalizer=f=5000:t=q:width=3:g=1,highpass=f=30`; if (theme && EFFECTS[theme]) { console.log(`\n🎨 Applying theme: ${EFFECTS[theme].name}`); masteringFilter = EFFECTS[theme].baseCommand + ',' + masteringFilter; } console.log(`\n🎧 Final mastering...`); await execPromise( `ffmpeg -i "${finalAudioPath}" -af "${masteringFilter}" -ar 44100 -b:a 192k -ac 2 -preset ultrafast -threads 0 -y "${finalPath}"`, { maxBuffer: 50 * 1024 * 1024 } ); const stats = fs.statSync(finalPath); console.log(`✅ Mashup created: ${outputFilename}`); setTimeout(() => { tempFiles.forEach(f => { if (fs.existsSync(f)) { try { fs.unlinkSync(f); } catch (e) {} } }); }, 15000); res.json({ success: true, message: 'Mashup created successfully', downloadUrl: `/download/${outputFilename}`, filename: outputFilename, compatibility: session.compatibility, details: { clipsUsed: arrangement.length, targetBPM: session.targetBPM, crossfadeDuration: crossfadeDuration, theme: theme || 'none', size_kb: (stats.size / 1024).toFixed(2), size_mb: (stats.size / 1024 / 1024).toFixed(2), quality: session.compatibility?.score >= 60 ? 'excellent' : 'experimental' } }); console.log('\n═══════════════════════════════════════════════════════════'); console.log('✅ MASHUP COMPLETE'); console.log('═══════════════════════════════════════════════════════════\n'); } catch (error) { console.error('\n❌ ERROR:', error); tempFiles.forEach(f => { if (fs.existsSync(f)) { try { fs.unlinkSync(f); } catch (e) {} } }); res.status(500).json({ success: false, error: error.message }); } }); app.get('/mashup/session/:sessionId', (req, res) => { try { const session = mashupStorage.get(req.params.sessionId); if (!session) { return res.status(404).json({ success: false, error: 'Session not found' }); } res.json({ success: true, session: { id: req.params.sessionId, status: session.status, analyzed: session.analyzed, targetBPM: session.targetBPM || null, compatibility: session.compatibility || null, createdAt: session.createdAt } }); } catch (error) { res.status(500).json({ success: false, error: error.message }); } }); app.get('/download/:filename', (req, res) => { const filePath = path.join(outputsDir, req.params.filename); if (!fs.existsSync(filePath)) { return res.status(404).json({ success: false, error: 'File not found' }); } res.download(filePath, req.params.filename, (err) => { if (!err) { setTimeout(() => { if (fs.existsSync(filePath)) { try { fs.unlinkSync(filePath); } catch (e) {} } }, 10000); } }); }); setInterval(() => { const now = Date.now(); for (const [sessionId, session] of mashupStorage.entries()) { if (now - session.createdAt.getTime() > 7200000) { mashupStorage.delete(sessionId); console.log(`🧹 Cleaned old session: ${sessionId}`); } } }, 600000); app.listen(PORT, '0.0.0.0', () => { console.log('\n═══════════════════════════════════════════════════════════'); console.log('🎵 UNIFIED MASHUP API v15.0 - NEW SLOWED EFFECT'); console.log('═══════════════════════════════════════════════════════════'); console.log(`📍 Server: http://0.0.0.0:${PORT}`); console.log(''); console.log('🔥 NEW FEATURES:'); console.log('═══════════════════════════════════════════════════════════'); console.log('✅ NEW: Pure "Slowed" effect (just slow tempo)'); console.log('✅ IMPROVED: Clean Bass Boost (smooth pulsing)'); console.log('✅ 8 Total effects with smart instruments'); console.log('✅ Genre detection & compatibility check'); console.log('✅ Timeline mashup editor'); console.log(''); console.log('📋 ENDPOINTS:'); console.log('═══════════════════════════════════════════════════════════'); console.log('GET / - API Info'); console.log('GET /health - Health Check'); console.log('GET /effects - List all effects (includes new slowed)'); console.log('POST /process - Single song processing'); console.log('POST /mashup/upload - Upload 2 songs'); console.log('POST /mashup/analyze/:sessionId - Analyze'); console.log('POST /mashup/create - Create mashup'); console.log('GET /download/:filename - Download'); console.log('═══════════════════════════════════════════════════════════'); console.log(''); console.log('🎨 AVAILABLE EFFECTS:'); console.log('═══════════════════════════════════════════════════════════'); console.log('1. lofi - LoFi Hip Hop'); console.log('2. slowed 🆕 - Pure Slowed (just slow tempo)'); console.log('3. slowed_reverb - Slowed + Reverb'); console.log('4. nightcore - Nightcore'); console.log('5. vaporwave - Vaporwave'); console.log('6. 8d_audio - 8D Audio'); console.log('7. bass_boosted ⭐ - Bass Boost (IMPROVED)'); console.log('8. ambient - Ambient Chill'); console.log('═══════════════════════════════════════════════════════════\n'); });