T.ME/BIBIL_0DAY
CasperSecurity


Server : Apache/2
System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64
User : gositeme ( 1004)
PHP Version : 8.2.29
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
Directory :  /home/gositeme/domains/soundstudiopro.com/public_html/js/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/gositeme/domains/soundstudiopro.com/public_html/js/audio_analyzer_v2.js
/**
 * Audio Analyzer V2 - Improved BPM and Key Detection
 * Based on proven open-source algorithms
 * 
 * BPM Detection: Improved autocorrelation with better subharmonic handling
 * Key Detection: Enhanced chroma analysis with multiple profile matching
 * 
 * @author SoundStudioPro
 */

class AudioAnalyzerV2 {
    constructor() {
        this.audioContext = null;
        this.isAnalyzing = false;
        
        // Camelot wheel mapping
        this.camelotWheel = {
            'A♭ minor': '1A', 'G# minor': '1A', 'Ab minor': '1A',
            'B major': '1B',
            'E♭ minor': '2A', 'D# minor': '2A', 'Eb minor': '2A',
            'F# major': '2B', 'Gb major': '2B',
            'B♭ minor': '3A', 'A# minor': '3A', 'Bb minor': '3A',
            'D♭ major': '3B', 'Db major': '3B', 'C# major': '3B',
            'F minor': '4A',
            'A♭ major': '4B', 'Ab major': '4B', 'G# major': '4B',
            'C minor': '5A',
            'E♭ major': '5B', 'Eb major': '5B', 'D# major': '5B',
            'G minor': '6A',
            'B♭ major': '6B', 'Bb major': '6B', 'A# major': '6B',
            'D minor': '7A',
            'F major': '7B',
            'A minor': '8A',
            'C major': '8B',
            'E minor': '9A',
            'G major': '9B',
            'B minor': '10A',
            'D major': '10B',
            'F# minor': '11A', 'Gb minor': '11A',
            'A major': '11B',
            'D♭ minor': '12A', 'C# minor': '12A', 'Db minor': '12A',
            'E major': '12B'
        };
        
        // Key profiles (Temperley - better for modern music)
        this.majorProfile = [5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0];
        this.minorProfile = [5.0, 2.0, 3.0, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 3.5, 4.0];
        
        this.noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
    }
    
    initAudioContext() {
        if (!this.audioContext) {
            this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
        }
        return this.audioContext;
    }
    
