@oletizi/audio-tools
Version:
Monorepo for hardware sampler utilities and format parsers
375 lines (332 loc) • 14.5 kB
text/typescript
/**
* Akai Disk Extraction CLI
*
* Command-line tool for extracting Akai disk images and converting
* sampler programs to SFZ and DecentSampler formats.
*/
import { Command } from "commander";
import { extractAkaiDisk } from "@/lib/extractor/disk-extractor.js";
import { extractBatch } from "@/lib/extractor/batch-extractor.js";
import { convertA3PToSFZ } from "@/lib/converters/s3k-to-sfz.js";
import { convertA3PToDecentSampler } from "@/lib/converters/s3k-to-decentsampler.js";
import { convertAKPToSFZ } from "@/lib/converters/s5k-to-sfz.js";
import { convertAKPToDecentSampler } from "@/lib/converters/s5k-to-decentsampler.js";
import { loadConfig, getEnabledExportSources, type BackupSource } from "@oletizi/audiotools-config";
import { homedir } from "os";
import { join } from "pathe";
import { existsSync } from "fs";
import { readdir } from "fs/promises";
const program = new Command();
const DEFAULT_BACKUP_ROOT = join(homedir(), '.audiotools', 'backup');
const DEFAULT_OUTPUT_ROOT = join(homedir(), '.audiotools', 'sampler-export', 'extracted');
/**
* Handle config-based extraction
*/
async function handleExtractCommand(sourceName: string | undefined, options: any): Promise<void> {
try {
// If --input flag is provided, use flag-based logic (backward compatibility)
if (options.input) {
// Extract single disk image
console.log(`Extracting Akai disk: ${options.input}`);
const outputDir = options.output || DEFAULT_OUTPUT_ROOT;
const result = await extractAkaiDisk({
diskImage: options.input,
outputDir,
convertToSFZ: options.format ? options.format.includes('sfz') : true,
convertToDecentSampler: options.format ? options.format.includes('decentsampler') : true,
});
if (!result.success) {
console.error("Extraction failed:");
result.errors.forEach((err) => console.error(` - ${err}`));
process.exit(1);
}
if (result.errors.length > 0) {
console.warn("Extraction completed with warnings:");
result.errors.forEach((err) => console.warn(` - ${err}`));
}
return;
}
// Config-based extraction
const config = await loadConfig();
if (!config.backup) {
console.error("Error: No backup configuration found");
console.error("Run 'audiotools config' to set up configuration");
process.exit(1);
}
const enabledSources = getEnabledExportSources(config);
if (enabledSources.length === 0) {
console.error("Error: No enabled export sources found in configuration");
console.error("Run 'audiotools config' to configure export sources");
process.exit(1);
}
// Determine formats to use
const formats = config.export?.formats || ['sfz', 'decentsampler'];
const convertToSFZ = formats.includes('sfz');
const convertToDecentSampler = formats.includes('decentsampler');
const outputRoot = config.export?.outputRoot || DEFAULT_OUTPUT_ROOT;
// If sourceName is provided, extract only that source
if (sourceName) {
const sourceConfig = enabledSources.find(s => s.name === sourceName);
if (!sourceConfig) {
console.error(`Error: Source '${sourceName}' not found or not enabled for export`);
console.error(`Available sources: ${enabledSources.map(s => s.name).join(', ')}`);
process.exit(1);
}
console.log(`Extracting source: ${sourceConfig.name}`);
console.log(`Sampler: ${sourceConfig.sampler || 'auto-detect'}`);
console.log(`Device: ${sourceConfig.device}`);
console.log(`Formats: ${formats.join(', ')}`);
console.log("");
await extractSource(sourceConfig, {
outputRoot,
convertToSFZ,
convertToDecentSampler,
force: options.force || false,
});
return;
}
// Extract all enabled sources
console.log(`Found ${enabledSources.length} enabled export source(s)\n`);
for (let i = 0; i < enabledSources.length; i++) {
const sourceConfig = enabledSources[i];
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`[${i + 1}/${enabledSources.length}] ${sourceConfig.name}`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`Sampler: ${sourceConfig.sampler || 'auto-detect'}`);
console.log(`Device: ${sourceConfig.device}`);
console.log(`Formats: ${formats.join(', ')}`);
console.log("");
try {
await extractSource(sourceConfig, {
outputRoot,
convertToSFZ,
convertToDecentSampler,
force: options.force || false,
});
} catch (err: any) {
console.error(`Failed to extract ${sourceConfig.name}: ${err.message}`);
// Continue with next source
}
console.log("");
}
console.log("✓ All extractions complete");
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
/**
* Extract disk images from a backup source
*/
async function extractSource(
source: BackupSource,
options: {
outputRoot: string;
convertToSFZ: boolean;
convertToDecentSampler: boolean;
force: boolean;
}
): Promise<void> {
// Find backup directory for this source
const backupDir = join(
DEFAULT_BACKUP_ROOT,
source.sampler || 'unknown',
source.device
);
if (!existsSync(backupDir)) {
console.warn(` ⚠ Backup directory not found: ${backupDir}`);
console.warn(` Skipping ${source.name}`);
return;
}
console.log(` Scanning: ${backupDir}`);
// Find all disk images in backup directory
const entries = await readdir(backupDir, { withFileTypes: true });
const diskImages = entries
.filter(entry => entry.isFile())
.filter(entry => {
const name = entry.name.toLowerCase();
return name.endsWith('.hds') ||
name.endsWith('.img') ||
name.endsWith('.iso');
})
.map(entry => join(backupDir, entry.name));
if (diskImages.length === 0) {
console.log(` No disk images found in ${backupDir}`);
return;
}
console.log(` Found ${diskImages.length} disk image(s)`);
console.log("");
// Extract each disk image
for (const diskImage of diskImages) {
const diskName = diskImage.split('/').pop()?.replace(/\.(hds|img|iso)$/i, '') || 'unknown';
console.log(` Extracting: ${diskName}`);
// Output directory: {outputRoot}/{sampler}/{device}/{diskName}
const outputDir = join(
options.outputRoot,
source.sampler || 'unknown',
source.device,
diskName
);
try {
const result = await extractAkaiDisk({
diskImage,
outputDir,
convertToSFZ: options.convertToSFZ,
convertToDecentSampler: options.convertToDecentSampler,
});
if (!result.success) {
console.error(` ✗ Extraction failed:`);
result.errors.forEach((err) => console.error(` - ${err}`));
} else if (result.errors.length > 0) {
console.warn(` ⚠ Extraction completed with warnings:`);
result.errors.forEach((err) => console.warn(` - ${err}`));
} else {
console.log(` ✓ Extracted to: ${outputDir}`);
}
} catch (err: any) {
console.error(` ✗ Error: ${err.message}`);
}
}
}
program
.name("akai-extract")
.description("Extract Akai disk images and convert programs to modern formats")
.version("1.0.0");
// Extract command (config-based with flag override)
program
.command("extract [source]")
.description("Extract from config (all enabled sources or specific source by name)")
.option("-i, --input <path>", "Override: extract single disk image (flag-based mode)")
.option("-o, --output <path>", "Override: output directory (requires --input)")
.option("-f, --format <formats>", "Override: output formats (comma-separated: sfz,decentsampler)")
.option("--force", "Force re-extraction even if unchanged")
.action(handleExtractCommand);
// Extract disk command (backward compatible)
program
.command("disk")
.description("Extract an Akai disk image")
.argument("<disk-image>", "Path to the Akai disk image (.hds, .img, etc.)")
.argument("<output-dir>", "Output directory for extracted files")
.option("--no-sfz", "Skip SFZ conversion")
.option("--no-decentsampler", "Skip DecentSampler conversion")
.action(async (diskImage: string, outputDir: string, options: any) => {
try {
console.log(`Extracting Akai disk: ${diskImage}`);
const result = await extractAkaiDisk({
diskImage,
outputDir,
convertToSFZ: options.sfz !== false,
convertToDecentSampler: options.decentsampler !== false,
});
if (!result.success) {
console.error("Extraction failed:");
result.errors.forEach((err) => console.error(` - ${err}`));
process.exit(1);
}
if (result.errors.length > 0) {
console.warn("Extraction completed with warnings:");
result.errors.forEach((err) => console.warn(` - ${err}`));
}
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
// Batch extraction command (backward compatible)
program
.command("batch")
.description("Extract all disk images from backup directories")
.option("--source <path>", "Source directory (default: ~/.audiotools/backup)")
.option("--dest <path>", "Destination directory (default: ~/.audiotools/sampler-export/extracted)")
.option("--samplers <types>", "Comma-separated sampler types: s5k,s3k (default: s5k,s3k)")
.option("--force", "Force re-extraction of all disks")
.option("--no-sfz", "Skip SFZ conversion")
.option("--no-decentsampler", "Skip DecentSampler conversion")
.action(async (options: any) => {
try {
const samplerTypes = options.samplers
? options.samplers.split(",").map((s: string) => s.trim())
: undefined;
const result = await extractBatch({
sourceDir: options.source,
destDir: options.dest,
samplerTypes,
force: options.force || false,
convertToSFZ: options.sfz !== false,
convertToDecentSampler: options.decentsampler !== false,
});
// Exit with error code only for actual failures, not warnings
if (result.failed > 0) {
process.exit(1);
}
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
// Convert S3K program to SFZ
program
.command("s3k-to-sfz")
.description("Convert S3K (.a3p) program to SFZ")
.argument("<a3p-file>", "Path to .a3p program file")
.argument("<sfz-dir>", "Output directory for SFZ file")
.argument("<wav-dir>", "Directory containing WAV samples")
.action(async (a3pFile: string, sfzDir: string, wavDir: string) => {
try {
const sfzPath = await convertA3PToSFZ(a3pFile, sfzDir, wavDir);
console.log(`Created: ${sfzPath}`);
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
// Convert S3K program to DecentSampler
program
.command("s3k-to-ds")
.description("Convert S3K (.a3p) program to DecentSampler")
.argument("<a3p-file>", "Path to .a3p program file")
.argument("<ds-dir>", "Output directory for .dspreset file")
.argument("<wav-dir>", "Directory containing WAV samples")
.action(async (a3pFile: string, dsDir: string, wavDir: string) => {
try {
const dsPath = await convertA3PToDecentSampler(a3pFile, dsDir, wavDir);
console.log(`Created: ${dsPath}`);
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
// Convert S5K program to SFZ
program
.command("s5k-to-sfz")
.description("Convert S5K (.akp) program to SFZ")
.argument("<akp-file>", "Path to .akp program file")
.argument("<sfz-dir>", "Output directory for SFZ file")
.argument("<wav-dir>", "Directory containing WAV samples")
.action((akpFile: string, sfzDir: string, wavDir: string) => {
try {
const sfzPath = convertAKPToSFZ(akpFile, sfzDir, wavDir);
console.log(`Created: ${sfzPath}`);
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
// Convert S5K program to DecentSampler
program
.command("s5k-to-ds")
.description("Convert S5K (.akp) program to DecentSampler")
.argument("<akp-file>", "Path to .akp program file")
.argument("<ds-dir>", "Output directory for .dspreset file")
.argument("<wav-dir>", "Directory containing WAV samples")
.action((akpFile: string, dsDir: string, wavDir: string) => {
try {
const dsPath = convertAKPToDecentSampler(akpFile, dsDir, wavDir);
console.log(`Created: ${dsPath}`);
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
program.parse();