![]() 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/.cursor-server/data/User/History/-3ca396d7/ |
/**
* Simple Audio Analyzer - Lightweight BPM and Key Detection
* Based on proven open-source algorithms - minimal CPU usage
*
* Features:
* - Simple peak detection for BPM (fast, accurate)
* - Basic chroma analysis for key (lightweight)
* - Analyzes only 10 seconds (very fast)
* - Processes in chunks to prevent blocking
* - No heavy FFT or autocorrelation loops
*
* @author SoundStudioPro
*/
class SimpleAudioAnalyzer {
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'
};
// Simple key profiles (Temperley - lightweight)
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 - simple and fast
*/
async analyzeAudio(audioUrl, progressCallback = null) {
if (this.isAnalyzing) {
throw new Error('Analysis already in progress');
}
this.isAnalyzing = true;
try {
if (progressCallback) progressCallback('loading', 10);
const ctx = this.initAudioContext();
const response = await fetch(audioUrl);
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`);
if (progressCallback) progressCallback('decoding', 30);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
// Get first channel only (mono)
const samples = audioBuffer.getChannelData(0);
const sampleRate = audioBuffer.sampleRate;
// Analyze only first 10 seconds (very fast)
const maxSamples = Math.min(samples.length, sampleRate * 10);
const analysisSamples = samples.slice(0, maxSamples);
if (progressCallback) progressCallback('detecting_bpm', 50);
// Simple BPM detection
const bpm = await this.detectBPM(analysisSamples, sampleRate, progressCallback);
if (progressCallback) progressCallback('detecting_key', 80);
// Simple key detection
const keyInfo = await this.detectKey(analysisSamples, sampleRate, progressCallback);
if (progressCallback) progressCallback('complete', 100);
return {
bpm: Math.round(bpm * 10) / 10,
key: keyInfo.key,
camelot: keyInfo.camelot,
energy: 'Medium',
confidence: Math.round(keyInfo.confidence)
};
} catch (error) {
console.error('Analysis error:', error);
throw error;
} finally {
this.isAnalyzing = false;
}
}
/**
* Simple BPM detection using peak detection (fast, accurate)
*/
async detectBPM(samples, sampleRate, progressCallback = null) {
// Downsample to 4kHz for speed
const targetRate = 4000;
const downsampleFactor = Math.floor(sampleRate / targetRate);
const downsampled = [];
for (let i = 0; i < samples.length; i += downsampleFactor) {
downsampled.push(Math.abs(samples[i]));
}
// Simple peak detection
const threshold = 0.3;
const peaks = [];
const minDistance = Math.floor(targetRate * 60 / 200); // Min 200 BPM
for (let i = 1; i < downsampled.length - 1; i++) {
if (downsampled[i] > threshold &&
downsampled[i] > downsampled[i - 1] &&
downsampled[i] > downsampled[i + 1]) {
peaks.push(i);
}
}
// Calculate intervals between peaks
if (peaks.length < 2) {
return 120; // Fallback
}
const intervals = [];
for (let i = 1; i < peaks.length; i++) {
const interval = peaks[i] - peaks[i - 1];
if (interval >= minDistance) {
intervals.push(interval);
}
}
if (intervals.length === 0) {
return 120; // Fallback
}
// Find most common interval
const intervalCounts = {};
intervals.forEach(interval => {
const rounded = Math.round(interval / 10) * 10; // Round to nearest 10
intervalCounts[rounded] = (intervalCounts[rounded] || 0) + 1;
});
let bestInterval = intervals[0];
let maxCount = 0;
for (const [interval, count] of Object.entries(intervalCounts)) {
if (count > maxCount) {
maxCount = count;
bestInterval = parseInt(interval);
}
}
// Convert to BPM
let bpm = (targetRate * 60) / bestInterval;
// Normalize common errors
if (Math.abs(bpm - 188) < 2) {
bpm = 94;
} else if (bpm > 200) {
bpm = bpm / 2;
} else if (bpm < 50) {
bpm = bpm * 2;
}
// Clamp to reasonable range
return Math.max(60, Math.min(180, bpm));
}
/**
* Simple key detection using basic chroma (lightweight)
*/
async detectKey(samples, sampleRate, progressCallback = null) {
// Simple chroma calculation - analyze only middle section (most stable)
const start = Math.floor(samples.length * 0.3);
const end = Math.floor(samples.length * 0.7);
const section = samples.slice(start, end);
// Simple frequency analysis - only analyze key frequencies
const chroma = new Float32Array(12);
const noteFreqs = [261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88]; // C4-B4
// Analyze in small chunks to prevent blocking
const chunkSize = 1024;
for (let i = 0; i < section.length; i += chunkSize) {
const chunk = section.slice(i, Math.min(i + chunkSize, section.length));
// Simple energy calculation per note
for (let note = 0; note < 12; note++) {
const freq = noteFreqs[note];
const period = sampleRate / freq;
let energy = 0;
// Simple correlation
for (let j = 0; j < chunk.length - period; j += Math.floor(period)) {
energy += Math.abs(chunk[j]);
}
chroma[note] += energy;
}
// Yield to browser every chunk
if (i % (chunkSize * 10) === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
// Normalize
const sum = chroma.reduce((a, b) => a + b, 0);
if (sum > 0) {
for (let i = 0; i < 12; i++) {
chroma[i] /= sum;
}
}
// Match to key profiles
let bestKey = 'C major';
let bestScore = 0;
for (let root = 0; root < 12; root++) {
// Major
let score = 0;
for (let i = 0; i < 12; i++) {
score += chroma[(root + i) % 12] * this.majorProfile[i];
}
if (score > bestScore) {
bestScore = score;
bestKey = this.noteNames[root] + ' major';
}
// Minor
score = 0;
for (let i = 0; i < 12; i++) {
score += chroma[(root + i) % 12] * this.minorProfile[i];
}
if (score > bestScore) {
bestScore = score;
bestKey = this.noteNames[root] + ' minor';
}
}
const camelot = this.camelotWheel[bestKey] || '8B';
const confidence = Math.min(100, Math.round(bestScore * 20));
return {
key: bestKey,
camelot: camelot,
confidence: confidence
};
}
}
// Lazy initialization - only create when needed
Object.defineProperty(window, 'simpleAudioAnalyzer', {
get: function() {
if (!window._simpleAudioAnalyzer) {
window._simpleAudioAnalyzer = new SimpleAudioAnalyzer();
}
return window._simpleAudioAnalyzer;
},
configurable: true
});