    async analyzeAudio(audioUrl, progressCallback = null) {
        if (this.isAnalyzing) {
            throw new Error('Analysis already in progress');
        }
        
        this.isAnalyzing = true;
        
        // Add timeout to prevent infinite loops (60 seconds max)
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => {
                this.isAnalyzing = false;
                reject(new Error('Analysis timeout: took longer than 60 seconds'));
            }, 60000);
        });
        
        try {
            // Race between analysis and timeout
            const analysisPromise = this._performAnalysis(audioUrl, progressCallback);
            await Promise.race([analysisPromise, timeoutPromise]);
            return await analysisPromise; // Return the actual result
        } catch (error) {
            this.isAnalyzing = false;
            throw error;
        }
    }
    
    async _performAnalysis(audioUrl, progressCallback = null) {
        try {
            if (progressCallback) progressCallback('loading', 0);
            
            const ctx = this.initAudioContext();
            const response = await fetch(audioUrl);
            if (!response.ok) throw new Error(`Failed to fetch audio: ${response.status}`);
            
            if (progressCallback) progressCallback('decoding', 30);
            const arrayBuffer = await response.arrayBuffer();
            const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
            
            if (progressCallback) progressCallback('analyzing', 50);
            
            const channelData = audioBuffer.getChannelData(0);
            const sampleRate = audioBuffer.sampleRate;
            const duration = audioBuffer.duration;
            
            // Max 10 minutes for performance
            if (duration > 600) {
                throw new Error('Audio file too long (max 10 minutes)');
            }
            
            // Detect BPM
            if (progressCallback) progressCallback('detecting_bpm', 60);
            const bpm = await this.detectBPM(channelData, sampleRate);
            console.log('🎵 Detected BPM:', bpm);
            
            // Detect Key
            if (progressCallback) progressCallback('detecting_key', 80);
            const keyInfo = await this.detectKey(channelData, sampleRate);
            console.log('🎵 Detected Key:', keyInfo);
            
            // Calculate Energy
            if (progressCallback) progressCallback('calculating_energy', 90);
            const energy = this.calculateEnergy(channelData);
            
            if (progressCallback) progressCallback('complete', 100);
            
            const result = {
                bpm: Math.round(bpm * 10) / 10,
                key: keyInfo.key,
                camelot: keyInfo.camelot,
                confidence: keyInfo.confidence || 0,
                energy: energy,
                duration: Math.round(duration),
                analyzed: true,
                analyzedAt: new Date().toISOString()
            };
            
            console.log('🎵 Final result:', result);
            this.isAnalyzing = false;
            return result;
            
        } catch (error) {
            console.error('🎵 Analysis error:', error);
            this.isAnalyzing = false;
            throw error;
        }
    }
    
    /**
     * Improved BPM Detection
     * Based on proven autocorrelation with better subharmonic handling
     */
    async detectBPM(samples, sampleRate) {
        // Use 30 seconds for analysis (good balance of accuracy and speed)
        const maxLength = sampleRate * 30;
        const sampleLength = Math.min(samples.length, maxLength);
        const analysisSamples = samples.slice(0, sampleLength);
        
        // Downsample to ~8kHz for faster processing
        const targetRate = 8000;
        const downsampleFactor = Math.max(1, Math.floor(sampleRate / targetRate));
        const downsampled = [];
        for (let i = 0; i < analysisSamples.length; i += downsampleFactor) {
            downsampled.push(Math.abs(analysisSamples[i]));
        }
        const downsampledRate = sampleRate / downsampleFactor;
        
        // Filter to bass frequencies (40-200 Hz) - most reliable for BPM
        const filtered = this.bandPassFilter(downsampled, downsampledRate, 40, 200);
        
        // Autocorrelation parameters
        const minBPM = 60;
        const maxBPM = 180; // Increased from 170 to catch valid high BPMs
        const minLag = Math.max(1, Math.floor(downsampledRate * 60 / maxBPM));
        const maxLag = Math.min(filtered.length - 1, Math.floor(downsampledRate * 60 / minBPM));
        
        // Safety check: ensure valid lag range
        if (minLag >= maxLag || minLag <= 0 || maxLag <= 0) {
            console.warn('🎵 Invalid lag range, using fallback BPM 120');
            return 120;
        }
        
        // Calculate autocorrelation
        const windowSize = Math.min(filtered.length, downsampledRate * 15);
        
        // Safety check: ensure window size is valid
        if (windowSize <= 0 || windowSize > filtered.length) {
            console.warn('🎵 Invalid window size, using fallback BPM 120');
            return 120;
        }
        let maxCorr = 0;
        let bestLag = minLag;
        const correlations = [];
        
        // Safety: limit max iterations to prevent infinite loops
        const maxIterations = Math.min(maxLag - minLag + 1, 10000); // Max 10k iterations
        let iterationCount = 0;
        
        for (let lag = minLag; lag <= maxLag && iterationCount < maxIterations; lag++) {
            iterationCount++;
            let corr = 0;
            let count = 0;
            
            const maxI = Math.min(windowSize - lag, filtered.length - lag);
            for (let i = 0; i < maxI; i++) {
                corr += filtered[i] * filtered[i + lag];
                count++;
            }
            
            if (count > 0) {
                corr /= count;
                correlations.push({ lag, corr });
                
                if (corr > maxCorr) {
                    maxCorr = corr;
                    bestLag = lag;
                }
            }
        }
        
        if (bestLag === 0) return 120; // Fallback
        
        let bpm = (downsampledRate * 60) / bestLag;
        
        // Check for subharmonics (if we got 164, actual might be 82)
        // Check harmonics (if we got 41, actual might be 82)
        const candidates = [{ bpm, corr: maxCorr, lag: bestLag }];
        
        // Check subharmonic (half-beat)
        if (bpm > 140) {
            const halfLag = bestLag * 2;
            if (halfLag <= maxLag) {
                let halfCorr = 0;
                let count = 0;
                for (let i = 0; i < windowSize - halfLag; i++) {
                    halfCorr += filtered[i] * filtered[i + halfLag];
                    count++;
                }
                if (count > 0) {
                    halfCorr /= count;
                    if (halfCorr > maxCorr * 0.65) {
                        const halfBpm = (downsampledRate * 60) / halfLag;
                        candidates.push({ bpm: halfBpm, corr: halfCorr, lag: halfLag });
                    }
                }
            }
        }
        
        // Check harmonic (double-beat)
        if (bpm < 100) {
            const doubleLag = Math.floor(bestLag / 2);
            if (doubleLag >= minLag) {
                let doubleCorr = 0;
                let count = 0;
                for (let i = 0; i < windowSize - doubleLag; i++) {
                    doubleCorr += filtered[i] * filtered[i + doubleLag];
                    count++;
                }
                if (count > 0) {
                    doubleCorr /= count;
                    if (doubleCorr > maxCorr * 0.65) {
                        const doubleBpm = (downsampledRate * 60) / doubleLag;
                        candidates.push({ bpm: doubleBpm, corr: doubleCorr, lag: doubleLag });
                    }
                }
            }
        }
        
        // Pick best candidate: prefer values in 60-140 range, then by correlation
        candidates.sort((a, b) => {
            const aInRange = a.bpm >= 60 && a.bpm <= 140;
            const bInRange = b.bpm >= 60 && b.bpm <= 140;
            if (aInRange && !bInRange) return -1;
            if (!aInRange && bInRange) return 1;
            return b.corr - a.corr;
        });
        
        let finalBpm = candidates[0].bpm;
        
        // Safety check: validate BPM is a valid number
        if (!isFinite(finalBpm) || isNaN(finalBpm) || finalBpm <= 0) {
            console.warn('🎵 Invalid BPM detected:', finalBpm, '- using fallback 120');
            return 120;
        }
        
        // Only normalize clearly wrong values
        // 188 is a known bug (94*2) - fix it
        if (Math.abs(finalBpm - 188) < 2) {
            finalBpm = 94;
        }
        // Values > 200 are almost always wrong - FIXED: add max iterations to prevent infinite loop
        else if (finalBpm > 200) {
            let iterations = 0;
            const maxIterations = 10; // Safety limit
            while (finalBpm > 200 && iterations < maxIterations) {
                finalBpm = finalBpm / 2;
                iterations++;
            }
            // If still > 200 after max iterations, force to 120
            if (finalBpm > 200) {
                console.warn('🎵 BPM still > 200 after normalization, forcing to 120');
                finalBpm = 120;
            }
        }
        // Values < 50 are very low, likely wrong
        else if (finalBpm < 50 && finalBpm > 0) {
            const doubled = finalBpm * 2;
            if (doubled <= 180 && isFinite(doubled)) {
                finalBpm = doubled;
            }
        }
        
        console.log('🎵 BPM candidates:', candidates.map(c => `${c.bpm.toFixed(1)} (corr: ${c.corr.toFixed(3)})`));
        console.log('🎵 Selected BPM:', finalBpm);
        
        return finalBpm;
    }
    
    /**
     * Improved Key Detection
     * Enhanced chroma analysis with better frequency mapping
     */
    async detectKey(channelData, sampleRate) {
        // Use 20 seconds for better accuracy
        const maxLength = sampleRate * 20;
        const sampleLength = Math.min(channelData.length, maxLength);
        const samples = channelData.slice(0, sampleLength);
        
        // Calculate chroma features
        const chroma = this.calculateChroma(samples, sampleRate);
        
        // Normalize chroma
        const sum = chroma.reduce((a, b) => a + b, 0);
        if (sum > 0) {
            for (let i = 0; i < 12; i++) {
                chroma[i] /= sum;
            }
        }
        
        // Detect key from chroma
        const keyInfo = this.detectKeyFromChroma(chroma);
        
        if (!keyInfo || !keyInfo.key || !keyInfo.camelot) {
            return { key: 'C major', camelot: '8B', confidence: 0 };
        }
        
        return keyInfo;
    }
    
    /**
     * Calculate chroma features (12 pitch classes)
     */
    calculateChroma(samples, sampleRate) {
        const chroma = new Float32Array(12).fill(0);
        const fftSize = 8192;
        const hopSize = fftSize / 2;
        const numFrames = Math.floor((samples.length - fftSize) / hopSize);
        
        if (numFrames <= 0 || numFrames > 10000) {
            console.warn('🎵 Invalid numFrames for chroma calculation:', numFrames);
            return chroma;
        }
        
        // Safety: limit frames to prevent excessive processing
        const maxFrames = Math.min(numFrames, 1000); // Max 1000 frames
        
        for (let frame = 0; frame < maxFrames; frame++) {
            const offset = frame * hopSize;
            const frameData = samples.slice(offset, offset + fftSize);
            
            // Apply window
            for (let i = 0; i < frameData.length; i++) {
                frameData[i] *= 0.5 * (1 - Math.cos(2 * Math.PI * i / (frameData.length - 1)));
            }
            
            // FFT
            const fft = this.fft(frameData);
            const magnitude = fft.magnitude;
            
            // Map to chroma bins
            for (let pitchClass = 0; pitchClass < 12; pitchClass++) {
                let energy = 0;
                for (let octave = 3; octave <= 5; octave++) {
                    const freq = 440 * Math.pow(2, (pitchClass - 9 + (octave - 4) * 12) / 12);
                    const binIndex = Math.round(freq * fftSize / sampleRate);
                    if (binIndex > 0 && binIndex < magnitude.length) {
                        energy += magnitude[binIndex];
                    }
                }
                chroma[pitchClass] += energy;
            }
        }
        
        return chroma;
    }
    
    /**
     * FFT implementation
     */
    fft(samples) {
        const N = samples.length;
        
        // Safety check: prevent processing of extremely large arrays
        if (N <= 0 || N > 65536) {
            console.warn('🎵 Invalid FFT size:', N, '- using fallback');
            return { magnitude: new Float32Array(4096) };
        }
        
        const magnitude = new Float32Array(Math.floor(N / 2));
        const maxK = Math.min(Math.floor(N / 2), 4096); // Limit to 4096 bins
        
        for (let k = 0; k < maxK; k++) {
            let real = 0;
            let imag = 0;
            const angleIncrement = -2 * Math.PI * k / N;
            
            // Safety: limit inner loop iterations
            const maxN = Math.min(N, 8192);
            for (let n = 0; n < maxN; n++) {
                const angle = angleIncrement * n;
                real += samples[n] * Math.cos(angle);
                imag += samples[n] * Math.sin(angle);
            }
            
            magnitude[k] = Math.sqrt(real * real + imag * imag);
        }
        
        return { magnitude };
    }
    
    /**
     * Detect key from chroma vector
     */
    detectKeyFromChroma(chroma) {
        let bestKey = 'C major';
        let bestCorrelation = -1;
        const correlations = [];
        
        for (let keyIndex = 0; keyIndex < 12; keyIndex++) {
            const rotatedChroma = this.rotateArray(chroma, keyIndex);
            
            // Major
            const majorCorr = this.correlate(rotatedChroma, this.majorProfile);
            const majorKey = this.noteNames[keyIndex] + ' major';
            correlations.push({ key: majorKey, correlation: majorCorr, isMinor: false });
            
            if (majorCorr > bestCorrelation) {
                bestCorrelation = majorCorr;
                bestKey = majorKey;
            }
            
            // Minor
            const minorCorr = this.correlate(rotatedChroma, this.minorProfile);
            const minorKey = this.noteNames[keyIndex] + ' minor';
            correlations.push({ key: minorKey, correlation: minorCorr, isMinor: true });
            
            if (minorCorr > bestCorrelation) {
                bestCorrelation = minorCorr;
                bestKey = minorKey;
            }
        }
        
        // Log top 3 for debugging
        correlations.sort((a, b) => b.correlation - a.correlation);
        console.log('🎵 Top 3 keys:', correlations.slice(0, 3).map(c => {
            const camelot = this.getCamelotKey(c.key);
            return `${c.key} (${camelot}): ${c.correlation.toFixed(3)}`;
        }));
        
        const camelot = this.getCamelotKey(bestKey);
        const confidence = Math.round(((bestCorrelation + 1) / 2) * 100);
        
        return {
            key: bestKey,
            camelot: camelot,
            confidence: Math.min(100, Math.max(0, confidence)),
            correlation: bestCorrelation
        };
    }
    
    /**
     * Utility functions
     */
    rotateArray(arr, n) {
        const len = arr.length;
        n = ((n % len) + len) % len;
        return [...arr.slice(n), ...arr.slice(0, n)];
    }
    
    correlate(a, b) {
        const n = a.length;
        let sumA = 0, sumB = 0, sumAB = 0, sumA2 = 0, sumB2 = 0;
        
        for (let i = 0; i < n; i++) {
            sumA += a[i];
            sumB += b[i];
            sumAB += a[i] * b[i];
            sumA2 += a[i] * a[i];
            sumB2 += b[i] * b[i];
        }
        
        const numerator = sumAB - (sumA * sumB / n);
        const denominator = Math.sqrt((sumA2 - sumA * sumA / n) * (sumB2 - sumB * sumB / n));
        
        return denominator === 0 ? 0 : numerator / denominator;
    }
    
    getCamelotKey(key) {
        const normalizedKey = key.replace('♯', '#').replace('♭', 'b');
        return this.camelotWheel[normalizedKey] || '8B';
    }
    
    bandPassFilter(samples, sampleRate, lowFreq, highFreq) {
        const filtered = new Float32Array(samples.length);
        const dt = 1.0 / sampleRate;
        const lowRC = 1.0 / (2 * Math.PI * lowFreq);
        const highRC = 1.0 / (2 * Math.PI * highFreq);
        
        // Simple IIR filter
        let low = samples[0];
        let high = samples[0];
        
        for (let i = 1; i < samples.length; i++) {
            const alphaLow = dt / (lowRC + dt);
            const alphaHigh = highRC / (highRC + dt);
            
            low = low + alphaLow * (samples[i] - low);
            high = alphaHigh * (high + samples[i] - samples[i-1]);
            
            filtered[i] = high - low;
        }
        
        return filtered;
    }
    
    calculateEnergy(channelData) {
        let sum = 0;
        const length = Math.min(channelData.length, channelData.length);
        
        for (let i = 0; i < length; i++) {
            sum += Math.abs(channelData[i]);
        }
        
        const avg = sum / length;
        
        if (avg > 0.3) return 'High';
        if (avg > 0.15) return 'Medium';
        return 'Low';
    }
}

