import multer from 'multer'; import ffmpeg from 'fluent-ffmpeg'; import { dirname, join, extname, basename } from 'node:path'; import { resolveWithin } from '../utils/network/security.util.js'; import { existsSync, mkdirSync, readFileSync, unlink } from 'node:fs'; import wav from 'wav-decoder'; import { fileURLToPath } from 'node:url'; import { Request, Response } from 'express'; import { randomBytes } from 'node:crypto'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const TEMP_DIR = join(__dirname, '../../temp'); const uploadDir = join(TEMP_DIR, 'uploads'); const processedDir = join(TEMP_DIR, 'processed'); import { ChordsResult } from '../types/index.js'; interface EssentiaVector { delete: () => void; } interface EssentiaPCVVector extends EssentiaVector { push_back: (val: EssentiaVector) => void; } interface EssentiaInstance { arrayToVector: (arr: Float32Array) => EssentiaVector; vectorToArray: (vec: EssentiaVector) => string[] | number[]; KeyExtractor: (vec: EssentiaVector) => { key: string; scale: string }; Windowing: (vec: EssentiaVector) => { frame: EssentiaVector }; Spectrum: (vec: EssentiaVector) => { spectrum: EssentiaVector }; SpectralPeaks: (vec: EssentiaVector) => { frequencies: EssentiaVector; magnitudes: EssentiaVector; }; HPCP: ( freqs: EssentiaVector, mags: EssentiaVector ) => { hpcp: EssentiaVector }; ChordsDetection: (vec: EssentiaPCVVector) => { chords: EssentiaVector }; module: { VectorVectorFloat: new () => EssentiaPCVVector; }; } let essentia: EssentiaInstance | null = null; async function getEssentia(): Promise { if (essentia) return essentia; try { const { default: EssentiaModule } = await (import('essentia.js') as unknown as { default: { Essentia: new (wasm: unknown) => EssentiaInstance; EssentiaWASM: unknown; }; }); essentia = new EssentiaModule.Essentia(EssentiaModule.EssentiaWASM); return essentia; } catch (error: unknown) { console.error('❌ Essentia WASM failed', (error as Error).message); return null; } } [uploadDir, processedDir].forEach((dir) => { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); }); const storage = multer.diskStorage({ destination: (_req, _file, callback) => { callback(null, uploadDir); }, filename: (_req, file, callback) => { const safeName = file.originalname.replace(/[^a-zA-Z0-9.-]/gu, '_'); callback(null, `${Date.now()}-${safeName}`); }, }); export const upload = multer({ storage, limits: { fileSize: 100 * 1024 * 1024 }, // max 100mb }); const keyMap: Record = { C: 0, 'C#': 1, Db: 1, D: 2, 'D#': 3, Eb: 3, E: 4, F: 5, 'F#': 6, Gb: 6, G: 7, 'G#': 8, Ab: 8, A: 9, 'A#': 10, Bb: 10, B: 11, }; const _generateChords = ( essentiaInstance: EssentiaInstance, signal: Float32Array ) => { const frameSize = 4096; const hopSize = 2048; const pcpVector = new essentiaInstance.module.VectorVectorFloat(); for (let i = 0; i < Math.min(signal.length, 44100 * 30); i += hopSize) { if (i + frameSize > signal.length) break; const frame = signal.slice(i, i + frameSize); const frameVec = essentiaInstance.arrayToVector(frame); const windowed = essentiaInstance.Windowing(frameVec).frame; const spectrum = essentiaInstance.Spectrum(windowed).spectrum; const peaks = essentiaInstance.SpectralPeaks(spectrum); const hpcp = essentiaInstance.HPCP( peaks.frequencies, peaks.magnitudes ).hpcp; pcpVector.push_back(hpcp); frameVec.delete(); } const chordsResult = essentiaInstance.ChordsDetection(pcpVector); const uniqueChords: string[] = []; if (chordsResult?.chords) { const chordsArray = essentiaInstance.vectorToArray( chordsResult.chords ) as string[]; chordsArray.forEach((chord: string) => { if (uniqueChords[uniqueChords.length - 1] !== chord) { uniqueChords.push(chord); } }); } pcpVector.delete(); return uniqueChords; }; const _handleAudioDecoding = ( essentiaInstance: EssentiaInstance, buffer: Buffer, tempWavPath: string, resolve: (value: ChordsResult | PromiseLike) => void, reject: (reason?: unknown) => void ) => { wav .decode(buffer) .then((audioData: { channelData: Float32Array[] }) => { const signal = audioData.channelData[0]; const audioVector = essentiaInstance.arrayToVector(signal); const keyResult = essentiaInstance.KeyExtractor(audioVector); const uniqueChords = _generateChords(essentiaInstance, signal); audioVector.delete(); unlink(tempWavPath, (_error) => {}); resolve({ key: keyResult.key, scale: keyResult.scale, chords: uniqueChords.filter((chord) => chord !== 'N').slice(0, 12), }); }) .catch((error: Error) => { unlink(tempWavPath, (_error) => {}); reject(error); }); }; const detectKeyFromFile = async (filePath: string): Promise => { const essentiaInstance = await getEssentia(); if (!essentiaInstance) throw new Error('Essentia engine not available'); return new Promise((resolve, reject) => { const randomId = randomBytes(4).toString('hex'); const tempWavPath = join(uploadDir, `temp-${Date.now()}-${randomId}.wav`); ffmpeg(filePath) .toFormat('wav') .audioChannels(1) .audioFrequency(44100) .on('end', () => { const buffer = readFileSync(tempWavPath); _handleAudioDecoding( essentiaInstance, buffer, tempWavPath, resolve, reject ); }) .on('error', (error: Error) => { unlink(tempWavPath, (_error) => {}); reject(error); }) .save(tempWavPath); }); }; export const detectKey = async (req: Request, res: Response): Promise => { if (!req.file) { res.status(400).json({ error: 'No file uploaded' }); return; } try { const result = await detectKeyFromFile(req.file.path); res.json(result); } catch (error) { console.error('[KeyChanger] Detection Error:', error); res.status(500).json({ error: 'Audio analysis failed' }); } finally { unlink(req.file.path, (_error) => {}); } }; export const detectProcessedKey = async ( req: Request, res: Response ): Promise => { const filename = String(req.params.filename); const filePath = resolveWithin(processedDir, filename); if (!filePath) { res.status(400).json({ error: 'Invalid path' }); return; } if (!existsSync(filePath)) { res.status(404).json({ error: 'File not found' }); return; } try { const result = await detectKeyFromFile(filePath); res.json(result); } catch (error) { console.error('[KeyChanger] Processed Detection Error:', error); res.status(500).json({ error: 'Analysis failed' }); } }; export const convertKey = (req: Request, res: Response): void => { if (!req.file) { res.status(400).json({ error: 'No file uploaded' }); return; } const { originalKey, targetKey } = req.body; if ( !originalKey || !targetKey || keyMap[originalKey] === undefined || keyMap[targetKey] === undefined ) { unlink(req.file.path, (_error) => {}); res.status(400).json({ error: 'Invalid keys provided' }); return; } const originalVal = keyMap[originalKey]; const targetVal = keyMap[targetKey]; let semitones = targetVal - originalVal; if (semitones > 6) semitones -= 12; if (semitones < -6) semitones += 12; const pitchScale = Math.pow(2, semitones / 12); const inputPath = req.file.path; const extension = extname(req.file.originalname); const baseNamePart = basename(req.file.originalname, extension).replace( /[^a-zA-Z0-9\s.-]/gu, '_' ); const outputFilename = `${Date.now()}__${targetKey}__${baseNamePart}${extension}`; const outputPath = join(processedDir, outputFilename); ffmpeg(inputPath) .audioFilters(`rubberband=pitch=${pitchScale}`) .on('end', () => { const protocol = (req.headers['x-forwarded-proto'] as string) || req.protocol; const host = req.headers.host; res.json({ success: true, filename: outputFilename, downloadUrl: `${protocol}://${host}/api/key-changer/download/${outputFilename}`, }); unlink(inputPath, (_error) => {}); }) .on('error', (error: unknown) => { const errorObj = error as Error; console.error('[KeyChanger] Conversion Error:', errorObj.message); if (!res.headersSent) res .status(500) .json({ error: 'Conversion failed.', details: errorObj.message }); unlink(inputPath, (_error) => {}); }) .save(outputPath); }; export const downloadFile = (req: Request, res: Response): void => { const filename = String(req.params.filename); const filePath = resolveWithin(processedDir, filename); if (!filePath) { res.status(400).json({ error: 'Invalid path' }); return; } if (existsSync(filePath)) { let prettyName = filename; const parts = filename.split('__'); if (parts.length >= 3) { const key = parts[1]; const nameWithExt = parts.slice(2).join('__'); const extension = extname(nameWithExt); const baseNamePart = basename(nameWithExt, extension); const cleanName = baseNamePart.replace(/_+/gu, ' ').trim(); prettyName = `(${key}) ${cleanName}${extension}`; } res.download(filePath, prettyName, (error) => { if (error) { const errorWithCode = error as Error & { code?: string }; if ( errorWithCode.code === 'ECONNABORTED' || errorWithCode.code === 'EPIPE' ) { return; } console.error('[KeyChanger] Download Error:', error); } }); } else { res.status(404).json({ error: 'File not found' }); } };