跳至内容

Jixun's Blog 填坑还是开坑,这是个好问题。

FLAC / MP3 / WAV 批量转 aac

自动检查 ffmpeg 是否有启用 fdkacc-lib,如果没有则使用 native_aac 编码器。

关于编译带有 fdk 支援的 ffmpeg 请参见官方文档;因为 GPL 病毒的原因,就不放出编译版了。

#!/usr/bin/env node
/**
 * FLAC/MP3 to aac script
 * MIT License
 * (C) 2019 Jixun.Moe
 */

/* configuration */
const srcExt = ['flac', 'mp3', 'wav'];
const maxThreads = 4;

/*
 * override with command line options
 * node to_acc.js ./rip ./aac
 */
const srcDir  = process.argv[2] || './rip';
const destDir = process.argv[3] || './aac';

/* import modules */
const fs = require('fs');
const util = require('util');
const readdir = util.promisify(fs.readdir);
const copyFile = util.promisify(fs.copyFile);
const exec = util.promisify(require('child_process').execFile);

/* global variables */
let threads = [];
let files = [];
let aacCodec = 'aac';

function getExt(name) {
	const m = name.toLowerCase().match(/\.(\w+)$/);
	if (m) return m[1];
	return '';
}

async function scanDir(dir, outDir) {
	const scanResult = (await readdir(dir)).filter(d => d[0] != '.');
	for (let i = 0; i < scanResult.length; i++) {
		const inPath = dir + '/' + scanResult[i];
		let outPath = outDir + '/' + scanResult[i];

		const stat = fs.statSync(inPath);

		// sync directory structure.
		if (stat.isDirectory()) {
			// create subdirectory
			try {
				fs.statSync(outPath);
			} catch {
				fs.mkdirSync(outPath);
			}
			await scanDir(inPath, outPath);
			continue;
		}

		const inExt = getExt(inPath);

		// Music file to convert
		if (srcExt.indexOf(inExt) != -1) {
			outPath = outPath.slice(0, -inExt.length) + 'aac';

			try {
				fs.statSync(outPath);

				// file exists, skip...
				continue;
			} catch {}

			files.push([inPath, outPath]);
			continue;
		}
		
		// other file type
		console.info('copy %s...', inPath);
		await copyFile(inPath, outPath);
	}
}

async function doWork(thread) {
	while (true) {
		let work = files.shift();
		if (!work) break;

		const [input, output] = work;
		console.info('[T%d] converting %s...', thread, input);
		await exec('ffmpeg', ['-i', input, '-c:a', aacCodec, '-vbr', '3', output]);
	}

	console.info('thread T%d complete.', thread);
}

async function detectFdk() {
	const {stdout, stderr} = await exec('ffmpeg', ['-codecs']);

	// test for libfdk
	const useFdk = /encoders:[\w\s]* libfdk_aac/.test(stdout);
	console.info('libfdk: %s', useFdk);

	aacCodec = useFdk ? 'libfdk_aac' : 'aac';
}

async function main() {
	// let detect and scan run at the same time.
	const detect = detectFdk();
	await scanDir(srcDir, destDir);
	await detect;

	console.info('to process: %d files.', files.length);

	// create threads
	for(let i = 1; i <= maxThreads; i++) {
		threads.push(doWork(i));
	}

	// wait for thread complete
	// 感谢 orzFly
	await Promise.all(threads);
	
	console.info('done');
}

main();