// Create global instance
window.audioAnalyzerV2 = new AudioAnalyzerV2();

/**
 * Analyze a track and update the UI
 */
async function analyzeTrackAudio(trackId, audioUrl) {
    // Validate analyzer is available
    if (!window.audioAnalyzerV2) {
        throw new Error('Audio analyzer not initialized. Please refresh the page.');
    }
    
    // Validate audio URL
    if (!audioUrl || audioUrl.trim() === '') {
        throw new Error('Audio URL is missing or invalid');
    }
    
    const analyzer = window.audioAnalyzerV2;
    
    // Get UI elements
    const bpmElement = document.querySelector('[data-analyze="bpm"]');
    const keyElement = document.querySelector('[data-analyze="key"]');
    const energyElement = document.querySelector('[data-analyze="energy"]');
    
    // Show analyzing state
    const elementsToUpdate = [bpmElement, keyElement, energyElement].filter(el => el);
    elementsToUpdate.forEach(el => {
        el.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
        el.classList.add('analyzing');
    });
    
    try {
        console.log('🎵 Starting analysis (V2)...', { trackId, audioUrl: audioUrl ? 'present' : 'missing' });
        
        const result = await analyzer.analyzeAudio(audioUrl, (stage, progress) => {
            console.log(`🎵 Analysis progress: ${stage} (${progress}%)`);
        });
        
        console.log('🎵 Analysis completed, result:', result);
        
        // Validate result
        if (!result || !result.bpm || !result.key || !result.camelot) {
            throw new Error('Analysis result is incomplete. Please try again.');
        }
        
        // Only fix clearly wrong values (188 -> 94)
        let displayBpm = result.bpm;
        if (Math.abs(displayBpm - 188) < 2) {
            displayBpm = 94;
            result.bpm = 94;
            console.log(`🎵 UI FIX: Fixed 188 → 94`);
        }
        
        // Update UI with results
        if (bpmElement) {
            bpmElement.textContent = displayBpm;
            bpmElement.classList.remove('analyzing');
        }
        
        if (keyElement) {
            keyElement.innerHTML = `${result.key} <span class="camelot-key">${result.camelot}</span>`;
            keyElement.classList.remove('analyzing');
        }
        
        if (energyElement) {
            energyElement.textContent = result.energy || 'Medium';
            energyElement.classList.remove('analyzing');
        }
        
        // Save to database
        try {
            const response = await fetch('/api/save_audio_analysis.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    track_id: trackId,
                    bpm: result.bpm,
                    key: result.key,
                    camelot: result.camelot,
                    energy: result.energy || 'Medium',
                    confidence: result.confidence || 0
                })
            });
            
            const data = await response.json();
            if (data.success) {
                console.log('🎵 Analysis saved to database');
            } else {
                console.warn('🎵 Failed to save analysis:', data.error);
            }
        } catch (saveError) {
            console.error('🎵 Error saving analysis:', saveError);
            // Don't throw - analysis succeeded, saving is secondary
        }
        
        return result;
        
    } catch (error) {
        console.error('🎵 Analysis error:', error);
        
        // Reset UI
        elementsToUpdate.forEach(el => {
            el.innerHTML = '<span style="color: #888;">—</span>';
            el.classList.remove('analyzing');
        });
        
        throw error;
    }
}


CasperSecurity Mini