import { spawn } from "child_process";
import { serverLogger } from "../utils/serverLogger.js";
import { getFFprobePath } from "../utils/ffmpegPaths.js";
export class AudioFeatureExtractionService {
    static instance;
    featureCache = new Map();
    constructor(){
    // TODO: [Priority 3.1] Enhance BPM and key detection for DJ features
    // - Integrate Essentia or aubio for accurate BPM detection
    // - Add key detection with Camelot/Open Key notation for harmonic mixing
    // - Store cue points and loop markers for DJ software compatibility
    // - Implement beat grid analysis for precise mixing
    // - Add support for Rekordbox/Serato metadata import/export
    }
    static getInstance() {
        if (!AudioFeatureExtractionService.instance) {
            AudioFeatureExtractionService.instance = new AudioFeatureExtractionService();
        }
        return AudioFeatureExtractionService.instance;
    }
    /**
   * Extract all audio features from a file
   */ async extractAllFeatures(filePath) {
        // Check cache
        if (this.featureCache.has(filePath)) {
            return this.featureCache.get(filePath);
        }
        const features = {};
        // Extract basic audio info using ffprobe
        const basicInfo = await this.extractBasicInfo(filePath);
        Object.assign(features, basicInfo);
        // Extract BPM using multiple methods
        const bpm = await this.extractBPM(filePath);
        if (bpm) features.bpm = bpm;
        // Extract musical key
        const keyInfo = await this.extractKey(filePath);
        if (keyInfo) {
            features.key = keyInfo.key;
            features.mode = keyInfo.mode;
        }
        // Extract loudness and dynamics
        const loudnessInfo = await this.extractLoudness(filePath);
        Object.assign(features, loudnessInfo);
        // Estimate mood and energy (using spectral analysis)
        const spectralFeatures = await this.extractSpectralFeatures(filePath);
        Object.assign(features, spectralFeatures);
        // Derive mood from features
        features.mood = this.deriveMood(features);
        // Estimate genre from features
        features.genre = this.estimateGenre(features);
        // Cache the result
        this.featureCache.set(filePath, features);
        return features;
    }
    /**
   * Extract basic audio information using ffprobe
   */ async extractBasicInfo(filePath) {
        return new Promise(async (resolve)=>{
            const features = {};
            let ffprobe = null;
            const timeout = setTimeout(()=>{
                if (ffprobe) {
                    ffprobe.kill("SIGKILL");
                    resolve(features);
                }
            }, 10000); // 10 second timeout
            try {
                ffprobe = spawn(getFFprobePath(), [
                    "-v",
                    "quiet",
                    "-print_format",
                    "json",
                    "-show_format",
                    "-show_streams",
                    filePath
                ]);
                let output = "";
                ffprobe.stdout.on("data", (data)=>output += data);
                ffprobe.stderr.on("data", ()=>{}); // Consume stderr to prevent buffer overflow
                ffprobe.on("close", (code)=>{
                    clearTimeout(timeout);
                    try {
                        if (code === 0 && output) {
                            const data = JSON.parse(output);
                            if (data.format) {
                                features.duration = parseFloat(data.format.duration);
                                features.bitrate = parseInt(data.format.bit_rate);
                            }
                            if (data.streams && data.streams[0]) {
                                const stream = data.streams[0];
                                features.sampleRate = parseInt(stream.sample_rate);
                                features.channels = stream.channels;
                            }
                        }
                    } catch (error) {
                        serverLogger.debug("Failed to parse ffprobe output", {
                            error
                        });
                    }
                    resolve(features);
                });
                ffprobe.on("error", (error)=>{
                    clearTimeout(timeout);
                    serverLogger.debug("ffprobe error", {
                        error: error.message
                    });
                    resolve(features);
                });
            } catch (error) {
                clearTimeout(timeout);
                const errorMessage = error instanceof Error ? error.message : "Unknown error";
                serverLogger.debug("Failed to spawn ffprobe", {
                    error: errorMessage
                });
                resolve(features);
            }
        });
    }
    /**
   * Extract BPM using multiple methods
   */ async extractBPM(filePath) {
        // Method 1: Try aubiotempo (most accurate for electronic music)
        try {
            const result = await this.executeCommand("aubiotempo", [
                filePath
            ]);
            const bpmMatch = result.stdout.match(/(\d+\.?\d*)\s*bpm/i);
            if (bpmMatch) {
                return Math.round(parseFloat(bpmMatch[1]));
            }
        } catch (error) {
            serverLogger.debug("aubiotempo not available");
        }
        // Method 2: Try bpm-tools
        try {
            const result = await this.executeCommand("bpm", [
                filePath
            ]);
            const bpm = parseFloat(result.stdout.trim());
            if (!isNaN(bpm) && bpm > 0) {
                return Math.round(bpm);
            }
        } catch (error) {
            serverLogger.debug("bpm-tools not available");
        }
        // Method 3: Use ffmpeg with showwaves filter (less accurate but always available)
        try {
            const result = await this.executeCommand("ffmpeg", [
                "-i",
                filePath,
                "-af",
                "abpm",
                "-f",
                "null",
                "-"
            ]);
            const bpmMatch = result.stderr.match(/BPM:\s*(\d+\.?\d*)/i);
            if (bpmMatch) {
                return Math.round(parseFloat(bpmMatch[1]));
            }
        } catch (error) {
            serverLogger.debug("ffmpeg BPM detection failed");
        }
        return undefined;
    }
    /**
   * Extract musical key
   */ async extractKey(filePath) {
        // Method 1: Try keyfinder-cli
        try {
            const result = await this.executeCommand("keyfinder-cli", [
                filePath
            ]);
            const key = result.stdout.trim();
            if (key) {
                // Parse key format (e.g., "C major", "A minor", "Gbm")
                const match = key.match(/^([A-G][b#]?)\s*(major|minor|maj|min|m)?$/i);
                if (match) {
                    return {
                        key: match[1],
                        mode: match[2]?.toLowerCase().includes("min") ? "minor" : "major"
                    };
                }
            }
        } catch (error) {
            serverLogger.debug("keyfinder-cli not available");
        }
        // Method 2: Try sonic-annotator with key detection plugin
        try {
            const result = await this.executeCommand("sonic-annotator", [
                "-d",
                "vamp:qm-vamp-plugins:qm-keydetector:key",
                filePath,
                "-w",
                "csv",
                "--csv-stdout"
            ]);
            // Parse CSV output for key
            const lines = result.stdout.split("\n");
            for (const line of lines){
                const parts = line.split(",");
                if (parts.length >= 3) {
                    const key = parts[2].trim().replace(/"/g, "");
                    if (key) {
                        return {
                            key: key.replace(/\s*(major|minor).*/i, ""),
                            mode: key.toLowerCase().includes("minor") ? "minor" : "major"
                        };
                    }
                }
            }
        } catch (error) {
            serverLogger.debug("sonic-annotator not available");
        }
        return undefined;
    }
    /**
   * Extract loudness and dynamic range
   */ async extractLoudness(filePath) {
        const features = {};
        try {
            // Cap how much audio we decode for the loudness measurement. The ffmpeg
            // loudnorm pass is decode-bound and unbounded by default, so a 60-90 min DJ
            // set decodes end-to-end and the long tail dominates the whole-library
            // backfill's wall-clock. `-t` as an INPUT option stops reading the file after
            // N seconds, mirroring the SOUND_ANALYSIS_MAX_SECONDS=180 cap already proven
            // safe for the essentia decode. Integrated LUFS over the first few minutes
            // closely tracks the full-track value for steady-state material; for long /
            // dynamic tracks it can drift slightly, but an unanalyzed track just plays at
            // gain 1.0, so the worst case is a marginally-off normalization, never broken
            // playback. Tunable/disable-able via env without a rebuild.
            const maxSeconds = Number(process.env.SOUND_LOUDNESS_MAX_SECONDS) || 180;
            const result = await this.executeCommand("ffmpeg", [
                "-t",
                String(maxSeconds),
                "-i",
                filePath,
                "-vn",
                "-af",
                "loudnorm=print_format=json",
                "-f",
                "null",
                "-"
            ]);
            // Parse loudness stats from stderr
            const jsonMatch = result.stderr.match(/\{[\s\S]*\}/);
            if (jsonMatch) {
                const stats = JSON.parse(jsonMatch[0]);
                features.loudness = parseFloat(stats.input_i); // Integrated loudness in LUFS
                features.dynamicRange = parseFloat(stats.input_lra); // Loudness range
                features.peakAmplitude = parseFloat(stats.input_tp); // True peak
            }
        } catch (error) {
            serverLogger.debug("Loudness extraction failed", {
                error
            });
        }
        return features;
    }
    /**
   * Public entry point for loudness-only measurement, used by the DJ analysis
   * pipeline to populate tracks.loudness (integrated LUFS) + tracks.loudness_true_peak
   * (true peak, dBTP) for client-side ReplayGain normalization.
   *
   * Reuses the existing ffmpeg loudnorm extractor (extractLoudness) rather than
   * running the full BPM/key/spectral pass (extractAllFeatures). Returns nulls
   * when ffmpeg can't measure the file (unsupported codec, corrupt input).
   */ async measureLoudness(filePath) {
        const features = await this.extractLoudness(filePath);
        return {
            integratedLufs: Number.isFinite(features.loudness) ? features.loudness : null,
            truePeak: Number.isFinite(features.peakAmplitude) ? features.peakAmplitude : null
        };
    }
    /**
   * Extract spectral features for mood/energy analysis
   */ async extractSpectralFeatures(filePath) {
        const features = {};
        try {
            // Use sox to get spectral statistics
            const result = await this.executeCommand("sox", [
                filePath,
                "-n",
                "stat",
                "-freq"
            ]);
            // Parse output for spectral characteristics
            const output = result.stderr || result.stdout;
            // Extract RMS amplitude (relates to energy)
            const rmsMatch = output.match(/RMS\s+amplitude:\s+([\d.]+)/);
            if (rmsMatch) {
                features.energy = Math.min(1, parseFloat(rmsMatch[1]) * 2); // Normalize to 0-1
            }
            // Rough frequency analysis for brightness/valence
            const freqData = output.match(/(\d+)\.\d+\s+(\d+\.\d+)/g);
            if (freqData) {
                // Calculate spectral centroid (brightness indicator)
                let totalMagnitude = 0;
                let weightedSum = 0;
                freqData.forEach((line)=>{
                    const [freq, magnitude] = line.split(/\s+/).map(parseFloat);
                    totalMagnitude += magnitude;
                    weightedSum += freq * magnitude;
                });
                if (totalMagnitude > 0) {
                    features.spectralCentroid = weightedSum / totalMagnitude;
                    // Higher spectral centroid often correlates with higher valence
                    features.valence = Math.min(1, features.spectralCentroid / 4000);
                }
            }
        } catch (error) {
            serverLogger.debug("Spectral feature extraction failed", {
                error
            });
        }
        // Estimate danceability based on BPM and energy
        if (features.energy) {
            features.danceability = features.energy * 0.7;
        }
        // Default values for features we can't extract
        features.acousticness = 0.5; // Would need ML model for accurate detection
        features.instrumentalness = 0.5; // Would need vocal detection
        features.liveness = 0.1; // Would need audience noise detection
        features.speechiness = 0.05; // Would need speech detection
        return features;
    }
    /**
   * Derive mood from audio features
   */ deriveMood(features) {
        const energy = features.energy || 0.5;
        const valence = features.valence || 0.5;
        const bpm = features.bpm || 120;
        const mode = features.mode || "major";
        // Mood mapping based on energy and valence
        if (energy > 0.7 && valence > 0.7) {
            return bpm > 140 ? "euphoric" : "happy";
        } else if (energy > 0.7 && valence < 0.3) {
            return "aggressive";
        } else if (energy < 0.3 && valence > 0.7) {
            return "peaceful";
        } else if (energy < 0.3 && valence < 0.3) {
            return "melancholic";
        } else if (energy > 0.5 && valence > 0.5) {
            return mode === "major" ? "uplifting" : "intense";
        } else if (energy < 0.5 && valence < 0.5) {
            return mode === "minor" ? "dark" : "chill";
        } else {
            return "neutral";
        }
    }
    /**
   * Estimate genre from audio features
   */ estimateGenre(features) {
        const bpm = features.bpm || 120;
        const energy = features.energy || 0.5;
        const acousticness = features.acousticness || 0.5;
        // Simple genre estimation based on features
        if (bpm >= 120 && bpm <= 135 && energy > 0.7) {
            return "house";
        } else if (bpm >= 140 && bpm <= 150 && energy > 0.8) {
            return "trance";
        } else if (bpm >= 160 && bpm <= 180 && energy > 0.8) {
            return "drum & bass";
        } else if (bpm >= 70 && bpm <= 90 && energy < 0.5) {
            return "hip-hop";
        } else if (bpm >= 60 && bpm <= 80 && acousticness > 0.7) {
            return "folk";
        } else if (bpm >= 100 && bpm <= 140 && energy > 0.6) {
            return "pop";
        } else if (bpm >= 110 && bpm <= 140 && energy > 0.7 && acousticness < 0.3) {
            return "rock";
        } else if (bpm < 100 && energy < 0.4 && acousticness > 0.6) {
            return "classical";
        } else if (bpm >= 95 && bpm <= 115 && energy < 0.6) {
            return "r&b";
        } else {
            return "electronic";
        }
    }
    /**
   * Execute command helper
   */ async executeCommand(command, args) {
        return new Promise((resolve, reject)=>{
            const child = spawn(command, args);
            let stdout = "";
            let stderr = "";
            child.stdout.on("data", (data)=>{
                stdout += data.toString();
            });
            child.stderr.on("data", (data)=>{
                stderr += data.toString();
            });
            child.on("error", reject);
            child.on("close", (code)=>{
                if (code === 0 || stderr.includes("BPM:") || stderr.includes("LUFS")) {
                    resolve({
                        stdout,
                        stderr
                    });
                } else {
                    reject(new Error(`Command failed with code ${code}: ${stderr}`));
                }
            });
        });
    }
    /**
   * Clear cache for a file
   */ clearCache(filePath) {
        if (filePath) {
            this.featureCache.delete(filePath);
        } else {
            this.featureCache.clear();
        }
    }
}
export const audioFeatureExtraction = AudioFeatureExtractionService.getInstance();

//# sourceMappingURL=audioFeatureExtraction.js.map