Spaces:
Running
Running
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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'); | |
| }); |