initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-12-12 22:13:54 +01:00
commit 0c913a770f
Se han modificado 18 ficheros con 3334 adiciones y 0 borrados

338
bin/cli.js Archivo normal
Ver fichero

@@ -0,0 +1,338 @@
#!/usr/bin/env node
/**
* AutoMixer CLI
*
* Command-line interface for the automixer library.
* Provides an easy way to mix multiple audio files from the terminal.
*
* Usage:
* automixer mix track1.mp3 track2.mp3 -o output.mp3
* automixer analyze track.mp3
*/
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import cliProgress from 'cli-progress';
import path from 'path';
import fs from 'fs/promises';
import { AutoMixer } from '../src/core/AutoMixer.js';
import { BPMDetector } from '../src/audio/BPMDetector.js';
import { AudioAnalyzer } from '../src/audio/AudioAnalyzer.js';
const program = new Command();
// Package info
const pkg = JSON.parse(
await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8')
);
program
.name('automixer')
.description('Automatic DJ-style audio mixer with BPM detection and beat synchronization')
.version(pkg.version);
/**
* Mix command - Main mixing functionality
*/
program
.command('mix')
.description('Mix multiple audio files with automatic beat matching')
.argument('<files...>', 'Audio files to mix (in order)')
.option('-o, --output <file>', 'Output file path', 'mix_output.mp3')
.option('-c, --crossfade <seconds>', 'Crossfade duration in seconds', '32')
.option('-b, --bpm <number>', 'Target BPM (auto-detect if not specified)')
.option('--max-bpm-change <percent>', 'Maximum BPM change allowed', '8')
.option('--no-preserve-pitch', 'Allow pitch to change with tempo')
.option('-q, --quiet', 'Suppress progress output')
.action(async (files, options) => {
const spinner = ora();
try {
// Validate input files
console.log(chalk.cyan('\n🎵 AutoMixer - Automatic DJ Mixer\n'));
if (files.length < 2) {
console.log(chalk.yellow('⚠️ At least 2 files are required for mixing.'));
console.log(chalk.gray(' Use "automixer analyze" to analyze a single file.\n'));
process.exit(1);
}
// Check files exist
spinner.start('Validating input files...');
const validatedFiles = [];
for (const file of files) {
const fullPath = path.resolve(file);
try {
await fs.access(fullPath);
validatedFiles.push(fullPath);
} catch {
spinner.fail(`File not found: ${file}`);
process.exit(1);
}
}
spinner.succeed(`Found ${validatedFiles.length} audio files`);
// Create mixer instance
const mixer = new AutoMixer({
crossfadeDuration: parseInt(options.crossfade, 10),
targetBPM: options.bpm ? parseFloat(options.bpm) : null,
maxBPMChange: parseFloat(options.maxBpmChange),
preservePitch: options.preservePitch
});
// Progress bar for analysis
const analysisBar = new cliProgress.SingleBar({
format: chalk.cyan('Analyzing') + ' |{bar}| {percentage}% | {track}',
hideCursor: true
}, cliProgress.Presets.shades_classic);
if (!options.quiet) {
console.log(chalk.gray('\n📊 Analyzing tracks...\n'));
analysisBar.start(validatedFiles.length, 0, { track: '' });
}
// Set up event listeners
let currentTrackIndex = 0;
mixer.on('analysis:track:start', ({ index, filepath }) => {
if (!options.quiet) {
analysisBar.update(index, { track: path.basename(filepath) });
}
});
mixer.on('analysis:track:complete', ({ index }) => {
currentTrackIndex = index + 1;
if (!options.quiet) {
analysisBar.update(currentTrackIndex);
}
});
// Analyze tracks
mixer.addTracks(validatedFiles);
const analyzedTracks = await mixer.analyzeTracks();
if (!options.quiet) {
analysisBar.stop();
console.log();
}
// Display track info
if (!options.quiet) {
console.log(chalk.cyan('📋 Track Information:\n'));
for (const track of analyzedTracks) {
console.log(chalk.white(` ${track.filename}`));
console.log(chalk.gray(` BPM: ${chalk.yellow(track.bpm)} | Duration: ${formatDuration(track.duration)}`));
}
console.log();
}
// Calculate target BPM
const targetBPM = mixer.calculateOptimalBPM();
if (!options.quiet) {
console.log(chalk.cyan(`🎯 Target BPM: ${chalk.yellow(targetBPM)}\n`));
}
// Create the mix
const outputPath = path.resolve(options.output);
if (!options.quiet) {
spinner.start('Creating mix...');
}
mixer.on('mix:prepare:complete', ({ index, tempoRatio }) => {
if (!options.quiet && Math.abs(tempoRatio - 1) > 0.001) {
const change = ((tempoRatio - 1) * 100).toFixed(1);
const sign = change > 0 ? '+' : '';
spinner.text = `Adjusting tempo for track ${index + 1} (${sign}${change}%)`;
}
});
mixer.on('mix:render:progress', ({ current, total, message }) => {
if (!options.quiet) {
spinner.text = message || `Mixing ${current}/${total}...`;
}
});
await mixer.createMix(outputPath);
if (!options.quiet) {
spinner.succeed('Mix created successfully!');
console.log(chalk.green(`\n✅ Output saved to: ${chalk.white(outputPath)}\n`));
}
} catch (error) {
spinner.fail(chalk.red(`Error: ${error.message}`));
if (process.env.DEBUG) {
console.error(error);
}
process.exit(1);
}
});
/**
* Analyze command - Analyze a single track
*/
program
.command('analyze')
.description('Analyze audio file(s) and display BPM and other information')
.argument('<files...>', 'Audio files to analyze')
.option('-j, --json', 'Output as JSON')
.action(async (files, options) => {
const spinner = ora();
const results = [];
try {
for (const file of files) {
const fullPath = path.resolve(file);
if (!options.json) {
spinner.start(`Analyzing ${path.basename(file)}...`);
}
try {
await fs.access(fullPath);
} catch {
if (options.json) {
results.push({ file, error: 'File not found' });
} else {
spinner.fail(`File not found: ${file}`);
}
continue;
}
const bpmDetector = new BPMDetector();
const audioAnalyzer = new AudioAnalyzer();
const [bpmResult, metadata] = await Promise.all([
bpmDetector.detect(fullPath),
audioAnalyzer.getMetadata(fullPath)
]);
const result = {
file: path.basename(file),
path: fullPath,
bpm: bpmResult.bpm,
confidence: bpmResult.confidence,
duration: metadata.duration,
durationFormatted: formatDuration(metadata.duration),
sampleRate: metadata.sampleRate,
channels: metadata.channels,
bitrate: metadata.bitrate,
format: metadata.format,
codec: metadata.codec
};
results.push(result);
if (!options.json) {
spinner.succeed(`${chalk.white(result.file)}`);
console.log(chalk.gray(` BPM: ${chalk.yellow(result.bpm)} (confidence: ${(result.confidence * 100).toFixed(0)}%)`));
console.log(chalk.gray(` Duration: ${result.durationFormatted}`));
console.log(chalk.gray(` Format: ${result.codec} ${result.sampleRate}Hz ${result.channels}ch ${Math.round(result.bitrate / 1000)}kbps`));
console.log();
}
}
if (options.json) {
console.log(JSON.stringify(results, null, 2));
}
} catch (error) {
if (options.json) {
console.log(JSON.stringify({ error: error.message }));
} else {
spinner.fail(chalk.red(`Error: ${error.message}`));
}
process.exit(1);
}
});
/**
* Check command - Verify FFmpeg installation
*/
program
.command('check')
.description('Check system requirements (FFmpeg installation)')
.action(async () => {
const spinner = ora();
console.log(chalk.cyan('\n🔍 Checking system requirements...\n'));
// Check FFmpeg
spinner.start('Checking FFmpeg...');
try {
const { spawn } = await import('child_process');
await new Promise((resolve, reject) => {
const ffmpeg = spawn('ffmpeg', ['-version']);
let output = '';
ffmpeg.stdout.on('data', (data) => {
output += data.toString();
});
ffmpeg.on('close', (code) => {
if (code === 0) {
const version = output.match(/ffmpeg version (\S+)/)?.[1] || 'unknown';
spinner.succeed(`FFmpeg installed (version: ${version})`);
resolve();
} else {
reject(new Error('FFmpeg not working'));
}
});
ffmpeg.on('error', () => {
reject(new Error('FFmpeg not found'));
});
});
} catch {
spinner.fail('FFmpeg not found');
console.log(chalk.yellow('\n Please install FFmpeg:'));
console.log(chalk.gray(' - macOS: brew install ffmpeg'));
console.log(chalk.gray(' - Ubuntu: sudo apt install ffmpeg'));
console.log(chalk.gray(' - Windows: choco install ffmpeg'));
console.log();
}
// Check FFprobe
spinner.start('Checking FFprobe...');
try {
const { spawn } = await import('child_process');
await new Promise((resolve, reject) => {
const ffprobe = spawn('ffprobe', ['-version']);
ffprobe.on('close', (code) => {
if (code === 0) {
spinner.succeed('FFprobe installed');
resolve();
} else {
reject(new Error('FFprobe not working'));
}
});
ffprobe.on('error', () => {
reject(new Error('FFprobe not found'));
});
});
} catch {
spinner.fail('FFprobe not found (usually included with FFmpeg)');
}
console.log();
});
/**
* Format duration in mm:ss format
*/
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Parse and run
program.parse();